diff --git a/Dockerfile b/Dockerfile index acfac7ac..cca69042 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM node:22-slim AS frontend RUN corepack enable && corepack prepare pnpm@10.32.1 --activate WORKDIR /app/web COPY web/package.json web/pnpm-lock.yaml ./ +COPY web/vendor/foliate-js ./vendor/foliate-js RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile COPY web/ . @@ -22,6 +23,7 @@ ENV GONOSUMDB=github.com/Silo-Server/* RUN apt-get update && apt-get install -y --no-install-recommends libvips-dev && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY go.mod go.sum ./ +COPY internal/compat/zishang520-webtransport-go/ internal/compat/zishang520-webtransport-go/ RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ go mod download diff --git a/Dockerfile.dev b/Dockerfile.dev index 62848dee..d70cf7cf 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -5,6 +5,7 @@ FROM node:22-slim AS frontend RUN corepack enable && corepack prepare pnpm@10.32.1 --activate WORKDIR /app/web COPY web/package.json web/pnpm-lock.yaml ./ +COPY web/vendor/foliate-js ./vendor/foliate-js RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile COPY web/ . @@ -19,6 +20,7 @@ ENV GONOSUMDB=github.com/Silo-Server/* RUN apt-get update && apt-get install -y --no-install-recommends libvips-dev && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY go.mod go.sum ./ +COPY internal/compat/zishang520-webtransport-go/ internal/compat/zishang520-webtransport-go/ RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ go mod download diff --git a/cmd/silo/main.go b/cmd/silo/main.go index 3718de69..172c7fa2 100644 --- a/cmd/silo/main.go +++ b/cmd/silo/main.go @@ -47,6 +47,7 @@ import ( "github.com/Silo-Server/silo-server/internal/clientip" "github.com/Silo-Server/silo-server/internal/config" "github.com/Silo-Server/silo-server/internal/database" + "github.com/Silo-Server/silo-server/internal/ebooks" evt "github.com/Silo-Server/silo-server/internal/events" "github.com/Silo-Server/silo-server/internal/historyimport" "github.com/Silo-Server/silo-server/internal/imagecache" @@ -72,8 +73,6 @@ import ( "github.com/Silo-Server/silo-server/internal/ratelimit" "github.com/Silo-Server/silo-server/internal/recommendations" mediarequests "github.com/Silo-Server/silo-server/internal/requests" - "github.com/Silo-Server/silo-server/internal/requests/radarr" - "github.com/Silo-Server/silo-server/internal/requests/sonarr" "github.com/Silo-Server/silo-server/internal/s3client" "github.com/Silo-Server/silo-server/internal/scanner" "github.com/Silo-Server/silo-server/internal/scanqueue" @@ -943,6 +942,7 @@ func main() { var seasonRepo *catalog.SeasonRepository var episodeRepo *catalog.EpisodeRepository var audiobookEnricher *audiobooks.Enricher + var ebookEnricher *ebooks.Enricher if needsWorkers && deps.DB != nil && deps.FileRepo != nil { chainRepo := metadata.NewChainRepository(deps.DB) skippedRootRepo = metadata.NewSkippedRootRepository(deps.DB) @@ -1024,6 +1024,14 @@ func main() { personRepo, providerIDRepo, ) + ebookEnricher = ebooks.NewEnricher( + deps.DB, + chainRepo, + pluginResolver, + itemRepo, + personRepo, + providerIDRepo, + ) // Always wire the image resolver so plugin-prefixed URLs (e.g. // metadb://) can be resolved to presigned HTTP URLs in API responses. @@ -1046,6 +1054,9 @@ func main() { audiobookEnricher.SetImageCacher(imageCacher) audiobookEnricher.SetFFmpegPath(scanner.FFmpegPathFromFFprobe(scanner.FFprobePathFromFFmpeg(cfg.Playback.FFmpegPath))) } + if ebookEnricher != nil { + ebookEnricher.SetImageCacher(imageCacher) + } } matchWorker = metadata.NewMatchWorker(metadataService, deps.FileRepo, cfg.Matcher.Workers, cfg.Matcher.BatchSize, 30*time.Second) @@ -1541,7 +1552,8 @@ func main() { catalog.NewProviderIDRepository(deps.DB), ), ) - requestReconcileSvc.SetFulfillmentAdapters(radarr.NewClient(nil), sonarr.NewClient(nil)) + requestReconcileSvc.SetRequesterIdentityResolver(plugins.RequesterIdentityFromLookup(plugins.NewPgUserIdentityLookup(deps.DB))) + api.AttachRequestRouter(requestReconcileSvc, pluginService) if userStoreProvider != nil { reconcileResolver := access.NewResolver( auth.NewUserRepository(deps.DB), @@ -1587,6 +1599,9 @@ func main() { if audiobookEnricher != nil { taskMgr.Register(tasks.NewSyncAudiobookMetadataTask(audiobookEnricher)) } + if ebookEnricher != nil { + taskMgr.Register(tasks.NewSyncEbookMetadataTask(ebookEnricher)) + } if pluginInstallationStore != nil && pluginRuntimeConfigStore != nil && pluginService != nil { pluginTasks, err := plugins.NewTaskRegistryWithTypedResolver(pluginInstallationStore, pluginRuntimeConfigStore, pluginService).Tasks(appCtx) if err != nil { diff --git a/docs/architecture/wholphin-endpoint-coverage.md b/docs/architecture/wholphin-endpoint-coverage.md new file mode 100644 index 00000000..f98ba31c --- /dev/null +++ b/docs/architecture/wholphin-endpoint-coverage.md @@ -0,0 +1,98 @@ +# Wholphin → jellycompat Endpoint Coverage Report + +Date: 2026-06-09 +Scope: every Jellyfin server endpoint the Wholphin Android TV client (jellyfin-sdk-kotlin based) can call, cross-referenced against the routes registered in `internal/jellycompat/router.go`. Wholphin source analyzed from a local clone of `damontecres/Wholphin` (`app/src/main`). Commands assume the repository root is the cwd. + +Wholphin's Seerr integration (`/api/v1/...` discover/request endpoints) targets a separate Seerr server, not Jellyfin, and is out of scope here. + +## Methodology + +1. Grepped Wholphin for every `*Api.method()` SDK call (`itemsApi`, `playStateApi`, etc.), websocket subscriptions, and raw URL construction; mapped each to its HTTP endpoint per the Jellyfin OpenAPI spec. +2. Inventoried every route registered in `internal/jellycompat/router.go`, including path normalization (`path_normalization.go` handles casing and `/emby`//`jellyfin` prefixes). +3. For each endpoint Wholphin uses but silo does not serve, checked whether a server-side gate (user policy flag, DTO field) prevents the client from ever calling it. + +## Summary + +- **Covered (full):** 22 endpoint families — everything on the core path: auth, browse, item detail, episodes/next-up, resume, playback info, direct/HLS streaming, subtitles, progress reporting, favorites/played, images, genres, persons, suggestions, media segments (skip intro), display preferences. +- **Covered (partial/stub):** 4 — websocket (keep-alive only, no remote control), `/Studios`, `/Users/Public`, `/QuickConnect/Enabled` (deliberate `false`). +- **Missing but safely gated:** 8 — the client never calls them because we don't advertise the capability (delete, subtitle download, trickplay, local trailers, additional parts, quick connect auth, live TV, playlists). +- **Missing and reachable:** 4 — theme songs, universal audio stream, item download, client log upload. These return 404 when the client calls them. None block video playback. + +**Bottom line: the entire core video path is covered.** The reachable gaps are quality-of-life features (theme songs, photo screensaver, debug log upload) plus one advertised-but-broken inconsistency (`CanDownload=true` with no `/Items/{id}/Download` route). + +## Coverage matrix + +### Fully covered + +| Wholphin call | Endpoint | silo handler | +|---|---|---| +| `userApi.authenticateUserByName` | `POST /Users/AuthenticateByName` | `handlers_auth.go` | +| `userApi.getCurrentUser` | `GET /Users/Me` | `handlers_auth.go` | +| `userApi.getUserById` | `GET /Users/{id}` | `handlers_auth.go` | +| `systemApi.getPublicSystemInfo` | `GET /System/Info/Public` | `handlers_system.go` | +| `sessionApi.postCapabilities` | `POST /Sessions/Capabilities` | `handlers_playback.go` | +| `itemsApi.getItems` | `GET /Items` | `handlers_items.go` | +| `itemsApi.getResumeItems` | `GET /UserItems/Resume` (+ legacy `/Users/{id}/Items/Resume`) | `handlers_items.go` | +| `userLibraryApi.getItem` | `GET /Items/{id}` (+ legacy user-scoped form) | `handlers_items.go` | +| `tvShowsApi.getEpisodes` | `GET /Shows/{id}/Episodes` | `handlers_items.go` | +| `tvShowsApi.getNextUp` | `GET /Shows/NextUp` | `handlers_items.go` | +| `suggestionsApi.getSuggestions` | `GET /Items/Suggestions` | `handlers_items.go` | +| `genresApi.getGenres` | `GET /Genres` | `handlers_items.go` | +| `personsApi.getPersons` | `GET /Persons` | `handlers_persons.go` (full when PersonRepo is wired; empty stub otherwise) | +| `searchApi.getSearchHints` | `GET /Search/Hints` | `handlers_items.go` (Wholphin's call is currently commented out upstream) | +| `mediaInfoApi.getPostedPlaybackInfo` | `POST /Items/{id}/PlaybackInfo` | `handlers_playback.go` | +| `videosApi.getVideoStreamUrl` | `GET/HEAD /Videos/{id}/stream(.{container})` | `streams.go` | +| (transcode path from PlaybackInfo) | `GET /Videos/{id}/master.m3u8` + HLS playlist/segments | `streams.go` | +| (subtitle delivery URLs) | `GET /Videos/{id}/{source}/Subtitles/{idx}/stream.{fmt}` | `streams.go` | +| `playStateApi.reportPlaybackStart/Progress/Stopped` | `POST /Sessions/Playing[/Progress|/Stopped]` | `streams.go` | +| `playStateApi.markPlayedItem/markUnplayedItem` | `POST/DELETE /UserPlayedItems/{id}` (+ legacy) | `handlers_userdata.go` | +| `userLibraryApi.markFavoriteItem/unmarkFavoriteItem` | `POST/DELETE /UserFavoriteItems/{id}` (+ legacy) | `handlers_userdata.go` | +| `mediaSegmentsApi.getItemSegments` | `GET /MediaSegments/{id}` | `handlers_items.go` (skip intro/credits) | +| `displayPreferencesApi.updateDisplayPreferences` | `GET/POST /DisplayPreferences/{id}` | `handlers_displayprefs.go` | +| `imageApi.getItemImageUrl` | `GET /Items/{id}/Images/{type}[/{index}]` | `handlers_images.go` | +| `imageApi.getUserImageUrl` | `GET /Users/{id}/Images/Primary` | `handlers_images.go` (placeholder avatar) | + +### Partial / stub (graceful degradation) + +| Endpoint | Status | Client impact | +|---|---|---| +| `GET /socket` (websocket) | Accepts upgrade, answers KeepAlive only (`handlers_websocket.go`) | Wholphin subscribes to `GeneralCommandMessage` and `PlaystateMessage` for server-initiated remote control. Connection stays healthy; remote-control commands are simply never delivered. No errors, feature inert. | +| `GET /Studios` | Empty-array stub | Studio browse rows render empty. | +| `GET /Users/Public` | Empty-array stub | No user tiles on the login screen; manual login works. | +| `GET /QuickConnect/Enabled` | Hardcoded `false` | Deliberate: Wholphin hides Quick Connect UI, so the unimplemented `POST /QuickConnect/Authorize` and `POST /Users/AuthenticateWithQuickConnect` are never called. | + +### Missing but safely gated (client never calls them) + +| Endpoint | Why it's unreachable | +|---|---| +| `DELETE /Items/{id}` | We return `CanDelete=false` per item (`mapping.go`) and `EnableContentDeletion=false` in user policy (`handlers_auth.go`), so Wholphin hides delete UI. | +| `POST /Items/{id}/RemoteSearch/Subtitles/{subtitleId}` | `EnableSubtitleManagement=false` in user policy hides subtitle search/download. | +| `GET /Videos/{id}/Trickplay/...` | We never emit trickplay metadata on item DTOs, so the scrubber never requests tiles. | +| `GET /Items/{id}/LocalTrailers` | Wholphin gates on `localTrailerCount > 0` (`TrailerService.kt`); we never set `LocalTrailerCount`. | +| `GET /Videos/{id}/AdditionalParts` | Gated on `partCount > 1` (`PlaylistCreator.kt`); we never set `PartCount`. | +| Quick Connect auth endpoints | Gated by `/QuickConnect/Enabled` = false. | +| Live TV suite (`/LiveTv/Channels`, `/Programs`, `/Recordings`, timers) | We expose no live-TV views or channels, so the feature never activates. | +| Playlists (`GET/POST/DELETE /Playlists/{id}/Items`) | We expose no playlist items in the catalog, so playlist screens are unreachable. Becomes a real gap the day silo grows playlists. | + +### Missing and reachable (return 404 today) + +| Endpoint | Wholphin caller | Impact | Severity | +|---|---|---|---| +| `GET /Items/{id}/ThemeSongs` | `ThemeSongPlayer.kt` | ~~404 → feature silently dead.~~ **Resolved 2026-06-09:** now stubbed with an empty `ThemeMediaResult` (including the `OwnerId` field jellyfin-sdk-kotlin requires). | Resolved | +| `GET /Audio/{id}/universal` | `ThemeSongPlayer.kt`, `MusicService.kt` | Audio streaming for theme songs and music playback. No music libraries are exposed today, but this is the second half of the theme-song path and the blocker for any future audio support in jellycompat. | Low (today) | +| `GET /Items/{id}/Download` | `SlideshowViewModel.kt`, `ScreensaverService.kt` | ~~Inconsistency: `CanDownload=true` while the route 404s.~~ **Resolved 2026-06-09:** a real `/Items/{id}/Download` route now serves the original file (`streams.go`), and `CanDownload=true` again. The earlier `CanDownload=false` workaround broke Infuse, which requires the flag for Direct Play. | Resolved | +| `POST /ClientLog/Document` | `MediaReportService.kt`, `DebugPage.kt` | "Upload logs to server" debug action fails. | Low | +| `GET /Audio/{id}/Lyrics` | `NowPlayingViewModel.kt` | Music-only; unreachable until audio libraries exist. | Low | + +## Recommendations (priority order) + +1. **Merge the repeated-`Fields` fix** (PR #110). `parseItemsQuery` still reads `q.Get("Fields")` (`internal/jellycompat/query.go`), which truncates jellyfin-sdk-kotlin's repeated query params and breaks Wholphin episode auto-advance. This is the only *covered* endpoint with a known correctness bug for this client. Follow up by sweeping the remaining single-value reads (`Ids`, `GenreIds`, `PersonIds`, `Filters`, `SortBy`, `SortOrder`) to `q.Values`, matching how `IncludeItemTypes`/`MediaTypes`/`ImageTypes` are already handled. +2. ~~Resolve the `CanDownload` inconsistency~~ — **done 2026-06-09**: real `/Items/{id}/Download` route added; `CanDownload` stays `true` (Infuse requires it for Direct Play). +3. ~~Stub `GET /Items/{id}/ThemeSongs`~~ — **done 2026-06-09**: empty `ThemeMediaResult` stub registered. +4. **Websocket server-push** (remote control: server-initiated pause/stop, display messages) — tracked in issue #122. +5. **Optional, later:** `POST /ClientLog/Document` accept-and-discard stub and real `/Studios` data. Playlists, lyrics, and universal audio only matter once silo exposes those content types through jellycompat. + +## Notes + +- Path normalization (`path_normalization.go`) already handles Wholphin's casing and any `/emby`//`jellyfin` prefixes, so none of the covered routes are at risk from URL-shape differences. +- Unmatched jellycompat paths fall through to chi's default 404 with no body; jellyfin-sdk-kotlin surfaces these as `InvalidStatusException`, which Wholphin generally catches per-feature. diff --git a/docs/pr124-ebooks-review-hardening.md b/docs/pr124-ebooks-review-hardening.md new file mode 100644 index 00000000..f727fd60 --- /dev/null +++ b/docs/pr124-ebooks-review-hardening.md @@ -0,0 +1,82 @@ +# PR #124 (ebooks) — Review & Hardening Pass + +Date: 2026-06-10 +Scope: full review of the ebook feature branch (`work/ebooks-reader-base`) followed by +fixes for every finding. Two review rounds were performed (a full-PR review, then an +adversarial edge-case/security pass over both the original code and the first round of +fixes), with all changes verified against the full Go test suite, the full web test +suite, production frontend build, ESLint/Prettier, and `make verify-local-paths`. +Commands assume the repository root is the cwd. + +## Security fixes + +- **Stored XSS via book content (high):** book sections render in same-origin blob + iframes with `allow-scripts` (WebKit requirement). A Content-Security-Policy is now + served with all SPA HTML responses (`internal/server/frontend.go`) — blob/srcdoc + documents inherit it, so `script-src 'self' 'wasm-unsafe-eval'` blocks script + execution from book content on all browsers. Threat model documented on the policy + constant. `X-Content-Type-Options: nosniff` added to frontend, jellycompat, and + ebook file responses. +- Ebook file serving can no longer fall through to `application/octet-stream` for an + admitted file (extension/container whitelist and MIME resolution share one resolver), + closing a browser-sniff escape combined with the `?token=` query-auth fallback. +- External links in books are intercepted app-side: http(s) only, opened with + `noopener,noreferrer` (blocks reverse tabnabbing and `javascript:` URLs). +- Request size caps (`http.MaxBytesReader`, 413) on progress/config/annotation writes; + `Content-Disposition` built with `mime.FormatMediaType` (RFC 5987, no header + injection); annotation PATCH is atomic (`SELECT ... FOR UPDATE`) with + presence-aware field semantics and invariant re-validation. +- Verified safe under adversarial review: no IDOR (all reader-state SQL scoped to + user+profile+content), no path traversal, range serving via stdlib, progress input + validation. Accepted residual: CSS-only loads inside book iframes can leak the + book's own content but cannot reach tokens, DOM, or navigation. + +## Reliability fixes + +- **Scanner missing-file reconciliation (critical class):** ebook scans now mark + missing files like video/audio, with real walk-failure tracking (failed roots are + excluded from deletion), symlinked-root support via the shared logical walker, and + the empty-root cleanup allowance — an unmounted share can no longer wipe a library. +- **Enrichment:** provider errors record failures (dedicated `ebook_enrichment_state` + backoff table, capped retries) instead of permanently stamping items done; + unconfigured chains and scan-window races skip without stamping. No longer shares + `media_items.refresh_failures` with the metadata refresh-debt system. +- Ebook items are created `pending` and promoted to `matched` on enrichment (backfill + migration included), making curated-metadata protection real: matched items keep + provider titles/people/series on re-scan (fill-empty only). +- PDF metadata: head+tail window scan (non-linearized PDFs keep the Info dict at the + end), delimiter-aware key matching, head-wins merge. `.md` dropped as an ebook + format; plain `.fb2` reads capped like `.fbz`. +- FK-cascade indexes added for the reader-state tables. + +## Consistency & correctness fixes + +- Hidden-history (`user_history_hidden_items`) gating applied to every ebook progress + surface (watched/in-progress filters, all sort plans, Continue Reading, sort + metrics, `Played` state, recommendation signals) with the exact video semantics. +- `GetItemWatchers` counts distinct watchers (no binge inflation) and the + `minWatchers` floor counts distinct accounts, not profiles; ebook reading now feeds + implicit taste signals and taste-seed candidates; Continue Reading pages past + dismissals and dedupes across pages. +- The 0.9 finished threshold is centralized (`models.EbookFinishedProgressThreshold`). +- Reader frontend: open-flow race teardown (no wrong-file progress saves), ordered + progress writes with `pagehide`/`visibilitychange` flush, settings no longer + clobbered by late server config, TTS stop actually stops, Media Session cleanup, + 512 MiB download guard, fraction bookmarks navigable. + +## Feature completion + +- Native read-state endpoints: `POST/DELETE /watched/{id}` and `/history/remove` + accept ebook content IDs (mark read = progress 1.0 preserving location; mark unread + mirrors video unwatch and clears position; history removal hides without losing + position). Web UI exposes Mark Read/Unread on the item page and card menus, with + Continue Reading dismiss copy and dismissal-path ID encoding fixed. + +## Client follow-ups (Android / Apple) + +- Ebook leaf user data encodes the reading ratio as `position_seconds` (0..1) with + `duration_seconds = 1`; do not render absolute times for ebooks. +- `/watched/{id}` returns `{type: "ebook", affected_count: 1, played: bool}` and the + existing watched SSE event fires. +- Reading progress is deliberately per-book (cross-format): one position per content + ID. diff --git a/docs/superpowers/plans/2026-06-08-ebook-reader-full-parity.md b/docs/superpowers/plans/2026-06-08-ebook-reader-full-parity.md new file mode 100644 index 00000000..729f6992 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-ebook-reader-full-parity.md @@ -0,0 +1,301 @@ +# Ebook Reader Full Parity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bring the core ebook reader up to the personal reader feature set with server-persisted config, annotations/bookmarks, selection tools, reading aids, and advanced settings. + +**Architecture:** Keep ebook reading in core, matching audiobooks: core owns the ebook media type and reader behavior; metadata remains plugin-provided. Extend the existing `/ebooks/{content_id}` reader API and `EbookReaderHandler` rather than creating a separate plugin-style reader subsystem. Persist per-user/per-profile/per-book state in Postgres so settings, annotations, and bookmarks roam across devices. + +**Tech Stack:** Go/Chi handlers, pgx/Postgres migrations, React 19, TanStack Query, Foliate/readest reader wrapper, Vitest, Go unit tests. + +--- + +## File Structure + +- `migrations/sql/20260608000300_ebook_reader_state.sql` + - Creates `ebook_reader_config` and `ebook_reader_annotations`. +- `internal/api/handlers/ebook_reader.go` + - Adds reader config and annotation types, store interfaces, handlers, validation, and Postgres store methods. +- `internal/api/handlers/ebook_reader_test.go` + - Adds handler tests for config, annotation CRUD, access scoping, and validation. +- `internal/api/router.go` + - Wires config/annotation stores and routes under `/ebooks/{content_id}`. +- `web/src/reader/FoliateBookReader.tsx` + - Adds selection/annotation/readable-text/content-popup reader handles and applies saved config. +- `web/src/pages/EbookReader.tsx` + - Adds server-backed settings, annotation/bookmark UI, selection tools, TTS controls, wake-lock/e-ink controls, and advanced reader controls. +- `web/src/hooks/useTTS.ts`, `web/src/hooks/useScreenWakeLock.ts`, `web/src/hooks/useEinkMode.ts` + - Port focused personal-reader hooks into core web. +- `web/src/reader/ebookReaderApi.ts` + - Centralizes reader config and annotation API calls for tests and page usage. +- `web/src/pages/EbookReader.test.tsx`, `web/src/reader/FoliateBookReader.test.ts` + - Adds UI and wrapper tests. + +## Task 1: Server-Persisted Reader Config + +**Files:** +- Create: `migrations/sql/20260608000300_ebook_reader_state.sql` +- Modify: `internal/api/handlers/ebook_reader.go` +- Modify: `internal/api/handlers/ebook_reader_test.go` +- Modify: `internal/api/router.go` +- Create: `web/src/reader/ebookReaderApi.ts` +- Modify: `web/src/pages/EbookReader.tsx` +- Modify: `web/src/pages/EbookReader.test.tsx` + +- [ ] **Step 1: Write failing Go handler tests** + +Add tests that: +- `GET /ebooks/{content_id}/reader-config` returns `{ "config": {} }` when no row exists. +- `PUT /ebooks/{content_id}/reader-config` saves JSON config for the authenticated user/profile/content. +- inaccessible content returns 404 through the existing `ItemAccess` path. +- invalid non-object config returns 400. + +Run: + +```bash +go test ./internal/api/handlers -run 'TestEbookReader.*Config' -count=1 +``` + +Expected: fail because config store/handlers do not exist. + +- [ ] **Step 2: Add migration** + +Create `ebook_reader_config`: + +```sql +CREATE TABLE ebook_reader_config ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id TEXT NOT NULL, + content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, profile_id, content_id) +); + +CREATE INDEX ebook_reader_config_profile_updated + ON ebook_reader_config (user_id, profile_id, updated_at DESC); +``` + +- [ ] **Step 3: Implement config store and handlers** + +Add `EbookReaderConfig`, `EbookReaderConfigStore`, `HandleGetConfig`, `HandleSaveConfig`, and `PGEbookReaderConfigStore` methods in `ebook_reader.go`. + +Validation: +- authenticated user required +- `content_id` required +- `config` must decode to a JSON object +- content access checked with `FileAuthorizer.ItemAccess.EnsureAccessible` + +- [ ] **Step 4: Wire routes and store** + +In `internal/api/router.go`, create the store when `deps.DB != nil`, assign it to `ebookReaderHandler`, and add: + +```go +r.Get("/{content_id}/reader-config", ebookReaderHandler.HandleGetConfig) +r.Put("/{content_id}/reader-config", ebookReaderHandler.HandleSaveConfig) +``` + +- [ ] **Step 5: Add web API wrapper** + +Create `web/src/reader/ebookReaderApi.ts` with: + +```ts +export function ebookReaderConfigPath(contentID: string): string +export async function fetchEbookReaderConfig(contentID: string): Promise> +export async function saveEbookReaderConfig(contentID: string, config: Record): Promise> +``` + +- [ ] **Step 6: Move settings persistence from localStorage to server** + +Update `EbookReader.tsx` to fetch reader config on load and save settings through the config endpoint. Keep a local fallback only until the server value arrives. + +- [ ] **Step 7: Verify and commit** + +Run: + +```bash +go test ./internal/api/handlers -run 'TestEbookReader.*Config' -count=1 +cd web && pnpm test src/pages/EbookReader.test.tsx src/reader/FoliateBookReader.test.ts --run +cd web && pnpm build +``` + +Commit: + +```bash +git add migrations/sql/20260608000300_ebook_reader_state.sql internal/api/handlers/ebook_reader.go internal/api/handlers/ebook_reader_test.go internal/api/router.go web/src/reader/ebookReaderApi.ts web/src/pages/EbookReader.tsx web/src/pages/EbookReader.test.tsx +git commit -m "feat: persist ebook reader config" +``` + +## Task 2: Server-Persisted Annotations And Bookmarks + +**Files:** +- Modify: `migrations/sql/20260608000300_ebook_reader_state.sql` +- Modify: `internal/api/handlers/ebook_reader.go` +- Modify: `internal/api/handlers/ebook_reader_test.go` +- Modify: `internal/api/router.go` +- Modify: `web/src/reader/ebookReaderApi.ts` +- Modify: `web/src/reader/FoliateBookReader.tsx` +- Modify: `web/src/pages/EbookReader.tsx` + +- [ ] **Step 1: Write failing annotation handler tests** + +Cover list, create, update, delete. Test annotation fields: + +```json +{ + "kind": "highlight", + "cfi_range": "epubcfi(/6/4,/1:0,/1:12)", + "selected_text": "sample text", + "note": "note text", + "style": "highlight", + "color": "#facc15" +} +``` + +Run: + +```bash +go test ./internal/api/handlers -run 'TestEbookReader.*Annotation|TestEbookReader.*Bookmark' -count=1 +``` + +Expected: fail because annotation APIs do not exist. + +- [ ] **Step 2: Add annotation table** + +Extend migration with: + +```sql +CREATE TABLE ebook_reader_annotations ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id TEXT NOT NULL, + content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, + kind TEXT NOT NULL CHECK (kind IN ('highlight', 'note', 'bookmark')), + cfi_range TEXT, + location TEXT, + selected_text TEXT NOT NULL DEFAULT '', + note TEXT NOT NULL DEFAULT '', + style TEXT NOT NULL DEFAULT 'highlight', + color TEXT NOT NULL DEFAULT '#facc15', + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CHECK ((kind = 'bookmark' AND location IS NOT NULL) OR (kind <> 'bookmark' AND cfi_range IS NOT NULL)) +); + +CREATE INDEX ebook_reader_annotations_book + ON ebook_reader_annotations (user_id, profile_id, content_id, updated_at DESC); +``` + +- [ ] **Step 3: Implement annotation store and handlers** + +Add handlers: +- `GET /ebooks/{content_id}/annotations` +- `POST /ebooks/{content_id}/annotations` +- `PATCH /ebooks/{content_id}/annotations/{annotation_id}` +- `DELETE /ebooks/{content_id}/annotations/{annotation_id}` + +- [ ] **Step 4: Wire Foliate annotation drawing and selection** + +Extend `FoliateBookReaderHandle` with: +- `createSelectionAnnotation()` +- `clearSelection()` + +Draw stored annotations using Foliate `addAnnotation`. + +- [ ] **Step 5: Add reader UI** + +Add: +- selection popover +- highlight button +- note button +- bookmark button +- annotation list in the side panel +- delete/update note controls + +- [ ] **Step 6: Verify and commit** + +Run handler tests, web tests, and build. Commit: + +```bash +git commit -m "feat: add ebook annotations and bookmarks" +``` + +## Task 3: Reader Tools, TTS, And Reading Aids + +**Files:** +- Create: `web/src/hooks/useTTS.ts` +- Create: `web/src/hooks/useScreenWakeLock.ts` +- Create: `web/src/hooks/useEinkMode.ts` +- Modify: `web/src/reader/FoliateBookReader.tsx` +- Modify: `web/src/pages/EbookReader.tsx` +- Modify: `web/src/pages/EbookReader.test.tsx` + +- [ ] **Step 1: Port focused hooks from personal reader** + +Bring over: +- Web Speech TTS controller +- screen wake lock hook +- e-ink body-class hook + +- [ ] **Step 2: Add readable text handle** + +Expose `getReadableText()` from `FoliateBookReader` for TTS. + +- [ ] **Step 3: Add UI controls** + +Add side-panel controls for: +- Speak current text +- Pause/resume/stop +- voice/rate/pitch +- wake lock +- e-ink mode + +- [ ] **Step 4: Add content popups and helpers** + +Add selection popover actions: +- define +- translate + +Add reader content popup handling for footnotes/images/tables when Foliate emits them. + +- [ ] **Step 5: Verify and commit** + +Run focused web tests and build. Commit: + +```bash +git commit -m "feat: add ebook reader tools and aids" +``` + +## Task 4: Advanced Reader Settings + +**Files:** +- Modify: `web/src/reader/FoliateBookReader.tsx` +- Modify: `web/src/pages/EbookReader.tsx` +- Modify: `web/src/pages/EbookReader.test.tsx` + +- [ ] **Step 1: Add advanced reader settings** + +Add server-persisted settings for: +- RTL +- writing mode +- zoom/scale +- brightness +- hyphenation toggle +- custom font selection/upload if API support is added in a later task + +- [ ] **Step 2: Verify and commit** + +Run focused tests and build. Commit: + +```bash +git commit -m "feat: add ebook advanced reader settings" +``` + +## Self-Review + +- The plan covers server persistence first, matching the user's explicit preference. +- The plan keeps ebook features in core and metadata in plugins, matching the audiobook pattern. +- The plan avoids extra ebook catalog tables; added tables are reader-user-state only. +- The biggest implementation risk is annotation rendering against Foliate selection APIs; Task 2 isolates it after backend persistence exists. +- The migration filename is intentionally after existing ebook migrations and can be adjusted if another migration is added before implementation. diff --git a/docs/superpowers/plans/2026-06-08-ebook-reader-shell-parity.md b/docs/superpowers/plans/2026-06-08-ebook-reader-shell-parity.md new file mode 100644 index 00000000..73ed0a0d --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-ebook-reader-shell-parity.md @@ -0,0 +1,68 @@ +# Ebook Reader Shell Parity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bring the core ebook reader up to the first useful parity slice from `personal/silo-plugin-ebooks`: TOC, search, progress scrub, reader settings, and keyboard navigation. + +**Architecture:** Keep the current core reader API and persistence model. Extend `FoliateBookReader` with imperative methods for TOC, search, href/fraction navigation, and runtime style updates; keep `EbookReader` responsible for the chrome/panels/settings state. Store reader preferences in localStorage for this first local slice so no backend schema is added. + +**Tech Stack:** React 19, React Router, TanStack Query, foliate-js custom element, existing Silo UI components, Vitest. + +--- + +### Task 1: Reader Handle Capabilities + +**Files:** +- Modify: `web/src/reader/FoliateBookReader.tsx` +- Test: `web/src/reader/FoliateBookReader.test.ts` + +- [ ] Add exported types `ReaderSettings`, `ReaderSearchOptions`, `ReaderSearchResult`, and `ReaderReadyState`. +- [ ] Extend `FoliateBookReaderHandle` with `goToFraction`, `goTo`, `search`, `clearSearch`, and `applySettings`. +- [ ] Add a `settings?: ReaderSettings` prop and convert `readerStyles()` to accept settings. +- [ ] Preserve existing progress save/restore behavior. +- [ ] Verify with `pnpm vitest run src/reader/FoliateBookReader.test.ts`. + +### Task 2: TOC and Search Panel + +**Files:** +- Modify: `web/src/pages/EbookReader.tsx` +- Test: `web/src/pages/EbookReader.test.tsx` + +- [ ] Add panel state for `toc`, `searchResults`, `searchTerm`, and current tab. +- [ ] Render side panel tabs for Contents, Search, and Settings. +- [ ] Wire TOC items to `readerRef.current.goTo(href)`. +- [ ] Wire search form to `readerRef.current.search(term)` and result clicks to `goTo(cfi)`. +- [ ] Verify with `pnpm vitest run src/pages/EbookReader.test.tsx`. + +### Task 3: Reader Settings + +**Files:** +- Modify: `web/src/pages/EbookReader.tsx` +- Modify: `web/src/reader/FoliateBookReader.tsx` +- Test: `web/src/pages/EbookReader.test.tsx` + +- [ ] Add localStorage-backed settings for theme, font size, font family, line height, margin, max width, spread, and flow. +- [ ] Render compact controls in the Settings tab. +- [ ] Apply settings live through `FoliateBookReader`. +- [ ] Verify settings persist across remount in tests. + +### Task 4: Progress Scrub and Keyboard Navigation + +**Files:** +- Modify: `web/src/pages/EbookReader.tsx` +- Modify: `web/src/reader/FoliateBookReader.tsx` +- Test: `web/src/pages/EbookReader.test.tsx` + +- [ ] Add a progress range input in the header. +- [ ] Call `goToFraction` on commit/change and update display from relocate events. +- [ ] Add ArrowLeft/ArrowRight keyboard navigation on the reader page. +- [ ] Verify with page-level tests. + +### Task 5: Local Verification + +**Files:** +- No source changes. + +- [ ] Run `cd web && pnpm run build`. +- [ ] Run `GOWORK=off go test ./internal/api/handlers ./internal/scanner`. +- [ ] Keep changes local; do not push. diff --git a/go.mod b/go.mod index 8c01f1cb..bb9ec600 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Silo-Server/silo-server -go 1.26.3 +go 1.26.4 require ( github.com/aws/aws-sdk-go-v2 v1.41.5 @@ -14,8 +14,8 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 // indirect github.com/redis/go-redis/v9 v9.18.0 - golang.org/x/crypto v0.50.0 - golang.org/x/sys v0.43.0 + golang.org/x/crypto v0.52.0 + golang.org/x/sys v0.45.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -32,7 +32,8 @@ require ( github.com/pressly/goose/v3 v3.27.1 github.com/zishang520/socket.io/v2 v2.5.0 go.n16f.net/thumbhash v1.1.0 - golang.org/x/image v0.39.0 + golang.org/x/image v0.41.0 + golang.org/x/net v0.55.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -41,13 +42,13 @@ require ( github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.5 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -55,8 +56,9 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.53.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.60.0 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect @@ -68,15 +70,11 @@ require ( github.com/zishang520/socket.io-go-parser/v2 v2.5.0 // indirect github.com/zishang520/webtransport-go v0.9.1 // indirect go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect - go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/tools v0.43.0 // indirect ) require ( - github.com/Silo-Server/silo-plugin-sdk v0.5.0 + github.com/Silo-Server/silo-plugin-sdk v0.6.0 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect @@ -101,8 +99,10 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/sync v0.20.0 - golang.org/x/text v0.36.0 + golang.org/x/text v0.37.0 golang.org/x/time v0.14.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 ) + +replace github.com/zishang520/webtransport-go => ./internal/compat/zishang520-webtransport-go diff --git a/go.sum b/go.sum index 2e30ad0f..58843ee5 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= -github.com/Silo-Server/silo-plugin-sdk v0.5.0 h1:0yJpD0RP1AetGsWVCXoPih2ggLjN8hVp8blS+2RkIWU= -github.com/Silo-Server/silo-plugin-sdk v0.5.0/go.mod h1:etqmxLTwjxpFH9goAjBDfNDoqHMv2/sqUXu8yx3hNfA= +github.com/Silo-Server/silo-plugin-sdk v0.6.0 h1:Gi9TdH9kt7b8X4xRXH493/nSYb9n0GO4VCWmlll0hKI= +github.com/Silo-Server/silo-plugin-sdk v0.6.0/go.mod h1:etqmxLTwjxpFH9goAjBDfNDoqHMv2/sqUXu8yx3hNfA= github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4= github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= @@ -44,7 +44,6 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,12 +51,12 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= @@ -166,10 +165,14 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI= -github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -222,8 +225,6 @@ github.com/zishang520/socket.io-go-parser/v2 v2.5.0 h1:uGKTwcH2qrrs9uwzfCo3ems+q github.com/zishang520/socket.io-go-parser/v2 v2.5.0/go.mod h1:GK9GIIs/KQbBKfnxgZJMBYSlsFemB2rL7EFUn0kmTfM= github.com/zishang520/socket.io/v2 v2.5.0 h1:+KdZLbl4wWVzyI84RG+h21t8OY4ifz/Oo6qwgt9zHl8= github.com/zishang520/socket.io/v2 v2.5.0/go.mod h1:+GyoPyakXDS6KsW81RAQpDA9+mJBXbcYcQ+Itx2D+rU= -github.com/zishang520/webtransport-go v0.9.1 h1:Y3gqPM8cIDvQILsTyXJ5G9fp2PYqGqLI2z+QXpgboQc= -github.com/zishang520/webtransport-go v0.9.1/go.mod h1:IgNAD6qLe3oWu7MSSkjusRNftpvjYxWjI4LmoH4VEyY= go.n16f.net/thumbhash v1.1.0 h1:aBEvuAd4yiwzeQ7Sm4BZoHJYbrQ1ewjrmrRlCE79snk= go.n16f.net/thumbhash v1.1.0/go.mod h1:mo9pP7WtfdV9ojIamGFR/Vc0PaPA2l0CUtmYQf/SweU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -242,21 +243,19 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= -golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -266,17 +265,15 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= diff --git a/internal/api/autoscan_wiring_test.go b/internal/api/autoscan_wiring_test.go index 926f2f86..b7aebf1c 100644 --- a/internal/api/autoscan_wiring_test.go +++ b/internal/api/autoscan_wiring_test.go @@ -1,6 +1,10 @@ package api -import "testing" +import ( + "testing" + + mediarequests "github.com/Silo-Server/silo-server/internal/requests" +) // TestCheckRequestIntegrationUsable verifies the Fix 4 gate: a reused Requests // connection is rejected when the integration is disabled or has no base_url, so @@ -22,3 +26,31 @@ func TestCheckRequestIntegrationUsable(t *testing.T) { } }) } + +// TestCheckRequestIntegrationUsableGatesReuse is the Task 17 regression test. It +// locks the autoscan→requests credential-reuse behavior after the +// request_integrations table was generalized into the two-tier connection +// registry (Task 9/10): autoscan must keep reusing the Sonarr/Radarr connection +// rows via the same enabled + base_url gate, regardless of the added +// capability/installation columns. +func TestCheckRequestIntegrationUsableGatesReuse(t *testing.T) { + // Enabled + non-blank base_url -> usable (autoscan can reuse the connection). + if err := checkRequestIntegrationUsable("int-1", true, "http://sonarr:8989"); err != nil { + t.Fatalf("enabled integration with base_url should be usable, got: %v", err) + } + // Disabled -> error (autoscan must not poll a disabled connection). + if err := checkRequestIntegrationUsable("int-1", false, "http://sonarr:8989"); err == nil { + t.Fatal("disabled integration must be rejected") + } + // Blank base_url -> error. + if err := checkRequestIntegrationUsable("int-1", true, " "); err == nil { + t.Fatal("blank base_url must be rejected") + } +} + +// Compile-time guard: autoscan's RequestIntegrationLookup reads these fields off +// mediarequests.Integration. If any is renamed/removed, autoscan reuse breaks — +// fail the build here rather than silently at runtime. +var _ = func(in mediarequests.Integration) (string, string, bool) { + return in.BaseURL, in.APIKeyRef, in.Enabled +} diff --git a/internal/api/handlers/audiobook_groups.go b/internal/api/handlers/audiobook_groups.go new file mode 100644 index 00000000..d36ded85 --- /dev/null +++ b/internal/api/handlers/audiobook_groups.go @@ -0,0 +1,108 @@ +package handlers + +import ( + "net/http" + "strconv" + "strings" + + "github.com/Silo-Server/silo-server/internal/catalog" +) + +type audiobookGroupResponse struct { + Name string `json:"name"` + ItemCount int `json:"item_count"` + TotalDurationSeconds int64 `json:"total_duration_seconds"` + InProgressCount int `json:"in_progress_count"` + FinishedCount int `json:"finished_count"` + PosterURLs []string `json:"poster_urls"` +} + +type audiobookGroupsResponse struct { + Total int `json:"total"` + Groups []audiobookGroupResponse `json:"groups"` +} + +// HandleGetAudiobookGroups — GET /api/v1/catalog/audiobook-groups +// +// Grouped browse for audiobook libraries: authors, narrators, or series with +// aggregate stats (book count, total duration, per-profile progress counts) +// and up to four poster URLs for cover stacks. Query parameters: +// library_id= (required), group_by=author|narrator|series (required), +// sort=name|count|duration (default name), limit, offset. +// +// Group names round-trip into the corresponding catalog filter fields +// (author / narrator / series), which match case-insensitively. +func (h *CatalogHandler) HandleGetAudiobookGroups(w http.ResponseWriter, r *http.Request) { + if h == nil || h.itemsH == nil || h.itemsH.browseRepo == nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Catalog is not configured") + return + } + + libraryID, err := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("library_id"))) + if err != nil || libraryID <= 0 { + writeError(w, http.StatusBadRequest, "bad_request", "library_id must be a positive integer") + return + } + + groupBy, ok := catalog.ParseAudiobookGroupBy(r.URL.Query().Get("group_by")) + if !ok { + writeError(w, http.StatusBadRequest, "bad_request", "group_by must be one of author, narrator, series") + return + } + + sort := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("sort"))) + switch sort { + case "", "name", "count", "duration": + default: + writeError(w, http.StatusBadRequest, "bad_request", "sort must be one of name, count, duration") + return + } + + limit := 200 + if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" { + n, parseErr := strconv.Atoi(raw) + if parseErr != nil || n <= 0 { + writeError(w, http.StatusBadRequest, "bad_request", "limit must be a positive integer") + return + } + limit = n + } + offset := max(catalog.ParseIntParam(r.URL.Query().Get("offset")), 0) + + groups, total, err := catalog.ListAudiobookGroups( + r.Context(), + h.itemsH.browseRepo.Pool(), + catalog.AudiobookGroupsQuery{ + LibraryID: libraryID, + GroupBy: groupBy, + Sort: sort, + Limit: limit, + Offset: offset, + }, + h.itemsH.accessFilter(r), + ) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list audiobook groups") + return + } + + resp := audiobookGroupsResponse{Total: total, Groups: make([]audiobookGroupResponse, 0, len(groups))} + for _, g := range groups { + posterURLs := make([]string, 0, len(g.PosterPaths)) + for _, path := range g.PosterPaths { + if url := h.itemsH.presignURL(r, cardThumbnailPath(path), "card"); url != "" { + posterURLs = append(posterURLs, url) + } + } + resp.Groups = append(resp.Groups, audiobookGroupResponse{ + Name: g.Name, + ItemCount: g.ItemCount, + TotalDurationSeconds: g.TotalDurationSeconds, + InProgressCount: g.InProgressCount, + FinishedCount: g.FinishedCount, + PosterURLs: posterURLs, + }) + } + + writeJSON(w, http.StatusOK, resp) +} diff --git a/internal/api/handlers/catalog.go b/internal/api/handlers/catalog.go index 04b33405..38eb6efe 100644 --- a/internal/api/handlers/catalog.go +++ b/internal/api/handlers/catalog.go @@ -10,6 +10,7 @@ import ( "strings" "time" + apimw "github.com/Silo-Server/silo-server/internal/api/middleware" "github.com/Silo-Server/silo-server/internal/catalog" "github.com/Silo-Server/silo-server/internal/models" "github.com/Silo-Server/silo-server/internal/sections" @@ -89,6 +90,7 @@ func (h *CatalogHandler) HandleGetCatalog(w http.ResponseWriter, r *http.Request h.itemsH.accessFilter(r), overlaySummaries, store, + apimw.GetUserID(r.Context()), profileID, ) items := make([]itemListResponse, 0, len(result.Items)) diff --git a/internal/api/handlers/catalog_resources.go b/internal/api/handlers/catalog_resources.go index efce0399..efb38368 100644 --- a/internal/api/handlers/catalog_resources.go +++ b/internal/api/handlers/catalog_resources.go @@ -497,8 +497,8 @@ func (h *CatalogResourceHandler) enrichItemDetail(r *http.Request, detail *catal detail.SeasonUserData = h.items.getAggregateUserData(r, episodes) } } - case "movie", "episode", "audiobook": - detail.SeasonUserData = h.items.getLeafUserData(r, detail.ContentID) + case "movie", "episode", "audiobook", "ebook": + detail.SeasonUserData = h.items.getLeafUserData(r, detail.ContentID, detail.Type) applyEffectiveEditionPreference(detail.SeasonUserData, &detail.EffectiveVersionEditionKey) } diff --git a/internal/api/handlers/ebook_read_state.go b/internal/api/handlers/ebook_read_state.go new file mode 100644 index 00000000..e6ad71c4 --- /dev/null +++ b/internal/api/handlers/ebook_read_state.go @@ -0,0 +1,150 @@ +package handlers + +import ( + "context" + "fmt" + "time" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +// Ebook read state lives exclusively in ebook_reader_progress: every consumer +// (user-state Played, catalog watched/in_progress filters, Continue Reading, +// sort metrics, recommendations) keys off progress crossing +// models.EbookFinishedProgressThreshold. The native watched endpoints therefore +// write that table instead of user_watch_progress/user_watch_history. The +// helpers live in this package — next to the ebook progress store and types — +// because the handler layer is the only producer today: the webhook-import +// matcher (internal/historyimport) only matches movie/series/episode kinds, so +// imported mark-played events can never resolve to an ebook content ID. + +// EbookReadStateStore is the ebook_reader_progress surface needed to set or +// clear an ebook's read state from the watched endpoints. +// *PGEbookReaderProgressStore implements it. +type EbookReadStateStore interface { + Get(ctx context.Context, userID int, profileID string, contentID string) (*EbookReaderProgress, error) + Upsert(ctx context.Context, progress EbookReaderProgress) error + Delete(ctx context.Context, userID int, profileID string, contentID string) error +} + +// EbookReaderProgressReadWriter is the full progress-store surface the items +// handler needs: batch reads for list/user-state responses plus read-state +// writes for the watched endpoints. +type EbookReaderProgressReadWriter interface { + EbookReaderProgressLister + EbookReadStateStore +} + +// markEbookRead marks an ebook as finished by upserting its reader-progress +// row with progress = 1.0 (>= models.EbookFinishedProgressThreshold). An +// existing row keeps its file_id and location so the reader still reopens at +// the user's last position even though the book is marked read; a missing row +// references the item's preferred reader file (resolved lazily via +// defaultFileID, since ebook_reader_progress.file_id is NOT NULL) with an +// empty location meaning "start at the beginning". A fresh updated_at also +// resurfaces a previously hidden book, mirroring how re-watching resurfaces +// hidden video history. +func markEbookRead( + ctx context.Context, + store EbookReadStateStore, + userID int, + profileID, contentID string, + now time.Time, + defaultFileID func(ctx context.Context) (int, error), +) error { + existing, err := store.Get(ctx, userID, profileID, contentID) + if err != nil { + return err + } + progress := EbookReaderProgress{ + UserID: userID, + ProfileID: profileID, + ContentID: contentID, + Progress: 1.0, + UpdatedAt: now, + } + if existing != nil { + progress.FileID = existing.FileID + progress.Location = existing.Location + } else { + fileID, err := defaultFileID(ctx) + if err != nil { + return err + } + progress.FileID = fileID + } + return store.Upsert(ctx, progress) +} + +// markEbookUnread clears an ebook's read state by deleting its reader-progress +// row — the direct analog of the video unwatch path, where +// userstore.ClearProgress DELETEs the user_watch_progress row (dropping the +// resume position along with the watched flag). +func markEbookUnread( + ctx context.Context, + store EbookReadStateStore, + userID int, + profileID, contentID string, +) error { + return store.Delete(ctx, userID, profileID, contentID) +} + +// setEbookReadState applies the watched toggle for an ebook target resolved by +// resolveWatchedTargets, which has already enforced the access filter on the +// item. Scoping matches the rest of the ebook progress code: user_id + +// profile_id. +func (h *ItemsHandler) setEbookReadState( + ctx context.Context, + userID int, + profileID, contentID string, + read bool, + filter catalog.AccessFilter, +) error { + if h.ebookReadStateStore == nil { + return fmt.Errorf("ebook reader progress store is not configured") + } + if !read { + return markEbookUnread(ctx, h.ebookReadStateStore, userID, profileID, contentID) + } + return markEbookRead(ctx, h.ebookReadStateStore, userID, profileID, contentID, time.Now().UTC(), func(ctx context.Context) (int, error) { + return h.defaultEbookFileID(ctx, contentID, filter) + }) +} + +// defaultEbookFileID resolves the file a fresh mark-read progress row should +// reference, respecting the caller's access filter. +func (h *ItemsHandler) defaultEbookFileID(ctx context.Context, contentID string, filter catalog.AccessFilter) (int, error) { + if h.fileRepo == nil { + return 0, catalog.ErrItemNotFound + } + files, err := h.fileRepo.GetByContentID(ctx, contentID) + if err != nil { + return 0, err + } + file := preferredEbookReadFile(catalog.FilterMediaFilesByAccess(files, filter)) + if file == nil { + return 0, catalog.ErrItemNotFound + } + return file.ID, nil +} + +// preferredEbookReadFile mirrors the web reader's default file choice +// (web/src/pages/ItemDetail/EbookContent.tsx preferredReadVersion): prefer an +// EPUB version, otherwise the first reader-supported ebook file. Callers must +// pass access-filtered files. +func preferredEbookReadFile(files []*models.MediaFile) *models.MediaFile { + var first *models.MediaFile + for _, file := range files { + if !isEbookFile(file) { + continue + } + if ebookReaderFormat(file.FilePath, file.Container) == "epub" { + return file + } + if first == nil { + first = file + } + } + return first +} diff --git a/internal/api/handlers/ebook_read_state_test.go b/internal/api/handlers/ebook_read_state_test.go new file mode 100644 index 00000000..94464695 --- /dev/null +++ b/internal/api/handlers/ebook_read_state_test.go @@ -0,0 +1,230 @@ +package handlers + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +// fakeEbookReadStateStore is an in-memory EbookReaderProgressReadWriter keyed +// by content ID for a single (user, profile) scope. +type fakeEbookReadStateStore struct { + rows map[string]EbookReaderProgress +} + +func newFakeEbookReadStateStore() *fakeEbookReadStateStore { + return &fakeEbookReadStateStore{rows: map[string]EbookReaderProgress{}} +} + +func (f *fakeEbookReadStateStore) Get(_ context.Context, _ int, _ string, contentID string) (*EbookReaderProgress, error) { + row, ok := f.rows[contentID] + if !ok { + return nil, nil + } + return &row, nil +} + +func (f *fakeEbookReadStateStore) Upsert(_ context.Context, progress EbookReaderProgress) error { + f.rows[progress.ContentID] = progress + return nil +} + +func (f *fakeEbookReadStateStore) Delete(_ context.Context, _ int, _ string, contentID string) error { + delete(f.rows, contentID) + return nil +} + +func (f *fakeEbookReadStateStore) ListByContentIDs(_ context.Context, _ int, _ string, contentIDs []string) (map[string]EbookReaderProgress, error) { + result := make(map[string]EbookReaderProgress, len(contentIDs)) + for _, contentID := range contentIDs { + if row, ok := f.rows[contentID]; ok { + result[contentID] = row + } + } + return result, nil +} + +// fakeEbookFileProvider implements EpisodeFileProvider for ebook file lookups. +type fakeEbookFileProvider struct { + files map[string][]*models.MediaFile + err error +} + +func (f *fakeEbookFileProvider) GetByContentID(_ context.Context, contentID string) ([]*models.MediaFile, error) { + if f.err != nil { + return nil, f.err + } + return f.files[contentID], nil +} + +func (f *fakeEbookFileProvider) GetByEpisodeID(context.Context, string) ([]*models.MediaFile, error) { + return nil, nil +} + +func (f *fakeEbookFileProvider) ListByContentIDs(context.Context, []string) (map[string][]*models.MediaFile, error) { + return nil, nil +} + +func TestMarkEbookReadPreservesExistingFileAndLocation(t *testing.T) { + ctx := context.Background() + store := newFakeEbookReadStateStore() + store.rows["ebook-1"] = EbookReaderProgress{ + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-1", + FileID: 7, + Location: "epubcfi(/6/14!/4/2/14)", + Progress: 0.42, + UpdatedAt: time.Date(2026, 6, 1, 8, 0, 0, 0, time.UTC), + } + now := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC) + + err := markEbookRead(ctx, store, 42, "profile-1", "ebook-1", now, func(context.Context) (int, error) { + t.Fatal("defaultFileID must not be called when a progress row exists") + return 0, nil + }) + if err != nil { + t.Fatalf("markEbookRead: %v", err) + } + + row := store.rows["ebook-1"] + if row.Progress != 1.0 { + t.Fatalf("progress = %v, want 1.0", row.Progress) + } + if row.FileID != 7 || row.Location != "epubcfi(/6/14!/4/2/14)" { + t.Fatalf("file/location not preserved: %+v", row) + } + if !row.UpdatedAt.Equal(now) { + t.Fatalf("updated_at = %v, want %v", row.UpdatedAt, now) + } +} + +func TestMarkEbookReadUsesDefaultFileWhenNoProgressExists(t *testing.T) { + ctx := context.Background() + store := newFakeEbookReadStateStore() + now := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC) + + err := markEbookRead(ctx, store, 42, "profile-1", "ebook-1", now, func(context.Context) (int, error) { + return 12, nil + }) + if err != nil { + t.Fatalf("markEbookRead: %v", err) + } + + row, ok := store.rows["ebook-1"] + if !ok { + t.Fatal("expected a progress row to be created") + } + if row.Progress != 1.0 || row.FileID != 12 || row.Location != "" { + t.Fatalf("row = %+v, want progress 1.0 with file 12 and empty location", row) + } + if row.UserID != 42 || row.ProfileID != "profile-1" { + t.Fatalf("row scope = user %d profile %q, want user 42 profile-1", row.UserID, row.ProfileID) + } +} + +func TestMarkEbookReadPropagatesDefaultFileError(t *testing.T) { + ctx := context.Background() + store := newFakeEbookReadStateStore() + + err := markEbookRead(ctx, store, 42, "profile-1", "ebook-1", time.Now().UTC(), func(context.Context) (int, error) { + return 0, catalog.ErrItemNotFound + }) + if !errors.Is(err, catalog.ErrItemNotFound) { + t.Fatalf("err = %v, want catalog.ErrItemNotFound", err) + } + if len(store.rows) != 0 { + t.Fatalf("no row must be written on error, got %+v", store.rows) + } +} + +func TestMarkEbookUnreadDeletesProgressRow(t *testing.T) { + ctx := context.Background() + store := newFakeEbookReadStateStore() + store.rows["ebook-1"] = EbookReaderProgress{ContentID: "ebook-1", FileID: 7, Progress: 1.0} + + if err := markEbookUnread(ctx, store, 42, "profile-1", "ebook-1"); err != nil { + t.Fatalf("markEbookUnread: %v", err) + } + if _, ok := store.rows["ebook-1"]; ok { + t.Fatal("progress row must be deleted on mark unread") + } +} + +func TestPreferredEbookReadFilePrefersEpub(t *testing.T) { + pdf := &models.MediaFile{ID: 1, BaseType: "ebook", FilePath: "/books/a.pdf", Container: "pdf"} + epub := &models.MediaFile{ID: 2, BaseType: "ebook", FilePath: "/books/a.epub", Container: "epub"} + video := &models.MediaFile{ID: 3, BaseType: "movie", FilePath: "/movies/a.mkv", Container: "mkv"} + + if got := preferredEbookReadFile([]*models.MediaFile{video, pdf, epub}); got == nil || got.ID != 2 { + t.Fatalf("preferredEbookReadFile = %+v, want epub file 2", got) + } + if got := preferredEbookReadFile([]*models.MediaFile{video, pdf}); got == nil || got.ID != 1 { + t.Fatalf("preferredEbookReadFile = %+v, want first reader-supported file 1", got) + } + if got := preferredEbookReadFile([]*models.MediaFile{video}); got != nil { + t.Fatalf("preferredEbookReadFile = %+v, want nil without ebook files", got) + } +} + +func TestSetEbookReadStateMarkReadShowsPlayedUserState(t *testing.T) { + ctx := context.Background() + store := newFakeEbookReadStateStore() + handler := &ItemsHandler{ + ebookProgressStore: store, + ebookReadStateStore: store, + fileRepo: &fakeEbookFileProvider{files: map[string][]*models.MediaFile{ + "ebook-1": {{ID: 5, BaseType: "ebook", FilePath: "/books/b.epub", Container: "epub"}}, + }}, + } + + if err := handler.setEbookReadState(ctx, 42, "profile-1", "ebook-1", true, catalog.AccessFilter{}); err != nil { + t.Fatalf("setEbookReadState(read): %v", err) + } + + userStore := newProfileTestStore(t) + items := []*models.MediaItem{{ContentID: "ebook-1", Type: "ebook", Title: "Book"}} + states, err := resolveItemUserStatesWithOptions(ctx, userStore, "profile-1", nil, items, itemUserStateOptions{ + UserID: 42, + EbookProgressStore: store, + }) + if err != nil { + t.Fatalf("resolveItemUserStatesWithOptions: %v", err) + } + if states["ebook-1"] == nil || !states["ebook-1"].Played { + t.Fatalf("user state = %+v, want played after mark read", states["ebook-1"]) + } + + // A marked-read book is finished, not in progress: the reader-progress + // response derived from the row must exclude it from Continue Reading. + row := store.rows["ebook-1"] + if row.Progress < models.EbookFinishedProgressThreshold { + t.Fatalf("progress %v must cross the finished threshold", row.Progress) + } + + if err := handler.setEbookReadState(ctx, 42, "profile-1", "ebook-1", false, catalog.AccessFilter{}); err != nil { + t.Fatalf("setEbookReadState(unread): %v", err) + } + if _, ok := store.rows["ebook-1"]; ok { + t.Fatal("mark unread must delete the reader progress row") + } +} + +func TestSetEbookReadStateFailsWithoutAccessibleFile(t *testing.T) { + ctx := context.Background() + store := newFakeEbookReadStateStore() + handler := &ItemsHandler{ + ebookProgressStore: store, + ebookReadStateStore: store, + fileRepo: &fakeEbookFileProvider{}, + } + + err := handler.setEbookReadState(ctx, 42, "profile-1", "ebook-1", true, catalog.AccessFilter{}) + if !errors.Is(err, catalog.ErrItemNotFound) { + t.Fatalf("err = %v, want catalog.ErrItemNotFound", err) + } +} diff --git a/internal/api/handlers/ebook_reader.go b/internal/api/handlers/ebook_reader.go new file mode 100644 index 00000000..40d4865e --- /dev/null +++ b/internal/api/handlers/ebook_reader.go @@ -0,0 +1,1069 @@ +package handlers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "mime" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + + apimw "github.com/Silo-Server/silo-server/internal/api/middleware" + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +// EbookReaderProgress is the persisted reader position for one profile and ebook. +type EbookReaderProgress struct { + UserID int `json:"-"` + ProfileID string `json:"-"` + ContentID string `json:"content_id"` + FileID int `json:"file_id"` + Location string `json:"location"` + Progress float64 `json:"progress"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EbookReaderProgressStore interface { + Get(ctx context.Context, userID int, profileID string, contentID string) (*EbookReaderProgress, error) + Upsert(ctx context.Context, progress EbookReaderProgress) error +} + +// EbookReaderConfig is persisted reader configuration for one profile and ebook. +type EbookReaderConfig struct { + UserID int `json:"-"` + ProfileID string `json:"-"` + ContentID string `json:"content_id"` + Config json.RawMessage `json:"config"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EbookReaderConfigStore interface { + Get(ctx context.Context, userID int, profileID string, contentID string) (*EbookReaderConfig, error) + Upsert(ctx context.Context, config EbookReaderConfig) error +} + +// EbookReaderAnnotation is a persisted reader annotation or bookmark. +type EbookReaderAnnotation struct { + ID string `json:"id"` + UserID int `json:"-"` + ProfileID string `json:"-"` + ContentID string `json:"content_id"` + Kind string `json:"kind"` + CFIRange string `json:"cfi_range,omitempty"` + Location string `json:"location,omitempty"` + SelectedText string `json:"selected_text"` + Note string `json:"note"` + Style string `json:"style"` + Color string `json:"color"` + Metadata json.RawMessage `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EbookReaderAnnotationStore interface { + List(ctx context.Context, userID int, profileID string, contentID string) ([]EbookReaderAnnotation, error) + Create(ctx context.Context, annotation EbookReaderAnnotation) error + // Update atomically rewrites the scoped annotation: the stored row is + // read under a write lock, merge is invoked with the current value, and + // the merged result is written back in the same transaction so concurrent + // PATCHes cannot lose updates. It returns (nil, nil) when no annotation + // matches the scope and propagates merge errors unchanged. + Update( + ctx context.Context, + userID int, + profileID string, + contentID string, + annotationID string, + merge func(existing EbookReaderAnnotation) (EbookReaderAnnotation, error), + ) (*EbookReaderAnnotation, error) + Delete(ctx context.Context, userID int, profileID string, contentID string, annotationID string) error +} + +// EbookReaderHandler serves ebook files for the in-app reader. +type EbookReaderHandler struct { + FileAuthorizer *MediaFileAuthorizer + ProgressStore EbookReaderProgressStore + ConfigStore EbookReaderConfigStore + AnnotationStore EbookReaderAnnotationStore +} + +func NewEbookReaderHandler(authorizer *MediaFileAuthorizer) *EbookReaderHandler { + return &EbookReaderHandler{FileAuthorizer: authorizer} +} + +// HandleReadFile serves an ebook file inline with byte-range support. +func (h *EbookReaderHandler) HandleReadFile(w http.ResponseWriter, r *http.Request) { + if apimw.GetUserID(r.Context()) == 0 { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") + return + } + if h == nil || h.FileAuthorizer == nil { + writeError(w, http.StatusServiceUnavailable, "unavailable", "Ebook reader is not configured") + return + } + + contentID := strings.TrimSpace(chi.URLParam(r, "content_id")) + fileID, err := strconv.Atoi(chi.URLParam(r, "file_id")) + if contentID == "" || err != nil || fileID <= 0 { + writeError(w, http.StatusBadRequest, "bad_request", "content_id and file_id are required") + return + } + + file, err := h.FileAuthorizer.Authorize(r, fileID) + if err != nil { + h.writeReadError(w, err) + return + } + if file == nil || file.ContentID != contentID || !isEbookFile(file) { + writeError(w, http.StatusNotFound, "not_found", "Ebook file not found") + return + } + + if err := serveEbookInline(w, r, file); err != nil { + if errors.Is(err, catalog.ErrItemNotFound) { + writeError(w, http.StatusNotFound, "not_found", "Ebook file not found") + return + } + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to serve ebook file") + } +} + +// Request body caps for the ebook reader write endpoints. Progress payloads +// are tiny; reader config and annotations can carry user text and metadata +// but should never approach these limits. +const ( + ebookReaderProgressMaxBodySize = 16 << 10 // 16 KiB + ebookReaderConfigMaxBodySize = 256 << 10 // 256 KiB + ebookReaderAnnotationMaxBodySize = 256 << 10 // 256 KiB +) + +// decodeEbookReaderBody decodes a JSON request body with a hard size cap. +// It writes the error response and returns false when decoding fails. +func decodeEbookReaderBody(w http.ResponseWriter, r *http.Request, maxBodySize int64, dst any) bool { + r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) + if err := json.NewDecoder(r.Body).Decode(dst); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + writeError(w, http.StatusRequestEntityTooLarge, "too_large", "Request body is too large") + return false + } + writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body") + return false + } + return true +} + +type ebookReaderProgressRequest struct { + FileID int `json:"file_id"` + Location string `json:"location"` + Progress float64 `json:"progress"` +} + +type ebookReaderConfigRequest struct { + Config json.RawMessage `json:"config"` +} + +type ebookReaderAnnotationRequest struct { + Kind string `json:"kind"` + CFIRange string `json:"cfi_range"` + Location string `json:"location"` + SelectedText string `json:"selected_text"` + Note string `json:"note"` + Style string `json:"style"` + Color string `json:"color"` + Metadata json.RawMessage `json:"metadata"` +} + +// ebookReaderAnnotationPatchRequest distinguishes absent fields (nil: keep the +// stored value) from present fields (set, including empty values to clear). +type ebookReaderAnnotationPatchRequest struct { + Kind *string `json:"kind"` + CFIRange *string `json:"cfi_range"` + Location *string `json:"location"` + SelectedText *string `json:"selected_text"` + Note *string `json:"note"` + Style *string `json:"style"` + Color *string `json:"color"` + Metadata json.RawMessage `json:"metadata"` +} + +func (h *EbookReaderHandler) HandleGetProgress(w http.ResponseWriter, r *http.Request) { + userID := apimw.GetUserID(r.Context()) + profileID := apimw.GetProfileID(r.Context()) + if userID == 0 { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") + return + } + if h == nil || h.ProgressStore == nil || h.FileAuthorizer == nil || h.FileAuthorizer.ItemAccess == nil { + writeError(w, http.StatusServiceUnavailable, "unavailable", "Ebook reader progress is not configured") + return + } + + contentID := strings.TrimSpace(chi.URLParam(r, "content_id")) + if contentID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "content_id is required") + return + } + if err := h.FileAuthorizer.ItemAccess.EnsureAccessible(r.Context(), contentID, requestAccessFilter(r)); err != nil { + h.writeReadError(w, err) + return + } + + progress, err := h.ProgressStore.Get(r.Context(), userID, profileID, contentID) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load ebook progress") + return + } + if progress == nil { + writeJSON(w, http.StatusOK, map[string]any{}) + return + } + writeJSON(w, http.StatusOK, progress) +} + +func (h *EbookReaderHandler) HandleSaveProgress(w http.ResponseWriter, r *http.Request) { + userID := apimw.GetUserID(r.Context()) + profileID := apimw.GetProfileID(r.Context()) + if userID == 0 { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") + return + } + if h == nil || h.ProgressStore == nil || h.FileAuthorizer == nil { + writeError(w, http.StatusServiceUnavailable, "unavailable", "Ebook reader progress is not configured") + return + } + + contentID := strings.TrimSpace(chi.URLParam(r, "content_id")) + if contentID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "content_id is required") + return + } + + var req ebookReaderProgressRequest + if !decodeEbookReaderBody(w, r, ebookReaderProgressMaxBodySize, &req) { + return + } + req.Location = strings.TrimSpace(req.Location) + if req.FileID <= 0 || req.Location == "" || req.Progress < 0 || req.Progress > 1 { + writeError(w, http.StatusBadRequest, "bad_request", "file_id, location, and progress are required") + return + } + + file, err := h.FileAuthorizer.Authorize(r, req.FileID) + if err != nil { + h.writeReadError(w, err) + return + } + if file == nil || file.ContentID != contentID || !isEbookFile(file) { + writeError(w, http.StatusNotFound, "not_found", "Ebook file not found") + return + } + + progress := EbookReaderProgress{ + UserID: userID, + ProfileID: profileID, + ContentID: contentID, + FileID: req.FileID, + Location: req.Location, + Progress: req.Progress, + UpdatedAt: time.Now().UTC(), + } + if err := h.ProgressStore.Upsert(r.Context(), progress); err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save ebook progress") + return + } + writeJSON(w, http.StatusOK, progress) +} + +func (h *EbookReaderHandler) HandleGetConfig(w http.ResponseWriter, r *http.Request) { + userID := apimw.GetUserID(r.Context()) + profileID := apimw.GetProfileID(r.Context()) + if userID == 0 { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") + return + } + if h == nil || h.ConfigStore == nil || h.FileAuthorizer == nil || h.FileAuthorizer.ItemAccess == nil { + writeError(w, http.StatusServiceUnavailable, "unavailable", "Ebook reader config is not configured") + return + } + + contentID := strings.TrimSpace(chi.URLParam(r, "content_id")) + if contentID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "content_id is required") + return + } + if err := h.FileAuthorizer.ItemAccess.EnsureAccessible(r.Context(), contentID, requestAccessFilter(r)); err != nil { + h.writeReadError(w, err) + return + } + + config, err := h.ConfigStore.Get(r.Context(), userID, profileID, contentID) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load ebook reader config") + return + } + if config == nil { + writeJSON(w, http.StatusOK, map[string]any{"config": map[string]any{}}) + return + } + writeJSON(w, http.StatusOK, config) +} + +func (h *EbookReaderHandler) HandleSaveConfig(w http.ResponseWriter, r *http.Request) { + userID := apimw.GetUserID(r.Context()) + profileID := apimw.GetProfileID(r.Context()) + if userID == 0 { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") + return + } + if h == nil || h.ConfigStore == nil || h.FileAuthorizer == nil || h.FileAuthorizer.ItemAccess == nil { + writeError(w, http.StatusServiceUnavailable, "unavailable", "Ebook reader config is not configured") + return + } + + contentID := strings.TrimSpace(chi.URLParam(r, "content_id")) + if contentID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "content_id is required") + return + } + + var req ebookReaderConfigRequest + if !decodeEbookReaderBody(w, r, ebookReaderConfigMaxBodySize, &req) { + return + } + if !jsonObject(req.Config) { + writeError(w, http.StatusBadRequest, "bad_request", "config must be a JSON object") + return + } + if err := h.FileAuthorizer.ItemAccess.EnsureAccessible(r.Context(), contentID, requestAccessFilter(r)); err != nil { + h.writeReadError(w, err) + return + } + + config := EbookReaderConfig{ + UserID: userID, + ProfileID: profileID, + ContentID: contentID, + Config: req.Config, + UpdatedAt: time.Now().UTC(), + } + if err := h.ConfigStore.Upsert(r.Context(), config); err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save ebook reader config") + return + } + writeJSON(w, http.StatusOK, config) +} + +func jsonObject(raw json.RawMessage) bool { + if len(raw) == 0 { + return false + } + var value map[string]any + return json.Unmarshal(raw, &value) == nil && value != nil +} + +func (h *EbookReaderHandler) HandleListAnnotations(w http.ResponseWriter, r *http.Request) { + userID, profileID, contentID, ok := h.annotationRequestScope(w, r) + if !ok { + return + } + items, err := h.AnnotationStore.List(r.Context(), userID, profileID, contentID) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load ebook annotations") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": items}) +} + +func (h *EbookReaderHandler) HandleCreateAnnotation(w http.ResponseWriter, r *http.Request) { + userID, profileID, contentID, ok := h.annotationRequestScope(w, r) + if !ok { + return + } + var req ebookReaderAnnotationRequest + if !decodeEbookReaderBody(w, r, ebookReaderAnnotationMaxBodySize, &req) { + return + } + annotation, err := buildEbookReaderAnnotation(req) + if err != nil { + writeError(w, http.StatusBadRequest, "bad_request", err.Error()) + return + } + now := time.Now().UTC() + annotation.ID = uuid.NewString() + annotation.UserID = userID + annotation.ProfileID = profileID + annotation.ContentID = contentID + annotation.CreatedAt = now + annotation.UpdatedAt = now + if err := h.AnnotationStore.Create(r.Context(), annotation); err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create ebook annotation") + return + } + writeJSON(w, http.StatusCreated, annotation) +} + +func (h *EbookReaderHandler) HandleUpdateAnnotation(w http.ResponseWriter, r *http.Request) { + userID, profileID, contentID, ok := h.annotationRequestScope(w, r) + if !ok { + return + } + annotationID := strings.TrimSpace(chi.URLParam(r, "annotation_id")) + if annotationID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "annotation_id is required") + return + } + var req ebookReaderAnnotationPatchRequest + if !decodeEbookReaderBody(w, r, ebookReaderAnnotationMaxBodySize, &req) { + return + } + // The merge runs inside the store transaction between the locked read and + // the write, so concurrent PATCHes serialize instead of losing updates. + var validationErr error + updated, err := h.AnnotationStore.Update( + r.Context(), userID, profileID, contentID, annotationID, + func(existing EbookReaderAnnotation) (EbookReaderAnnotation, error) { + merged, mergeErr := mergeEbookReaderAnnotationPatch(existing, req) + if mergeErr != nil { + validationErr = mergeErr + return EbookReaderAnnotation{}, mergeErr + } + merged.UpdatedAt = time.Now().UTC() + return merged, nil + }, + ) + if validationErr != nil { + writeError(w, http.StatusBadRequest, "bad_request", validationErr.Error()) + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update ebook annotation") + return + } + if updated == nil { + writeError(w, http.StatusNotFound, "not_found", "Ebook annotation not found") + return + } + writeJSON(w, http.StatusOK, updated) +} + +func (h *EbookReaderHandler) HandleDeleteAnnotation(w http.ResponseWriter, r *http.Request) { + userID, profileID, contentID, ok := h.annotationRequestScope(w, r) + if !ok { + return + } + annotationID := strings.TrimSpace(chi.URLParam(r, "annotation_id")) + if annotationID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "annotation_id is required") + return + } + if err := h.AnnotationStore.Delete(r.Context(), userID, profileID, contentID, annotationID); err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete ebook annotation") + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *EbookReaderHandler) annotationRequestScope(w http.ResponseWriter, r *http.Request) (int, string, string, bool) { + userID := apimw.GetUserID(r.Context()) + profileID := apimw.GetProfileID(r.Context()) + if userID == 0 { + writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") + return 0, "", "", false + } + if h == nil || h.AnnotationStore == nil || h.FileAuthorizer == nil || h.FileAuthorizer.ItemAccess == nil { + writeError(w, http.StatusServiceUnavailable, "unavailable", "Ebook annotations are not configured") + return 0, "", "", false + } + contentID := strings.TrimSpace(chi.URLParam(r, "content_id")) + if contentID == "" { + writeError(w, http.StatusBadRequest, "bad_request", "content_id is required") + return 0, "", "", false + } + if err := h.FileAuthorizer.ItemAccess.EnsureAccessible(r.Context(), contentID, requestAccessFilter(r)); err != nil { + h.writeReadError(w, err) + return 0, "", "", false + } + return userID, profileID, contentID, true +} + +// validateEbookReaderAnnotation enforces the annotation invariants shared by +// create and patch: a known kind, a location for bookmarks, a CFI range for +// highlights/notes, and object-shaped metadata. +func validateEbookReaderAnnotation(annotation EbookReaderAnnotation) error { + if annotation.Kind != "highlight" && annotation.Kind != "note" && annotation.Kind != "bookmark" { + return fmt.Errorf("kind must be highlight, note, or bookmark") + } + if annotation.Kind == "bookmark" { + if annotation.Location == "" { + return fmt.Errorf("location is required for bookmarks") + } + } else if annotation.CFIRange == "" { + return fmt.Errorf("cfi_range is required for annotations") + } + if !jsonObject(annotation.Metadata) { + return fmt.Errorf("metadata must be a JSON object") + } + return nil +} + +func buildEbookReaderAnnotation(req ebookReaderAnnotationRequest) (EbookReaderAnnotation, error) { + kind := strings.ToLower(strings.TrimSpace(req.Kind)) + if kind == "" { + kind = "highlight" + } + metadata := req.Metadata + if len(metadata) == 0 { + metadata = json.RawMessage(`{}`) + } + style := strings.TrimSpace(req.Style) + if style == "" { + style = "highlight" + } + color := strings.TrimSpace(req.Color) + if color == "" { + color = "#facc15" + } + annotation := EbookReaderAnnotation{ + Kind: kind, + CFIRange: strings.TrimSpace(req.CFIRange), + Location: strings.TrimSpace(req.Location), + SelectedText: strings.TrimSpace(req.SelectedText), + Note: strings.TrimSpace(req.Note), + Style: style, + Color: color, + Metadata: metadata, + } + if err := validateEbookReaderAnnotation(annotation); err != nil { + return EbookReaderAnnotation{}, err + } + return annotation, nil +} + +// mergeEbookReaderAnnotationPatch applies a PATCH request onto the stored +// annotation. Absent fields keep their stored values; present fields are set +// to the provided value (empty values clear). The merged result is validated +// against the same invariants as annotation creation. +func mergeEbookReaderAnnotationPatch( + existing EbookReaderAnnotation, + req ebookReaderAnnotationPatchRequest, +) (EbookReaderAnnotation, error) { + merged := existing + if req.Kind != nil { + merged.Kind = strings.ToLower(strings.TrimSpace(*req.Kind)) + } + if req.CFIRange != nil { + merged.CFIRange = strings.TrimSpace(*req.CFIRange) + } + if req.Location != nil { + merged.Location = strings.TrimSpace(*req.Location) + } + if req.SelectedText != nil { + merged.SelectedText = strings.TrimSpace(*req.SelectedText) + } + if req.Note != nil { + merged.Note = strings.TrimSpace(*req.Note) + } + if req.Style != nil { + merged.Style = strings.TrimSpace(*req.Style) + } + if req.Color != nil { + merged.Color = strings.TrimSpace(*req.Color) + } + if req.Metadata != nil { + merged.Metadata = req.Metadata + } + if err := validateEbookReaderAnnotation(merged); err != nil { + return EbookReaderAnnotation{}, err + } + return merged, nil +} + +func (h *EbookReaderHandler) writeReadError(w http.ResponseWriter, err error) { + switch { + case errors.Is(err, catalog.ErrItemNotFound), errors.Is(err, catalog.ErrEpisodeNotFound): + writeError(w, http.StatusNotFound, "not_found", "Ebook file not found") + default: + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to authorize ebook file") + } +} + +func serveEbookInline(w http.ResponseWriter, r *http.Request, file *models.MediaFile) error { + f, err := os.Open(file.FilePath) + if err != nil { + if os.IsNotExist(err) { + return catalog.ErrItemNotFound + } + return fmt.Errorf("opening ebook file: %w", err) + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return fmt.Errorf("stat ebook file: %w", err) + } + + w.Header().Set("Content-Type", ebookMimeType(file.FilePath, file.Container)) + w.Header().Set("Content-Disposition", inlineContentDisposition(filepath.Base(file.FilePath))) + // Ebook files are served inline on the API origin (and reachable via the + // ?token= query fallback), so browsers must never content-sniff them. + w.Header().Set("X-Content-Type-Options", "nosniff") + http.ServeContent(w, r, stat.Name(), stat.ModTime(), f) + return nil +} + +// inlineContentDisposition builds an inline Content-Disposition header value. +// mime.FormatMediaType handles quoting, backslash escaping, and RFC 5987 +// (filename*) encoding for non-ASCII names. +func inlineContentDisposition(name string) string { + name = strings.TrimSpace(name) + if name == "" || name == "." || name == string(filepath.Separator) { + name = "ebook" + } + if disposition := mime.FormatMediaType("inline", map[string]string{"filename": name}); disposition != "" { + return disposition + } + return `inline; filename="ebook"` +} + +func isEbookFile(file *models.MediaFile) bool { + if file == nil { + return false + } + if !strings.EqualFold(file.BaseType, "ebook") { + return false + } + return ebookReaderFormat(file.FilePath, file.Container) != "" +} + +// ebookReaderFormat resolves the whitelisted reader format for a file from +// its filename extension, falling back to the catalog container when the +// extension is not a known ebook format. It returns "" when neither identity +// is whitelisted. isEbookFile admission and ebookMimeType both key off this +// resolution, so an admitted file always maps to a concrete ebook MIME type +// and never falls through to application/octet-stream. +func ebookReaderFormat(path, container string) string { + if strings.HasSuffix(strings.ToLower(path), ".fb2.zip") { + return "fbz" + } + if format := normalizeEbookReaderFormat(filepath.Ext(path)); format != "" { + return format + } + return normalizeEbookReaderFormat(container) +} + +// normalizeEbookReaderFormat maps an extension or container value onto the +// ebook reader format whitelist, returning "" for unknown values. +func normalizeEbookReaderFormat(value string) string { + format := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(value)), ".") + switch format { + case "epub", "pdf", "mobi", "azw", "azw3", "cbz", "cbr", "fb2", "fbz": + return format + default: + return "" + } +} + +func ebookMimeType(path, container string) string { + switch ebookReaderFormat(path, container) { + case "epub": + return "application/epub+zip" + case "pdf": + return "application/pdf" + case "mobi": + return "application/x-mobipocket-ebook" + case "azw": + return "application/vnd.amazon.ebook" + case "azw3": + return "application/vnd.amazon.mobi8-ebook" + case "cbz": + return "application/vnd.comicbook+zip" + case "cbr": + return "application/vnd.comicbook-rar" + case "fb2": + return "application/x-fictionbook+xml" + case "fbz": + return "application/x-zip-compressed-fb2" + default: + return "application/octet-stream" + } +} + +type PGEbookReaderProgressStore struct { + pool *pgxpool.Pool +} + +func NewPGEbookReaderProgressStore(pool *pgxpool.Pool) *PGEbookReaderProgressStore { + return &PGEbookReaderProgressStore{pool: pool} +} + +type PGEbookReaderConfigStore struct { + pool *pgxpool.Pool +} + +func NewPGEbookReaderConfigStore(pool *pgxpool.Pool) *PGEbookReaderConfigStore { + return &PGEbookReaderConfigStore{pool: pool} +} + +func (s *PGEbookReaderConfigStore) Get(ctx context.Context, userID int, profileID string, contentID string) (*EbookReaderConfig, error) { + if s == nil || s.pool == nil { + return nil, fmt.Errorf("ebook reader config store is not configured") + } + var config EbookReaderConfig + err := s.pool.QueryRow(ctx, ` + SELECT user_id, profile_id, content_id, config, updated_at + FROM ebook_reader_config + WHERE user_id = $1 AND profile_id = $2 AND content_id = $3`, + userID, profileID, contentID, + ).Scan( + &config.UserID, + &config.ProfileID, + &config.ContentID, + &config.Config, + &config.UpdatedAt, + ) + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("get ebook reader config: %w", err) + } + return &config, nil +} + +func (s *PGEbookReaderConfigStore) Upsert(ctx context.Context, config EbookReaderConfig) error { + if s == nil || s.pool == nil { + return fmt.Errorf("ebook reader config store is not configured") + } + if _, err := s.pool.Exec(ctx, ` + INSERT INTO ebook_reader_config + (user_id, profile_id, content_id, config, updated_at) + VALUES ($1, $2, $3, $4::jsonb, $5) + ON CONFLICT (user_id, profile_id, content_id) DO UPDATE SET + config = EXCLUDED.config, + updated_at = EXCLUDED.updated_at`, + config.UserID, + config.ProfileID, + config.ContentID, + config.Config, + config.UpdatedAt, + ); err != nil { + return fmt.Errorf("upsert ebook reader config: %w", err) + } + return nil +} + +type PGEbookReaderAnnotationStore struct { + pool *pgxpool.Pool +} + +func NewPGEbookReaderAnnotationStore(pool *pgxpool.Pool) *PGEbookReaderAnnotationStore { + return &PGEbookReaderAnnotationStore{pool: pool} +} + +func (s *PGEbookReaderAnnotationStore) List(ctx context.Context, userID int, profileID string, contentID string) ([]EbookReaderAnnotation, error) { + if s == nil || s.pool == nil { + return nil, fmt.Errorf("ebook reader annotation store is not configured") + } + rows, err := s.pool.Query(ctx, ` + SELECT id, user_id, profile_id, content_id, kind, + COALESCE(cfi_range, ''), COALESCE(location, ''), + selected_text, note, style, color, metadata, created_at, updated_at + FROM ebook_reader_annotations + WHERE user_id = $1 AND profile_id = $2 AND content_id = $3 + ORDER BY updated_at DESC`, + userID, profileID, contentID, + ) + if err != nil { + return nil, fmt.Errorf("list ebook reader annotations: %w", err) + } + defer rows.Close() + var items []EbookReaderAnnotation + for rows.Next() { + annotation, err := scanEbookReaderAnnotation(rows) + if err != nil { + return nil, err + } + items = append(items, annotation) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate ebook reader annotations: %w", err) + } + return items, nil +} + +func (s *PGEbookReaderAnnotationStore) Create(ctx context.Context, annotation EbookReaderAnnotation) error { + if s == nil || s.pool == nil { + return fmt.Errorf("ebook reader annotation store is not configured") + } + _, err := s.pool.Exec(ctx, ` + INSERT INTO ebook_reader_annotations + (id, user_id, profile_id, content_id, kind, cfi_range, location, + selected_text, note, style, color, metadata, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), NULLIF($7, ''), + $8, $9, $10, $11, $12::jsonb, $13, $14)`, + annotation.ID, + annotation.UserID, + annotation.ProfileID, + annotation.ContentID, + annotation.Kind, + annotation.CFIRange, + annotation.Location, + annotation.SelectedText, + annotation.Note, + annotation.Style, + annotation.Color, + annotation.Metadata, + annotation.CreatedAt, + annotation.UpdatedAt, + ) + if err != nil { + return fmt.Errorf("create ebook reader annotation: %w", err) + } + return nil +} + +func (s *PGEbookReaderAnnotationStore) Update( + ctx context.Context, + userID int, + profileID string, + contentID string, + annotationID string, + merge func(existing EbookReaderAnnotation) (EbookReaderAnnotation, error), +) (*EbookReaderAnnotation, error) { + if s == nil || s.pool == nil { + return nil, fmt.Errorf("ebook reader annotation store is not configured") + } + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, fmt.Errorf("begin ebook reader annotation update: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + + existing, err := scanEbookReaderAnnotation(tx.QueryRow(ctx, ` + SELECT id, user_id, profile_id, content_id, kind, + COALESCE(cfi_range, ''), COALESCE(location, ''), + selected_text, note, style, color, metadata, created_at, updated_at + FROM ebook_reader_annotations + WHERE id = $1 AND user_id = $2 AND profile_id = $3 AND content_id = $4 + FOR UPDATE`, + annotationID, userID, profileID, contentID, + )) + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("get ebook reader annotation for update: %w", err) + } + + merged, err := merge(existing) + if err != nil { + return nil, err + } + + updated, err := scanEbookReaderAnnotation(tx.QueryRow(ctx, ` + UPDATE ebook_reader_annotations + SET kind = $5, + cfi_range = NULLIF($6, ''), + location = NULLIF($7, ''), + selected_text = $8, + note = $9, + style = $10, + color = $11, + metadata = $12::jsonb, + updated_at = $13 + WHERE id = $1 AND user_id = $2 AND profile_id = $3 AND content_id = $4 + RETURNING id, user_id, profile_id, content_id, kind, + COALESCE(cfi_range, ''), COALESCE(location, ''), + selected_text, note, style, color, metadata, created_at, updated_at`, + annotationID, + userID, + profileID, + contentID, + merged.Kind, + merged.CFIRange, + merged.Location, + merged.SelectedText, + merged.Note, + merged.Style, + merged.Color, + merged.Metadata, + merged.UpdatedAt, + )) + if err != nil { + return nil, fmt.Errorf("update ebook reader annotation: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return nil, fmt.Errorf("commit ebook reader annotation update: %w", err) + } + return &updated, nil +} + +func (s *PGEbookReaderAnnotationStore) Delete(ctx context.Context, userID int, profileID string, contentID string, annotationID string) error { + if s == nil || s.pool == nil { + return fmt.Errorf("ebook reader annotation store is not configured") + } + if _, err := s.pool.Exec(ctx, ` + DELETE FROM ebook_reader_annotations + WHERE id = $1 AND user_id = $2 AND profile_id = $3 AND content_id = $4`, + annotationID, userID, profileID, contentID, + ); err != nil { + return fmt.Errorf("delete ebook reader annotation: %w", err) + } + return nil +} + +func scanEbookReaderAnnotation(scanner interface{ Scan(dest ...any) error }) (EbookReaderAnnotation, error) { + var annotation EbookReaderAnnotation + if err := scanner.Scan( + &annotation.ID, + &annotation.UserID, + &annotation.ProfileID, + &annotation.ContentID, + &annotation.Kind, + &annotation.CFIRange, + &annotation.Location, + &annotation.SelectedText, + &annotation.Note, + &annotation.Style, + &annotation.Color, + &annotation.Metadata, + &annotation.CreatedAt, + &annotation.UpdatedAt, + ); err != nil { + return EbookReaderAnnotation{}, fmt.Errorf("scan ebook reader annotation: %w", err) + } + return annotation, nil +} + +func (s *PGEbookReaderProgressStore) Get(ctx context.Context, userID int, profileID string, contentID string) (*EbookReaderProgress, error) { + if s == nil || s.pool == nil { + return nil, fmt.Errorf("ebook reader progress store is not configured") + } + var progress EbookReaderProgress + err := s.pool.QueryRow(ctx, ` + SELECT user_id, profile_id, content_id, file_id, location, progress, updated_at + FROM ebook_reader_progress + WHERE user_id = $1 AND profile_id = $2 AND content_id = $3`, + userID, profileID, contentID, + ).Scan( + &progress.UserID, + &progress.ProfileID, + &progress.ContentID, + &progress.FileID, + &progress.Location, + &progress.Progress, + &progress.UpdatedAt, + ) + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("get ebook reader progress: %w", err) + } + return &progress, nil +} + +// ListByContentIDs lists reading progress for a set of items, excluding rows +// hidden from history. The hidden gating mirrors the video path +// (internal/userstore/pgstore/progress.go): a row is hidden while its +// updated_at <= hidden_before, and counts again once progress is updated +// after the hide. +func (s *PGEbookReaderProgressStore) ListByContentIDs(ctx context.Context, userID int, profileID string, contentIDs []string) (map[string]EbookReaderProgress, error) { + if s == nil || s.pool == nil { + return nil, fmt.Errorf("ebook reader progress store is not configured") + } + if userID <= 0 || profileID == "" || len(contentIDs) == 0 { + return nil, nil + } + rows, err := s.pool.Query(ctx, ` + SELECT user_id, profile_id, content_id, file_id, location, progress, updated_at + FROM ebook_reader_progress + WHERE user_id = $1 AND profile_id = $2 AND content_id = ANY($3::text[]) + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = ebook_reader_progress.user_id + AND hhi.profile_id = ebook_reader_progress.profile_id + AND hhi.media_item_id = ebook_reader_progress.content_id + AND ebook_reader_progress.updated_at <= hhi.hidden_before + )`, + userID, profileID, contentIDs, + ) + if err != nil { + return nil, fmt.Errorf("list ebook reader progress: %w", err) + } + defer rows.Close() + + progressByContentID := make(map[string]EbookReaderProgress, len(contentIDs)) + for rows.Next() { + var progress EbookReaderProgress + if err := rows.Scan( + &progress.UserID, + &progress.ProfileID, + &progress.ContentID, + &progress.FileID, + &progress.Location, + &progress.Progress, + &progress.UpdatedAt, + ); err != nil { + return nil, fmt.Errorf("scan ebook reader progress: %w", err) + } + progressByContentID[progress.ContentID] = progress + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate ebook reader progress: %w", err) + } + return progressByContentID, nil +} + +func (s *PGEbookReaderProgressStore) Delete(ctx context.Context, userID int, profileID string, contentID string) error { + if s == nil || s.pool == nil { + return fmt.Errorf("ebook reader progress store is not configured") + } + if _, err := s.pool.Exec(ctx, ` + DELETE FROM ebook_reader_progress + WHERE user_id = $1 AND profile_id = $2 AND content_id = $3`, + userID, profileID, contentID, + ); err != nil { + return fmt.Errorf("delete ebook reader progress: %w", err) + } + return nil +} + +func (s *PGEbookReaderProgressStore) Upsert(ctx context.Context, progress EbookReaderProgress) error { + if s == nil || s.pool == nil { + return fmt.Errorf("ebook reader progress store is not configured") + } + if _, err := s.pool.Exec(ctx, ` + INSERT INTO ebook_reader_progress + (user_id, profile_id, content_id, file_id, location, progress, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (user_id, profile_id, content_id) DO UPDATE SET + file_id = EXCLUDED.file_id, + location = EXCLUDED.location, + progress = EXCLUDED.progress, + updated_at = EXCLUDED.updated_at`, + progress.UserID, + progress.ProfileID, + progress.ContentID, + progress.FileID, + progress.Location, + progress.Progress, + progress.UpdatedAt, + ); err != nil { + return fmt.Errorf("upsert ebook reader progress: %w", err) + } + return nil +} diff --git a/internal/api/handlers/ebook_reader_test.go b/internal/api/handlers/ebook_reader_test.go new file mode 100644 index 00000000..063f0bf8 --- /dev/null +++ b/internal/api/handlers/ebook_reader_test.go @@ -0,0 +1,901 @@ +package handlers + +import ( + "context" + "encoding/json" + "errors" + "mime" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/go-chi/chi/v5" + + apimw "github.com/Silo-Server/silo-server/internal/api/middleware" + "github.com/Silo-Server/silo-server/internal/auth" + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +func newEbookReaderAuthRequest(method, path string) *http.Request { + req := httptest.NewRequest(method, path, nil) + ctx := apimw.SetClaims(context.Background(), &auth.Claims{ + UserID: 1, + Role: "user", + TokenType: auth.TokenTypeAccess, + }) + ctx = apimw.SetProfileID(ctx, "profile-1") + return req.WithContext(ctx) +} + +func withEbookReaderRouteParams(req *http.Request, contentID, fileID string) *http.Request { + routeCtx := chi.NewRouteContext() + routeCtx.URLParams.Add("content_id", contentID) + routeCtx.URLParams.Add("file_id", fileID) + return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) +} + +func withEbookReaderContentRouteParam(req *http.Request, contentID string) *http.Request { + routeCtx := chi.NewRouteContext() + routeCtx.URLParams.Add("content_id", contentID) + return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) +} + +func withEbookReaderAnnotationRouteParams(req *http.Request, contentID, annotationID string) *http.Request { + routeCtx := chi.NewRouteContext() + routeCtx.URLParams.Add("content_id", contentID) + routeCtx.URLParams.Add("annotation_id", annotationID) + return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) +} + +type fakeEbookProgressStore struct { + progress *EbookReaderProgress + upserted *EbookReaderProgress + err error +} + +func (s *fakeEbookProgressStore) Get( + context.Context, + int, + string, + string, +) (*EbookReaderProgress, error) { + return s.progress, s.err +} + +func (s *fakeEbookProgressStore) Upsert(_ context.Context, progress EbookReaderProgress) error { + if s.err != nil { + return s.err + } + s.upserted = &progress + return nil +} + +type fakeEbookReaderConfigStore struct { + config *EbookReaderConfig + saved *EbookReaderConfig + getErr error + saveErr error +} + +func (s *fakeEbookReaderConfigStore) Get( + context.Context, + int, + string, + string, +) (*EbookReaderConfig, error) { + return s.config, s.getErr +} + +func (s *fakeEbookReaderConfigStore) Upsert(_ context.Context, config EbookReaderConfig) error { + if s.saveErr != nil { + return s.saveErr + } + s.saved = &config + return nil +} + +type fakeEbookReaderAnnotationStore struct { + items []EbookReaderAnnotation + existing *EbookReaderAnnotation + created *EbookReaderAnnotation + updated *EbookReaderAnnotation + mergedFrom *EbookReaderAnnotation + mergeCalls int + deleted string + err error +} + +func (s *fakeEbookReaderAnnotationStore) List(context.Context, int, string, string) ([]EbookReaderAnnotation, error) { + return s.items, s.err +} + +func (s *fakeEbookReaderAnnotationStore) Create(_ context.Context, annotation EbookReaderAnnotation) error { + if s.err != nil { + return s.err + } + s.created = &annotation + s.items = append(s.items, annotation) + return nil +} + +// Update models the PG store contract: read the stored row, invoke merge with +// it, and persist the merged result, all as one atomic operation. +func (s *fakeEbookReaderAnnotationStore) Update( + _ context.Context, + _ int, + _ string, + _ string, + annotationID string, + merge func(existing EbookReaderAnnotation) (EbookReaderAnnotation, error), +) (*EbookReaderAnnotation, error) { + if s.err != nil { + return nil, s.err + } + if s.existing == nil || s.existing.ID != annotationID { + return nil, nil + } + existing := *s.existing + s.mergedFrom = &existing + s.mergeCalls++ + merged, err := merge(existing) + if err != nil { + return nil, err + } + s.updated = &merged + stored := merged + s.existing = &stored + return &merged, nil +} + +func (s *fakeEbookReaderAnnotationStore) Delete(_ context.Context, userID int, profileID, contentID, annotationID string) error { + if s.err != nil { + return s.err + } + s.deleted = annotationID + return nil +} + +func TestEbookReaderServesEbookInlineWithRangeSupport(t *testing.T) { + filePath := writePlaybackTestMediaFile(t, "book.epub") + if err := os.WriteFile(filePath, []byte("0123456789"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{ + file: &models.MediaFile{ + ID: 42, + ContentID: "ebook-1", + FilePath: filePath, + Container: "epub", + BaseType: "ebook", + }, + }, + ItemAccess: stubItemAccessChecker{}, + }) + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/files/42/read") + req.Header.Set("Range", "bytes=2-5") + req = withEbookReaderRouteParams(req, "ebook-1", "42") + + rr := httptest.NewRecorder() + handler.HandleReadFile(rr, req) + + if rr.Code != http.StatusPartialContent { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if got := rr.Header().Get("Content-Type"); got != "application/epub+zip" { + t.Fatalf("content-type = %q", got) + } + if got := rr.Header().Get("Content-Disposition"); got != "inline; filename=book.epub" { + t.Fatalf("content-disposition = %q", got) + } + if got := rr.Header().Get("X-Content-Type-Options"); got != "nosniff" { + t.Fatalf("x-content-type-options = %q, want nosniff", got) + } + if rr.Body.String() != "2345" { + t.Fatalf("body = %q", rr.Body.String()) + } +} + +func TestEbookReaderServesContainerMimeForMismatchedExtension(t *testing.T) { + filePath := writePlaybackTestMediaFile(t, "book.txt") + if err := os.WriteFile(filePath, []byte("not really"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{ + file: &models.MediaFile{ + ID: 42, + ContentID: "ebook-1", + FilePath: filePath, + Container: "epub", + BaseType: "ebook", + }, + }, + ItemAccess: stubItemAccessChecker{}, + }) + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/files/42/read") + req = withEbookReaderRouteParams(req, "ebook-1", "42") + + rr := httptest.NewRecorder() + handler.HandleReadFile(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + // The file is admitted via its validated container, so the response must + // carry that container's MIME type, never application/octet-stream. + if got := rr.Header().Get("Content-Type"); got != "application/epub+zip" { + t.Fatalf("content-type = %q, want application/epub+zip", got) + } + if got := rr.Header().Get("X-Content-Type-Options"); got != "nosniff" { + t.Fatalf("x-content-type-options = %q, want nosniff", got) + } +} + +func TestEbookMimeTypeNeverReturnsOctetStreamForAdmittedFiles(t *testing.T) { + cases := []struct { + path string + container string + wantMime string + }{ + {"/library/book.txt", "epub", "application/epub+zip"}, + {"/library/book", "pdf", "application/pdf"}, + {"/library/book.epub", "pdf", "application/epub+zip"}, // known extension wins + } + for _, tc := range cases { + file := &models.MediaFile{FilePath: tc.path, Container: tc.container, BaseType: "ebook"} + if !isEbookFile(file) { + t.Fatalf("isEbookFile(%q, %q) = false, want admitted", tc.path, tc.container) + } + if got := ebookMimeType(tc.path, tc.container); got != tc.wantMime { + t.Fatalf("ebookMimeType(%q, %q) = %q, want %q", tc.path, tc.container, got, tc.wantMime) + } + } +} + +func TestEbookReaderRejectsNonEbookFile(t *testing.T) { + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{ + file: &models.MediaFile{ + ID: 42, + ContentID: "movie-1", + FilePath: "/tmp/movie.mkv", + Container: "mkv", + BaseType: "movie", + }, + }, + ItemAccess: stubItemAccessChecker{}, + }) + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/movie-1/files/42/read") + req = withEbookReaderRouteParams(req, "movie-1", "42") + + rr := httptest.NewRecorder() + handler.HandleReadFile(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} + +func TestEbookReaderRecognizesReadestFormats(t *testing.T) { + cases := map[string]string{ + "book.epub": "application/epub+zip", + "book.pdf": "application/pdf", + "book.mobi": "application/x-mobipocket-ebook", + "book.azw": "application/vnd.amazon.ebook", + "book.azw3": "application/vnd.amazon.mobi8-ebook", + "book.cbz": "application/vnd.comicbook+zip", + "book.cbr": "application/vnd.comicbook-rar", + "book.fb2": "application/x-fictionbook+xml", + "book.fbz": "application/x-zip-compressed-fb2", + "book.fb2.zip": "application/x-zip-compressed-fb2", + "book.md": "application/octet-stream", + "book.unknown": "application/octet-stream", + } + rejected := map[string]bool{"book.md": true, "book.unknown": true} + + for name, wantMime := range cases { + t.Run(name, func(t *testing.T) { + container := name + if name == "book.fb2.zip" { + container = "fbz" + } + file := &models.MediaFile{FilePath: "/library/" + name, Container: container} + if rejected[name] { + file.BaseType = "ebook" + if isEbookFile(file) { + t.Fatalf("%s should not be treated as an ebook reader format", name) + } + if got := ebookMimeType(file.FilePath, file.Container); got != wantMime { + t.Fatalf("ebookMimeType() = %q, want %q", got, wantMime) + } + return + } + file.BaseType = "ebook" + if !isEbookFile(file) { + t.Fatal("expected ebook reader format") + } + if got := ebookMimeType(file.FilePath, file.Container); got != wantMime { + t.Fatalf("ebookMimeType() = %q, want %q", got, wantMime) + } + }) + } +} + +func TestEbookReaderRejectsPlainText(t *testing.T) { + file := &models.MediaFile{ + FilePath: "/library/book.txt", + BaseType: "ebook", + } + + if isEbookFile(file) { + t.Fatal("plain text should not be treated as an ebook reader format") + } + if got := ebookMimeType(file.FilePath, file.Container); got == "text/plain; charset=utf-8" { + t.Fatal("plain text should not have an ebook reader MIME type") + } +} + +func TestEbookReaderRecognizesFBZFromCompoundFilenameWithoutContainer(t *testing.T) { + file := &models.MediaFile{ + FilePath: "/library/book.fb2.zip", + BaseType: "ebook", + } + + if !isEbookFile(file) { + t.Fatal("expected .fb2.zip path to be treated as an ebook reader format") + } +} + +func TestEbookReaderMapsAccessFailureToNotFound(t *testing.T) { + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{ + file: &models.MediaFile{ + ID: 42, + ContentID: "ebook-1", + FilePath: "/tmp/book.epub", + Container: "epub", + BaseType: "ebook", + }, + }, + ItemAccess: stubItemAccessChecker{err: catalog.ErrItemNotFound}, + }) + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/files/42/read") + req = withEbookReaderRouteParams(req, "ebook-1", "42") + + rr := httptest.NewRecorder() + handler.HandleReadFile(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} + +func TestEbookReaderReturnsInternalErrorForUnexpectedAuthorizeFailure(t *testing.T) { + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{err: errors.New("db down")}, + ItemAccess: stubItemAccessChecker{}, + }) + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/files/42/read") + req = withEbookReaderRouteParams(req, "ebook-1", "42") + + rr := httptest.NewRecorder() + handler.HandleReadFile(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} + +func TestEbookReaderSavesProgressForAuthorizedEbookFile(t *testing.T) { + store := &fakeEbookProgressStore{} + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{ + file: &models.MediaFile{ + ID: 42, + ContentID: "ebook-1", + FilePath: "/tmp/book.epub", + Container: "epub", + BaseType: "ebook", + }, + }, + ItemAccess: stubItemAccessChecker{}, + }) + handler.ProgressStore = store + + req := httptest.NewRequest( + http.MethodPut, + "/ebooks/ebook-1/progress", + strings.NewReader(`{"file_id":42,"location":"epubcfi(/6/4)","progress":0.42}`), + ) + ctx := apimw.SetClaims(context.Background(), &auth.Claims{ + UserID: 1, + Role: "user", + TokenType: auth.TokenTypeAccess, + }) + req = req.WithContext(apimw.SetProfileID(ctx, "profile-9")) + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleSaveProgress(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.upserted == nil { + t.Fatal("expected progress to be saved") + } + if store.upserted.UserID != 1 || store.upserted.ProfileID != "profile-9" { + t.Fatalf("scope = user %d profile %q", store.upserted.UserID, store.upserted.ProfileID) + } + if store.upserted.ContentID != "ebook-1" || store.upserted.FileID != 42 { + t.Fatalf("target = content %q file %d", store.upserted.ContentID, store.upserted.FileID) + } + if store.upserted.Location != "epubcfi(/6/4)" || store.upserted.Progress != 0.42 { + t.Fatalf("progress = %+v", store.upserted) + } +} + +func TestEbookReaderReturnsSavedProgress(t *testing.T) { + updatedAt := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC) + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.ProgressStore = &fakeEbookProgressStore{ + progress: &EbookReaderProgress{ + UserID: 1, + ProfileID: "profile-1", + ContentID: "ebook-1", + FileID: 42, + Location: "epubcfi(/6/8)", + Progress: 0.75, + UpdatedAt: updatedAt, + }, + } + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/progress") + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleGetProgress(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + for _, want := range []string{ + `"content_id":"ebook-1"`, + `"file_id":42`, + `"location":"epubcfi(/6/8)"`, + `"progress":0.75`, + `"updated_at":"2026-06-08T12:00:00Z"`, + } { + if !strings.Contains(body, want) { + t.Fatalf("response %s missing %s", body, want) + } + } +} + +func TestEbookReaderReturnsEmptyConfigWhenNoneSaved(t *testing.T) { + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.ConfigStore = &fakeEbookReaderConfigStore{} + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/reader-config") + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleGetConfig(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if got := strings.TrimSpace(rr.Body.String()); got != `{"config":{}}` { + t.Fatalf("body = %s", got) + } +} + +func TestEbookReaderSavesConfigForAccessibleEbook(t *testing.T) { + store := &fakeEbookReaderConfigStore{} + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.ConfigStore = store + + req := httptest.NewRequest( + http.MethodPut, + "/ebooks/ebook-1/reader-config", + strings.NewReader(`{"config":{"theme":"dark","flow":"scrolled","fontSize":120}}`), + ) + ctx := apimw.SetClaims(context.Background(), &auth.Claims{ + UserID: 1, + Role: "user", + TokenType: auth.TokenTypeAccess, + }) + req = req.WithContext(apimw.SetProfileID(ctx, "profile-9")) + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleSaveConfig(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.saved == nil { + t.Fatal("expected config to be saved") + } + if store.saved.UserID != 1 || store.saved.ProfileID != "profile-9" || store.saved.ContentID != "ebook-1" { + t.Fatalf("scope = %+v", store.saved) + } + var config map[string]any + if err := json.Unmarshal(store.saved.Config, &config); err != nil { + t.Fatalf("config json: %v", err) + } + if config["theme"] != "dark" || config["flow"] != "scrolled" || config["fontSize"].(float64) != 120 { + t.Fatalf("config = %#v", config) + } +} + +func TestEbookReaderRejectsNonObjectConfig(t *testing.T) { + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.ConfigStore = &fakeEbookReaderConfigStore{} + + req := newEbookReaderAuthRequest(http.MethodPut, "/ebooks/ebook-1/reader-config") + req.Body = http.NoBody + req = httptest.NewRequest( + http.MethodPut, + "/ebooks/ebook-1/reader-config", + strings.NewReader(`{"config":["bad"]}`), + ).WithContext(req.Context()) + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleSaveConfig(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} + +func TestEbookReaderConfigRequiresAccessibleContent(t *testing.T) { + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{err: catalog.ErrItemNotFound}, + }) + handler.ConfigStore = &fakeEbookReaderConfigStore{} + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/reader-config") + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleGetConfig(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} + +func TestEbookReaderCreatesAnnotationForAccessibleEbook(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{} + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.AnnotationStore = store + + req := httptest.NewRequest( + http.MethodPost, + "/ebooks/ebook-1/annotations", + strings.NewReader(`{"kind":"highlight","cfi_range":"epubcfi(/6/4,/1:0,/1:12)","selected_text":"sample text","note":"note text","style":"highlight","color":"#facc15"}`), + ) + ctx := apimw.SetClaims(context.Background(), &auth.Claims{ + UserID: 1, + Role: "user", + TokenType: auth.TokenTypeAccess, + }) + req = req.WithContext(apimw.SetProfileID(ctx, "profile-9")) + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleCreateAnnotation(rr, req) + + if rr.Code != http.StatusCreated { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.created == nil { + t.Fatal("expected annotation to be created") + } + if store.created.UserID != 1 || store.created.ProfileID != "profile-9" || store.created.ContentID != "ebook-1" { + t.Fatalf("scope = %+v", store.created) + } + if store.created.Kind != "highlight" || store.created.CFIRange != "epubcfi(/6/4,/1:0,/1:12)" { + t.Fatalf("annotation = %+v", store.created) + } + if store.created.SelectedText != "sample text" || store.created.Note != "note text" { + t.Fatalf("annotation text = %+v", store.created) + } + if store.created.ID == "" { + t.Fatal("expected generated annotation id") + } +} + +func TestEbookReaderReturnsAnnotations(t *testing.T) { + updatedAt := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC) + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.AnnotationStore = &fakeEbookReaderAnnotationStore{ + items: []EbookReaderAnnotation{ + { + ID: "ann-1", + UserID: 1, + ProfileID: "profile-1", + ContentID: "ebook-1", + Kind: "bookmark", + Location: "epubcfi(/6/8)", + SelectedText: "", + Note: "Saved spot", + Style: "highlight", + Color: "#facc15", + Metadata: json.RawMessage(`{}`), + CreatedAt: updatedAt, + UpdatedAt: updatedAt, + }, + }, + } + + req := newEbookReaderAuthRequest(http.MethodGet, "/ebooks/ebook-1/annotations") + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleListAnnotations(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + for _, want := range []string{`"items":[`, `"id":"ann-1"`, `"kind":"bookmark"`, `"location":"epubcfi(/6/8)"`} { + if !strings.Contains(body, want) { + t.Fatalf("response %s missing %s", body, want) + } + } +} + +func existingEbookReaderAnnotation() *EbookReaderAnnotation { + createdAt := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) + return &EbookReaderAnnotation{ + ID: "ann-1", + UserID: 1, + ProfileID: "profile-1", + ContentID: "ebook-1", + Kind: "highlight", + CFIRange: "epubcfi(/6/4,/1:0,/1:12)", + SelectedText: "sample text", + Note: "original note", + Style: "highlight", + Color: "#facc15", + Metadata: json.RawMessage(`{"chapter":"one"}`), + CreatedAt: createdAt, + UpdatedAt: createdAt, + } +} + +func patchEbookReaderAnnotation(t *testing.T, store *fakeEbookReaderAnnotationStore, body string) *httptest.ResponseRecorder { + t.Helper() + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.AnnotationStore = store + + req := newEbookReaderAuthRequest(http.MethodPatch, "/ebooks/ebook-1/annotations/ann-1") + req.Body = ioNopCloser{strings.NewReader(body)} + req = withEbookReaderAnnotationRouteParams(req, "ebook-1", "ann-1") + + rr := httptest.NewRecorder() + handler.HandleUpdateAnnotation(rr, req) + return rr +} + +func TestEbookReaderPatchKeepsAbsentFields(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{existing: existingEbookReaderAnnotation()} + + rr := patchEbookReaderAnnotation(t, store, `{"note":"updated","color":"#bfdbfe"}`) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.updated == nil || store.updated.ID != "ann-1" || store.updated.Note != "updated" || store.updated.Color != "#bfdbfe" { + t.Fatalf("updated = %+v", store.updated) + } + if string(store.updated.Metadata) != `{"chapter":"one"}` { + t.Fatalf("metadata should be preserved when absent, got %s", store.updated.Metadata) + } + if store.updated.Kind != "highlight" || store.updated.CFIRange != "epubcfi(/6/4,/1:0,/1:12)" { + t.Fatalf("absent fields should keep stored values, got %+v", store.updated) + } +} + +func TestEbookReaderPatchClearsExplicitlyEmptyFields(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{existing: existingEbookReaderAnnotation()} + + rr := patchEbookReaderAnnotation(t, store, `{"note":"","metadata":{}}`) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.updated == nil || store.updated.Note != "" { + t.Fatalf("note should be cleared, got %+v", store.updated) + } + if string(store.updated.Metadata) != `{}` { + t.Fatalf("metadata should be cleared to {}, got %s", store.updated.Metadata) + } +} + +func TestEbookReaderPatchValidatesResultingState(t *testing.T) { + cases := map[string]string{ + "kind change without location": `{"kind":"bookmark"}`, + "clearing required cfi_range": `{"cfi_range":""}`, + "unknown kind": `{"kind":"doodle"}`, + "non-object metadata": `{"metadata":["bad"]}`, + } + for name, body := range cases { + t.Run(name, func(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{existing: existingEbookReaderAnnotation()} + rr := patchEbookReaderAnnotation(t, store, body) + if rr.Code != http.StatusBadRequest { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.updated != nil { + t.Fatalf("invalid patch must not reach the store, got %+v", store.updated) + } + }) + } +} + +func TestEbookReaderPatchAllowsKindChangeWithRequiredFields(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{existing: existingEbookReaderAnnotation()} + + rr := patchEbookReaderAnnotation(t, store, `{"kind":"bookmark","location":"epubcfi(/6/8)"}`) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.updated == nil || store.updated.Kind != "bookmark" || store.updated.Location != "epubcfi(/6/8)" { + t.Fatalf("updated = %+v", store.updated) + } +} + +func TestEbookReaderPatchMergesOntoRowReadInsideStoreUpdate(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{existing: existingEbookReaderAnnotation()} + + rr := patchEbookReaderAnnotation(t, store, `{"note":"merged note"}`) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + // The handler must not pre-read and merge outside the store: the merge + // runs exactly once, on the row handed over by the (locked) store read. + if store.mergeCalls != 1 { + t.Fatalf("merge calls = %d, want 1", store.mergeCalls) + } + if store.mergedFrom == nil || store.mergedFrom.Note != "original note" { + t.Fatalf("merge input = %+v, want the stored row", store.mergedFrom) + } + if store.updated == nil || store.updated.Note != "merged note" { + t.Fatalf("written row = %+v, want merged note", store.updated) + } + if store.updated.CFIRange != store.mergedFrom.CFIRange || store.updated.Kind != store.mergedFrom.Kind { + t.Fatalf("written row %+v must keep unpatched fields from merge input %+v", store.updated, store.mergedFrom) + } + if !store.updated.UpdatedAt.After(store.mergedFrom.UpdatedAt) { + t.Fatalf("updated_at %v must advance past %v", store.updated.UpdatedAt, store.mergedFrom.UpdatedAt) + } +} + +func TestEbookReaderPatchReturnsNotFoundForMissingAnnotation(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{} + + rr := patchEbookReaderAnnotation(t, store, `{"note":"updated"}`) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} + +func TestEbookReaderRejectsOversizedProgressBody(t *testing.T) { + store := &fakeEbookProgressStore{} + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.ProgressStore = store + + body := `{"file_id":42,"location":"` + strings.Repeat("x", int(ebookReaderProgressMaxBodySize)) + `","progress":0.5}` + req := newEbookReaderAuthRequest(http.MethodPut, "/ebooks/ebook-1/progress") + req.Body = ioNopCloser{strings.NewReader(body)} + req = withEbookReaderContentRouteParam(req, "ebook-1") + + rr := httptest.NewRecorder() + handler.HandleSaveProgress(rr, req) + + if rr.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.upserted != nil { + t.Fatalf("oversized body must not be saved, got %+v", store.upserted) + } +} + +func TestInlineContentDispositionProducesParseableHeaders(t *testing.T) { + cases := map[string]string{ + "book.epub": "book.epub", + `bo"ok\.epub`: `bo"ok\.epub`, + `trailing-slash\`: `trailing-slash\`, + "börk \U0001F4DA.epub": "börk \U0001F4DA.epub", + "": "ebook", + ".": "ebook", + } + for name, want := range cases { + t.Run(name, func(t *testing.T) { + got := inlineContentDisposition(name) + mediaType, params, err := mime.ParseMediaType(got) + if err != nil { + t.Fatalf("header %q is malformed: %v", got, err) + } + if mediaType != "inline" { + t.Fatalf("media type = %q", mediaType) + } + if params["filename"] != want { + t.Fatalf("filename = %q, want %q", params["filename"], want) + } + }) + } +} + +func TestEbookReaderDeletesAnnotation(t *testing.T) { + store := &fakeEbookReaderAnnotationStore{} + handler := NewEbookReaderHandler(&MediaFileAuthorizer{ + FileResolver: stubMediaFileResolver{}, + ItemAccess: stubItemAccessChecker{}, + }) + handler.AnnotationStore = store + + req := newEbookReaderAuthRequest(http.MethodDelete, "/ebooks/ebook-1/annotations/ann-1") + req = withEbookReaderAnnotationRouteParams(req, "ebook-1", "ann-1") + + rr := httptest.NewRecorder() + handler.HandleDeleteAnnotation(rr, req) + + if rr.Code != http.StatusNoContent { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + if store.deleted != "ann-1" { + t.Fatalf("deleted = %q", store.deleted) + } +} + +type ioNopCloser struct { + *strings.Reader +} + +func (c ioNopCloser) Close() error { return nil } diff --git a/internal/api/handlers/favorites.go b/internal/api/handlers/favorites.go index ef562ee2..17bc0f33 100644 --- a/internal/api/handlers/favorites.go +++ b/internal/api/handlers/favorites.go @@ -40,6 +40,7 @@ type PersonalDataHandler struct { localFavoriteDispatcher LocalFavoriteEventDispatcher profileStaler ProfileStaler profileRefreshRequester ProfileRefreshRequester + ebookProgressStore EbookReaderProgressLister } // NewPersonalDataHandler creates a new PersonalDataHandler. @@ -55,6 +56,10 @@ func (h *PersonalDataHandler) SetDetailService(svc *catalog.DetailService) { h.detailSvc = svc } +func (h *PersonalDataHandler) SetEbookReaderProgressStore(store EbookReaderProgressLister) { + h.ebookProgressStore = store +} + // SetProfileStaler configures an optional staleness trigger for taste profiles. func (h *PersonalDataHandler) SetProfileStaler(ps ProfileStaler) { h.profileStaler = ps @@ -552,7 +557,10 @@ func resolveItemsByIDs(h *PersonalDataHandler, r *http.Request, ids []string) ([ userStates := map[string]*itemUserStateResponse{} if store, profileID, ok := h.userStoreForRequest(r); ok { - if resolvedStates, err := resolveItemUserStates(r.Context(), store, profileID, h.episodeRepo, accessibleItems); err == nil { + if resolvedStates, err := resolveItemUserStatesWithOptions(r.Context(), store, profileID, h.episodeRepo, accessibleItems, itemUserStateOptions{ + UserID: apimw.GetUserID(r.Context()), + EbookProgressStore: h.ebookProgressStore, + }); err == nil { userStates = resolvedStates } } @@ -669,7 +677,11 @@ func (h *PersonalDataHandler) resolveHistoryRemovalMediaItemIDs( return nil, err } switch item.Type { - case "movie": + case "movie", "ebook": + // Hiding an ebook gates ebook_reader_progress reads via + // user_history_hidden_items (updated_at <= hidden_before) + // without deleting the progress row, so the reader position + // survives: hidden is not the same as unread. return []string{item.ContentID}, nil case "series": return h.seriesEpisodeIDs(ctx, item.ContentID) diff --git a/internal/api/handlers/favorites_history_remove_test.go b/internal/api/handlers/favorites_history_remove_test.go new file mode 100644 index 00000000..9295d23a --- /dev/null +++ b/internal/api/handlers/favorites_history_remove_test.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" + "github.com/Silo-Server/silo-server/internal/userstore" +) + +type fakeHistoryItemRepo struct { + items map[string]*models.MediaItem + inaccessible map[string]bool +} + +func (r *fakeHistoryItemRepo) GetByID(_ context.Context, contentID string) (*models.MediaItem, error) { + item, ok := r.items[contentID] + if !ok { + return nil, catalog.ErrItemNotFound + } + return item, nil +} + +func (r *fakeHistoryItemRepo) GetByIDs(_ context.Context, contentIDs []string) ([]*models.MediaItem, error) { + result := make([]*models.MediaItem, 0, len(contentIDs)) + for _, contentID := range contentIDs { + if item, ok := r.items[contentID]; ok { + result = append(result, item) + } + } + return result, nil +} + +func (r *fakeHistoryItemRepo) EnsureAccessible(_ context.Context, contentID string, _ catalog.AccessFilter) error { + if r.inaccessible[contentID] { + return catalog.ErrItemNotFound + } + return nil +} + +func TestHandleRemoveHistoryAcceptsEbookTargets(t *testing.T) { + ctx := context.Background() + store := newProfileTestStore(t) + handler := NewPersonalDataHandler(testUserStoreProvider{store: store}, &fakeHistoryItemRepo{ + items: map[string]*models.MediaItem{ + "ebook-1": {ContentID: "ebook-1", Type: "ebook", Title: "Book"}, + }, + }) + + // Seed a history entry keyed by the ebook content ID so the removal has + // something to hide. RemoveHistoryItems only touches watch history, + // watch progress, and the hidden-items gate — PersonalDataHandler has no + // write access to ebook_reader_progress (its store interface is read-only), + // so the reader position survives: hidden is not the same as unread. + if err := store.AddHistory(ctx, userstore.WatchHistoryEntry{ + ProfileID: "profile-1", + MediaItemID: "ebook-1", + WatchedAt: "2026-06-01T10:00:00Z", + Completed: true, + Source: userstore.WatchHistorySourceManual, + }); err != nil { + t.Fatalf("AddHistory: %v", err) + } + + req := newAuthorizedProfileRequestWithRole( + http.MethodPost, + "/history/remove", + `{"targets":[{"content_id":"ebook-1"}]}`, + "user", + "profile-1", + ) + rr := httptest.NewRecorder() + handler.HandleRemoveHistory(rr, req) + + if rr.Code != http.StatusNoContent { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } + + entries, err := store.ListHistory(ctx, "profile-1", 10, 0) + if err != nil { + t.Fatalf("ListHistory: %v", err) + } + if len(entries) != 0 { + t.Fatalf("history entries = %+v, want hidden after removal", entries) + } +} + +func TestHandleRemoveHistoryRejectsInaccessibleEbook(t *testing.T) { + store := newProfileTestStore(t) + handler := NewPersonalDataHandler(testUserStoreProvider{store: store}, &fakeHistoryItemRepo{ + items: map[string]*models.MediaItem{ + "ebook-1": {ContentID: "ebook-1", Type: "ebook", Title: "Book"}, + }, + inaccessible: map[string]bool{"ebook-1": true}, + }) + + req := newAuthorizedProfileRequestWithRole( + http.MethodPost, + "/history/remove", + `{"targets":[{"content_id":"ebook-1"}]}`, + "user", + "profile-1", + ) + rr := httptest.NewRecorder() + handler.HandleRemoveHistory(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String()) + } +} diff --git a/internal/api/handlers/items.go b/internal/api/handlers/items.go index 43358491..7697073c 100644 --- a/internal/api/handlers/items.go +++ b/internal/api/handlers/items.go @@ -46,6 +46,10 @@ type LocalWatchEventDispatcher interface { HandleLocalWatchEvent(ctx context.Context, event watchsync.LocalWatchEvent) error } +type EbookReaderProgressLister interface { + ListByContentIDs(ctx context.Context, userID int, profileID string, contentIDs []string) (map[string]EbookReaderProgress, error) +} + // ItemsHandler handles browse, search, item detail, and series endpoints. type ItemsHandler struct { browseRepo *catalog.BrowseRepository @@ -62,6 +66,8 @@ type ItemsHandler struct { profileRefreshRequester ProfileRefreshRequester metadataRefreshRequester MetadataRefreshRequester localWatchDispatcher LocalWatchEventDispatcher + ebookProgressStore EbookReaderProgressLister + ebookReadStateStore EbookReadStateStore EventsHub *evt.Hub UserRepo *auth.UserRepository } @@ -114,6 +120,11 @@ func (h *ItemsHandler) SetLocalWatchEventDispatcher(dispatcher LocalWatchEventDi h.localWatchDispatcher = dispatcher } +func (h *ItemsHandler) SetEbookReaderProgressStore(store EbookReaderProgressReadWriter) { + h.ebookProgressStore = store + h.ebookReadStateStore = store +} + func (h *ItemsHandler) maybeRequestStaleDetailMetadataRefresh(ctx context.Context, detail *catalog.ItemDetail) { if h == nil || detail == nil || h.metadataRefreshRequester == nil { return @@ -225,6 +236,9 @@ type sortMetricsResponse struct { ProgressRatio *float64 `json:"progress_ratio,omitempty"` ViewedAt string `json:"viewed_at,omitempty"` PlayCount *int `json:"play_count,omitempty"` + Author string `json:"author,omitempty"` + Narrator string `json:"narrator,omitempty"` + SeriesName string `json:"series_name,omitempty"` } // browseResponse is the paginated response for the /items endpoint. @@ -373,8 +387,8 @@ func (h *ItemsHandler) HandleGetWatchDetail(w http.ResponseWriter, r *http.Reque } } - if detail.Type == "movie" || detail.Type == "episode" { - detail.UserData = h.getLeafUserData(r, detail.ContentID) + if detail.Type == "movie" || detail.Type == "episode" || detail.Type == "ebook" { + detail.UserData = h.getLeafUserData(r, detail.ContentID, detail.Type) applyEffectiveEditionPreference(detail.UserData, &detail.EffectiveVersionEditionKey) } @@ -429,7 +443,8 @@ func (h *ItemsHandler) handleSetWatchedState(w http.ResponseWriter, r *http.Requ return } - targetType, targets, err := h.resolveWatchedTargets(r.Context(), id, h.accessFilter(r)) + filter := h.accessFilter(r) + targetType, targets, err := h.resolveWatchedTargets(r.Context(), id, filter) if err != nil { switch { case isNotFound(err): @@ -440,12 +455,16 @@ func (h *ItemsHandler) handleSetWatchedState(w http.ResponseWriter, r *http.Requ return } - if h.watchState == nil { + switch { + case targetType == "ebook": + // Ebook read state lives in ebook_reader_progress, not in + // user_watch_progress/user_watch_history; watch providers do not sync + // books, so no local watch event is dispatched. + err = h.setEbookReadState(r.Context(), userID, profileID, id, played, filter) + case h.watchState == nil: writeError(w, http.StatusInternalServerError, "internal_error", "Failed to access user store") return - } - - if played { + case played: leafTargets := make([]watchstate.LeafWatchTarget, 0, len(targets)) for _, target := range targets { leafTargets = append(leafTargets, watchstate.LeafWatchTarget{ @@ -459,7 +478,7 @@ func (h *ItemsHandler) handleSetWatchedState(w http.ResponseWriter, r *http.Requ if err == nil { h.dispatchLocalWatchEvent(r.Context(), watchsync.LocalWatchEventMarkedWatched, userID, profileID, result) } - } else { + default: targetIDs := make([]string, 0, len(targets)) for _, target := range targets { targetIDs = append(targetIDs, target.ContentID) @@ -471,6 +490,10 @@ func (h *ItemsHandler) handleSetWatchedState(w http.ResponseWriter, r *http.Requ } } if err != nil { + if isNotFound(err) { + writeError(w, http.StatusNotFound, "not_found", "Item not found") + return + } writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update watched state") return } @@ -739,7 +762,10 @@ func (h *ItemsHandler) listItemUserStates(r *http.Request, items []*models.Media if !ok { return map[string]*itemUserStateResponse{} } - states, err := resolveItemUserStates(r.Context(), store, profileID, h.episodeRepo, items) + states, err := resolveItemUserStatesWithOptions(r.Context(), store, profileID, h.episodeRepo, items, itemUserStateOptions{ + UserID: apimw.GetUserID(r.Context()), + EbookProgressStore: h.ebookProgressStore, + }) if err != nil { return map[string]*itemUserStateResponse{} } @@ -850,6 +876,7 @@ func (h *ItemsHandler) listSortMetrics( filter catalog.AccessFilter, overlaySummaries map[string]*models.OverlaySummary, store userstore.UserStore, + userID int, profileID string, ) map[string]*sortMetricsResponse { metrics := make(map[string]*sortMetricsResponse, len(items)) @@ -890,16 +917,97 @@ func (h *ItemsHandler) listSortMetrics( } } case "progress", "date_viewed", "plays": - h.listUserSortMetrics(ctx, items, sortField, store, profileID, metrics) + h.listUserSortMetrics(ctx, items, sortField, store, userID, profileID, metrics) + case "author", "narrator", "series": + h.listAudiobookSortMetrics(ctx, items, sortField, metrics) } return metrics } +func (h *ItemsHandler) listAudiobookSortMetrics( + ctx context.Context, + items []*models.MediaItem, + sortField string, + metrics map[string]*sortMetricsResponse, +) { + if h == nil || h.browseRepo == nil || h.browseRepo.Pool() == nil || len(items) == 0 { + return + } + contentIDs := uniqueItemContentIDs(items) + if len(contentIDs) == 0 { + return + } + + var query string + switch sortField { + case "author": + query = audiobookPersonSortMetricQuery(int(models.PersonKindAuthor)) + case "narrator": + query = audiobookPersonSortMetricQuery(int(models.PersonKindNarrator)) + case "series": + query = ` + SELECT target.content_id, BTRIM(s.series_name) AS value + FROM unnest($1::text[]) WITH ORDINALITY AS target(content_id, ord) + JOIN audiobook_series s ON s.content_id = target.content_id + WHERE BTRIM(s.series_name) <> ''` + default: + return + } + + rows, err := h.browseRepo.Pool().Query(ctx, query, contentIDs) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var contentID, value string + if err := rows.Scan(&contentID, &value); err != nil { + return + } + value = strings.TrimSpace(value) + if contentID == "" || value == "" { + continue + } + resp := metrics[contentID] + if resp == nil { + resp = &sortMetricsResponse{} + metrics[contentID] = resp + } + switch sortField { + case "author": + resp.Author = value + case "narrator": + resp.Narrator = value + case "series": + resp.SeriesName = value + } + } +} + +func audiobookPersonSortMetricQuery(kind int) string { + return fmt.Sprintf(` + SELECT target.content_id, person.name AS value + FROM unnest($1::text[]) WITH ORDINALITY AS target(content_id, ord) + JOIN LATERAL ( + SELECT BTRIM(p.name) AS name + FROM item_people ip + JOIN people p ON p.id = ip.person_id + WHERE ip.content_id = target.content_id + AND ip.kind = %d + AND p.name IS NOT NULL + AND BTRIM(p.name) <> '' + ORDER BY p.name ASC + LIMIT 1 + ) person ON TRUE`, kind) +} + func (h *ItemsHandler) listUserSortMetrics( ctx context.Context, items []*models.MediaItem, sortField string, store userstore.UserStore, + userID int, profileID string, metrics map[string]*sortMetricsResponse, ) { @@ -915,6 +1023,10 @@ func (h *ItemsHandler) listUserSortMetrics( if err != nil { return } + ebookProgressMap, err := h.listEbookReaderProgressByContentIDs(ctx, userID, profileID, contentIDs) + if err != nil { + return + } switch sortField { case "progress": @@ -923,10 +1035,16 @@ func (h *ItemsHandler) listUserSortMetrics( continue } progress, ok := progressMap[item.ContentID] - if !ok || progress.Completed || progress.PositionSeconds <= 0 || progress.DurationSeconds <= 0 { + if ok && !progress.Completed && progress.PositionSeconds > 0 && progress.DurationSeconds > 0 { + ratio := progress.PositionSeconds / progress.DurationSeconds + metrics[item.ContentID] = &sortMetricsResponse{ProgressRatio: &ratio} continue } - ratio := progress.PositionSeconds / progress.DurationSeconds + ebookProgress, ok := ebookProgressMap[item.ContentID] + if !ok || ebookProgress.Progress <= 0 || ebookProgress.Progress >= models.EbookFinishedProgressThreshold { + continue + } + ratio := ebookProgress.Progress metrics[item.ContentID] = &sortMetricsResponse{ProgressRatio: &ratio} } case "date_viewed", "plays": @@ -955,6 +1073,12 @@ func (h *ItemsHandler) listUserSortMetrics( if progress, ok := progressMap[item.ContentID]; ok && progress.Completed && progress.UpdatedAt > viewedAt { viewedAt = progress.UpdatedAt } + if progress, ok := ebookProgressMap[item.ContentID]; ok && progress.Progress >= models.EbookFinishedProgressThreshold { + ebookViewedAt := progress.UpdatedAt.UTC().Format(time.RFC3339) + if ebookViewedAt > viewedAt { + viewedAt = ebookViewedAt + } + } if viewedAt == "" { continue } @@ -964,6 +1088,9 @@ func (h *ItemsHandler) listUserSortMetrics( if progress, ok := progressMap[item.ContentID]; ok && progress.Completed && playCount < 1 { playCount = 1 } + if progress, ok := ebookProgressMap[item.ContentID]; ok && progress.Progress >= models.EbookFinishedProgressThreshold && playCount < 1 { + playCount = 1 + } if playCount <= 0 { continue } @@ -974,6 +1101,18 @@ func (h *ItemsHandler) listUserSortMetrics( } } +func (h *ItemsHandler) listEbookReaderProgressByContentIDs( + ctx context.Context, + userID int, + profileID string, + contentIDs []string, +) (map[string]EbookReaderProgress, error) { + if h == nil || h.ebookProgressStore == nil || userID <= 0 || profileID == "" || len(contentIDs) == 0 { + return nil, nil + } + return h.ebookProgressStore.ListByContentIDs(ctx, userID, profileID, contentIDs) +} + func uniqueItemContentIDs(items []*models.MediaItem) []string { contentIDs := make([]string, 0, len(items)) seen := make(map[string]struct{}, len(items)) @@ -1110,7 +1249,11 @@ func (h *ItemsHandler) toSeasonResponse(r *http.Request, seriesID string, s *mod return resp } -func (h *ItemsHandler) getLeafUserData(r *http.Request, contentID string) *catalog.SeasonUserData { +func (h *ItemsHandler) getLeafUserData(r *http.Request, contentID string, itemType ...string) *catalog.SeasonUserData { + if len(itemType) > 0 && itemType[0] == "ebook" { + return h.getEbookLeafUserData(r, contentID) + } + store, profileID, ok := h.userStoreForRequest(r) if !ok { return nil @@ -1124,7 +1267,7 @@ func (h *ItemsHandler) getLeafUserData(r *http.Request, contentID string) *catal return &catalog.SeasonUserData{ PositionSeconds: progress.PositionSeconds, DurationSeconds: progress.DurationSeconds, - IsInProgress: !progress.Completed && progress.PositionSeconds > 0, + IsInProgress: progress.PositionSeconds > 0, Played: progress.Completed, LastFileID: progress.LastFileID, LastResolution: progress.LastResolution, @@ -1134,6 +1277,38 @@ func (h *ItemsHandler) getLeafUserData(r *http.Request, contentID string) *catal } } +func (h *ItemsHandler) getEbookLeafUserData(r *http.Request, contentID string) *catalog.SeasonUserData { + if h == nil || h.ebookProgressStore == nil { + return nil + } + userID := apimw.GetUserID(r.Context()) + profileID := requestProfileID(r) + if userID <= 0 || profileID == "" || contentID == "" { + return nil + } + + progress, err := h.ebookProgressStore.ListByContentIDs(r.Context(), userID, profileID, []string{contentID}) + if err != nil { + return nil + } + row, ok := progress[contentID] + if !ok || row.Progress <= 0 { + return nil + } + + // Ebooks have no playback duration, so PositionSeconds/DurationSeconds + // encode the 0..1 reading ratio (position=ratio, duration=1). Clients can + // derive percent-complete from the pair as usual, but rendering them as + // absolute times is meaningless for ebooks. Changing the wire shape needs + // coordinated Android/Apple client updates. + return &catalog.SeasonUserData{ + PositionSeconds: row.Progress, + DurationSeconds: 1, + IsInProgress: row.Progress < models.EbookFinishedProgressThreshold, + Played: row.Progress >= models.EbookFinishedProgressThreshold, + } +} + func applyEffectiveEditionPreference(userData *catalog.SeasonUserData, target **string) { if userData == nil || userData.LastEditionKey == nil || *userData.LastEditionKey == "" { return @@ -1178,6 +1353,16 @@ func (h *ItemsHandler) getAggregateUserData(r *http.Request, episodes []*models. } } +// requestProfileID resolves the active profile for a request, falling back to +// the X-Profile-Id header when the auth context does not carry one. +func requestProfileID(r *http.Request) string { + profileID := apimw.GetProfileID(r.Context()) + if profileID == "" { + profileID = r.Header.Get("X-Profile-Id") + } + return profileID +} + func (h *ItemsHandler) userStoreForRequest(r *http.Request) (userstore.UserStore, string, bool) { if h.storeProvider == nil { return nil, "", false @@ -1188,10 +1373,7 @@ func (h *ItemsHandler) userStoreForRequest(r *http.Request) (userstore.UserStore return nil, "", false } - profileID := apimw.GetProfileID(r.Context()) - if profileID == "" { - profileID = r.Header.Get("X-Profile-Id") - } + profileID := requestProfileID(r) if profileID == "" { return nil, "", false } @@ -1385,6 +1567,10 @@ func (h *ItemsHandler) resolveWatchedTargets(ctx context.Context, contentID stri ContentID: item.ContentID, DurationSeconds: h.contentDurationSeconds(ctx, item.ContentID, "", item.Runtime), }}, nil + case "ebook": + // Ebooks have no playback duration; read state is keyed off + // ebook_reader_progress, so the leaf target only carries the ID. + return "ebook", []watchedLeafTarget{{ContentID: item.ContentID}}, nil case "series": if h.episodeRepo == nil { return "", nil, catalog.ErrItemNotFound diff --git a/internal/api/handlers/items_sort_metrics_test.go b/internal/api/handlers/items_sort_metrics_test.go index b8b77ff0..e80e32cc 100644 --- a/internal/api/handlers/items_sort_metrics_test.go +++ b/internal/api/handlers/items_sort_metrics_test.go @@ -40,20 +40,145 @@ func TestListSortMetricsIncludesUserStateSorts(t *testing.T) { } } - progressMetrics := handler.listSortMetrics(ctx, items, "progress", catalog.AccessFilter{}, nil, store, "profile-1") + progressMetrics := handler.listSortMetrics(ctx, items, "progress", catalog.AccessFilter{}, nil, store, 0, "profile-1") progress := progressMetrics["movie-progress"] if progress == nil || progress.ProgressRatio == nil || *progress.ProgressRatio != 0.25 { t.Fatalf("progress metrics = %#v", progress) } - viewedMetrics := handler.listSortMetrics(ctx, items, "date_viewed", catalog.AccessFilter{}, nil, store, "profile-1") + viewedMetrics := handler.listSortMetrics(ctx, items, "date_viewed", catalog.AccessFilter{}, nil, store, 0, "profile-1") if got := viewedMetrics["movie-viewed"]; got == nil || got.ViewedAt != "2026-05-29T18:30:00Z" { t.Fatalf("viewed metrics = %#v", got) } - playsMetrics := handler.listSortMetrics(ctx, items, "plays", catalog.AccessFilter{}, nil, store, "profile-1") + playsMetrics := handler.listSortMetrics(ctx, items, "plays", catalog.AccessFilter{}, nil, store, 0, "profile-1") plays := playsMetrics["movie-plays"] if plays == nil || plays.PlayCount == nil || *plays.PlayCount != 2 { t.Fatalf("plays metrics = %#v", plays) } } + +func TestListSortMetricsIncludesEbookReaderProgress(t *testing.T) { + ctx := context.Background() + store := newProfileTestStore(t) + updatedAt := time.Date(2026, 6, 3, 9, 15, 0, 0, time.UTC) + handler := &ItemsHandler{ + ebookProgressStore: &fakeEbookReaderProgressLister{ + progress: map[string]EbookReaderProgress{ + "ebook-progress": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-progress", + Progress: 0.37, + UpdatedAt: updatedAt, + }, + "ebook-complete": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-complete", + Progress: 0.95, + UpdatedAt: updatedAt.Add(time.Hour), + }, + }, + }, + } + items := []*models.MediaItem{ + {ContentID: "ebook-progress", Type: "ebook", Title: "Progress"}, + {ContentID: "ebook-complete", Type: "ebook", Title: "Complete"}, + } + + progressMetrics := handler.listSortMetrics(ctx, items, "progress", catalog.AccessFilter{}, nil, store, 42, "profile-1") + progress := progressMetrics["ebook-progress"] + if progress == nil || progress.ProgressRatio == nil || *progress.ProgressRatio != 0.37 { + t.Fatalf("progress metrics = %#v", progress) + } + + viewedMetrics := handler.listSortMetrics(ctx, items, "date_viewed", catalog.AccessFilter{}, nil, store, 42, "profile-1") + if got := viewedMetrics["ebook-complete"]; got == nil || got.ViewedAt != "2026-06-03T10:15:00Z" { + t.Fatalf("viewed metrics = %#v", got) + } + + playsMetrics := handler.listSortMetrics(ctx, items, "plays", catalog.AccessFilter{}, nil, store, 42, "profile-1") + plays := playsMetrics["ebook-complete"] + if plays == nil || plays.PlayCount == nil || *plays.PlayCount != 1 { + t.Fatalf("plays metrics = %#v", plays) + } +} + +func TestListSortMetricsExcludesHiddenEbookReaderProgress(t *testing.T) { + ctx := context.Background() + store := newProfileTestStore(t) + updatedAt := time.Date(2026, 6, 3, 9, 15, 0, 0, time.UTC) + handler := &ItemsHandler{ + ebookProgressStore: &fakeEbookReaderProgressLister{ + progress: map[string]EbookReaderProgress{ + "ebook-hidden": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-hidden", + Progress: 0.95, + UpdatedAt: updatedAt, + }, + "ebook-reread": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-reread", + Progress: 0.95, + UpdatedAt: updatedAt.Add(2 * time.Hour), + }, + }, + hiddenBefore: map[string]time.Time{ + "ebook-hidden": updatedAt.Add(time.Hour), + "ebook-reread": updatedAt.Add(time.Hour), + }, + }, + } + items := []*models.MediaItem{ + {ContentID: "ebook-hidden", Type: "ebook", Title: "Hidden"}, + {ContentID: "ebook-reread", Type: "ebook", Title: "Reread"}, + } + + viewedMetrics := handler.listSortMetrics(ctx, items, "date_viewed", catalog.AccessFilter{}, nil, store, 42, "profile-1") + if got := viewedMetrics["ebook-hidden"]; got != nil { + t.Fatalf("hidden ebook must have no viewed metrics, got %#v", got) + } + if got := viewedMetrics["ebook-reread"]; got == nil || got.ViewedAt != "2026-06-03T11:15:00Z" { + t.Fatalf("progress updated after hidden_before must count again, got %#v", got) + } + + playsMetrics := handler.listSortMetrics(ctx, items, "plays", catalog.AccessFilter{}, nil, store, 42, "profile-1") + if got := playsMetrics["ebook-hidden"]; got != nil { + t.Fatalf("hidden ebook must have no play count, got %#v", got) + } + if got := playsMetrics["ebook-reread"]; got == nil || got.PlayCount == nil || *got.PlayCount != 1 { + t.Fatalf("progress updated after hidden_before must count again, got %#v", got) + } +} + +type fakeEbookReaderProgressLister struct { + progress map[string]EbookReaderProgress + // hiddenBefore models user_history_hidden_items: like the PG store, a + // row is excluded while progress.UpdatedAt <= hidden_before and counts + // again once the progress row is updated after the hide. + hiddenBefore map[string]time.Time +} + +func (f *fakeEbookReaderProgressLister) ListByContentIDs( + _ context.Context, + _ int, + _ string, + contentIDs []string, +) (map[string]EbookReaderProgress, error) { + result := make(map[string]EbookReaderProgress, len(contentIDs)) + for _, contentID := range contentIDs { + progress, ok := f.progress[contentID] + if !ok { + continue + } + if hiddenBefore, hidden := f.hiddenBefore[contentID]; hidden && !progress.UpdatedAt.After(hiddenBefore) { + continue + } + result[contentID] = progress + } + return result, nil +} diff --git a/internal/api/handlers/items_user_data_test.go b/internal/api/handlers/items_user_data_test.go new file mode 100644 index 00000000..81f111d6 --- /dev/null +++ b/internal/api/handlers/items_user_data_test.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "net/http/httptest" + "testing" + "time" + + apimw "github.com/Silo-Server/silo-server/internal/api/middleware" + "github.com/Silo-Server/silo-server/internal/auth" +) + +func TestGetLeafUserDataUsesEbookReaderProgress(t *testing.T) { + handler := &ItemsHandler{ + ebookProgressStore: &fakeEbookReaderProgressLister{ + progress: map[string]EbookReaderProgress{ + "ebook-progress": { + UserID: 7, + ProfileID: "profile-1", + ContentID: "ebook-progress", + Progress: 0.42, + UpdatedAt: time.Date(2026, 6, 5, 10, 0, 0, 0, time.UTC), + }, + "ebook-complete": { + UserID: 7, + ProfileID: "profile-1", + ContentID: "ebook-complete", + Progress: 0.93, + UpdatedAt: time.Date(2026, 6, 5, 11, 0, 0, 0, time.UTC), + }, + }, + }, + } + req := httptest.NewRequest("GET", "/items/ebook-progress", nil) + ctx := apimw.SetClaims(req.Context(), &auth.Claims{UserID: 7}) + ctx = apimw.SetProfileID(ctx, "profile-1") + req = req.WithContext(ctx) + + progress := handler.getLeafUserData(req, "ebook-progress", "ebook") + if progress == nil || progress.Played || !progress.IsInProgress || progress.PositionSeconds != 0.42 || progress.DurationSeconds != 1 { + t.Fatalf("partial ebook user data = %#v", progress) + } + + complete := handler.getLeafUserData(req, "ebook-complete", "ebook") + if complete == nil || !complete.Played || complete.IsInProgress { + t.Fatalf("completed ebook user data = %#v", complete) + } +} diff --git a/internal/api/handlers/libraries.go b/internal/api/handlers/libraries.go index b2b2889f..fae7be03 100644 --- a/internal/api/handlers/libraries.go +++ b/internal/api/handlers/libraries.go @@ -574,6 +574,11 @@ func (h *LibraryHandler) HandleCreateLibrary(w http.ResponseWriter, r *http.Requ if seedErr := h.SectionRepo.SeedDefaults(r.Context(), "library", &folder.ID, sections.DefaultLibrarySectionsForType(&folder.ID, folder.Type)); seedErr != nil { slog.Warn("seed default sections for new library", "library_id", folder.ID, "error", seedErr) } + if sections.IsAudiobookLibraryType(folder.Type) { + if _, seedErr := h.SectionRepo.EnsureHomeContinueListeningSection(r.Context()); seedErr != nil { + slog.Warn("ensure home continue listening section", "library_id", folder.ID, "error", seedErr) + } + } if _, seedErr := h.SectionRepo.CreateGeneratedHomeLibraryRecentSections(r.Context(), folder.ID, folder.Name, folder.Type); seedErr != nil { slog.Warn("seed generated home sections for new library", "library_id", folder.ID, "error", seedErr) } @@ -2029,18 +2034,8 @@ func (h *LibraryHandler) seedDefaultChain(ctx context.Context, libraryType strin return nil } - // Determine which content levels apply. - var levels []string - switch libraryType { - case "series": - levels = []string{"series", "season", "episode"} - case "movies", "movie": - levels = []string{"movie"} - case "audiobooks", "audiobook": - levels = []string{"audiobook"} - case "mixed": - levels = []string{"movie", "series", "season", "episode", "audiobook"} - default: + levels := metadataContentLevelsForLibraryType(libraryType) + if len(levels) == 0 { return nil } @@ -2055,7 +2050,7 @@ func (h *LibraryHandler) seedDefaultChain(ctx context.Context, libraryType strin var candidates []candidate for _, c := range caps { - defaultPriority := metadata.LookupDefaultPriority(ctx, h.ChainRepo.Pool(), c.PluginInstallationID, level) + defaultPriority := metadata.LookupDefaultPriority(ctx, h.ChainRepo.Pool(), c.PluginInstallationID, c.CapabilityID, level) if defaultPriority > 0 { candidates = append(candidates, candidate{ installationID: c.PluginInstallationID, @@ -2093,6 +2088,23 @@ func (h *LibraryHandler) seedDefaultChain(ctx context.Context, libraryType strin return entries } +func metadataContentLevelsForLibraryType(libraryType string) []string { + switch libraryType { + case "series": + return []string{"series", "season", "episode"} + case "movies", "movie": + return []string{"movie"} + case "audiobooks", "audiobook": + return []string{"audiobook"} + case "ebooks", "ebook": + return []string{"ebook"} + case "mixed": + return []string{"movie", "series", "season", "episode", "audiobook", "ebook"} + default: + return nil + } +} + // HandleListStaleIDs handles GET /libraries/stale-ids. func (h *LibraryHandler) HandleListStaleIDs(w http.ResponseWriter, r *http.Request) { if h.StaleIDRepo == nil { diff --git a/internal/api/handlers/libraries_metadata_levels_test.go b/internal/api/handlers/libraries_metadata_levels_test.go new file mode 100644 index 00000000..b6a542b2 --- /dev/null +++ b/internal/api/handlers/libraries_metadata_levels_test.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "reflect" + "testing" +) + +func TestMetadataContentLevelsForLibraryTypeIncludesEbooks(t *testing.T) { + cases := []struct { + name string + libraryType string + want []string + }{ + {name: "plural ebooks", libraryType: "ebooks", want: []string{"ebook"}}, + {name: "singular ebook", libraryType: "ebook", want: []string{"ebook"}}, + {name: "mixed includes ebook", libraryType: "mixed", want: []string{"movie", "series", "season", "episode", "audiobook", "ebook"}}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := metadataContentLevelsForLibraryType(tc.libraryType); !reflect.DeepEqual(got, tc.want) { + t.Fatalf("metadataContentLevelsForLibraryType(%q) = %#v, want %#v", tc.libraryType, got, tc.want) + } + }) + } +} diff --git a/internal/api/handlers/playback.go b/internal/api/handlers/playback.go index cc224b6d..7149bb3a 100644 --- a/internal/api/handlers/playback.go +++ b/internal/api/handlers/playback.go @@ -49,6 +49,8 @@ type SessionManagerInterface interface { SetEffectiveMediaFileID(sessionID string, fileID int) error SetTranscodeNodeURL(sessionID, url string) error SetWebSocket(sessionID string, connected bool) error + SetRealtimeConnection(sessionID string, connected bool) error + SetProgressPersistenceDisabled(sessionID string, disabled bool) error StopSession(sessionID string) error GetSession(sessionID string) (*playback.Session, error) } @@ -257,6 +259,7 @@ type startPlaybackRequest struct { StartPosition *float64 `json:"start_position,omitempty"` AudioTrackIndex *int `json:"audio_track_index,omitempty"` PreserveDirectAudioSelection bool `json:"preserve_direct_audio_selection,omitempty"` + DisableProgressPersistence bool `json:"disable_progress_persistence,omitempty"` CodecsVideo []string `json:"codecs_video"` CodecsAudio []string `json:"codecs_audio"` Containers []string `json:"containers"` @@ -761,6 +764,9 @@ func (h *PlaybackHandler) persistProgress(ctx context.Context, session *playback if h.StoreProvider == nil || h.fileResolver == nil { return } + if session == nil || session.DisableProgressPersistence { + return + } file, err := h.loadFileByPreferredID(ctx, requestedMediaFileID(session), session.MediaFileID) targetID := playbackProgressTarget(file) @@ -799,7 +805,7 @@ func (h *PlaybackHandler) persistStopAndHistory(ctx context.Context, session *pl if h.StoreProvider == nil || h.fileResolver == nil { return watchstate.PlaybackStopResult{} } - if session == nil || session.Position <= 0 { + if session == nil || session.DisableProgressPersistence || session.Position <= 0 { return watchstate.PlaybackStopResult{} } @@ -1281,6 +1287,13 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req return } setPlaybackSessionLogContext(r, session.ID) + if req.DisableProgressPersistence { + if err := h.sessionMgr.SetProgressPersistenceDisabled(session.ID, true); err != nil { + slog.Error("failed to disable progress persistence", "session", session.ID, "error", err) + } else { + session.DisableProgressPersistence = true + } + } if err := h.sessionMgr.UpdateAudioTrack(session.ID, audioTrackIndex, session.PlayMethod); err != nil { slog.Error("failed to set audio track", "session", session.ID, "error", err) @@ -1320,7 +1333,7 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req } else { h.restoreSessionProgress(r.Context(), session, file) } - if h.WatchScrobbler != nil && effectiveFile != nil { + if !session.DisableProgressPersistence && h.WatchScrobbler != nil && effectiveFile != nil { targetID := playbackProgressTarget(effectiveFile) if targetID != "" { event := h.scrobbleEventForSession(r.Context(), session, targetID, float64(effectiveFile.Duration), session.Position) @@ -1549,7 +1562,7 @@ func (h *PlaybackHandler) HandleUpdateProgress(w http.ResponseWriter, r *http.Re // Persist progress to UserStore (best-effort). if sess, getErr := h.sessionMgr.GetSession(sessionID); getErr == nil { h.persistProgress(r.Context(), sess) - if h.WatchScrobbler != nil && wasPaused != sess.IsPaused { + if !sess.DisableProgressPersistence && h.WatchScrobbler != nil && wasPaused != sess.IsPaused { if file, loadErr := h.loadFileByPreferredID(r.Context(), requestedMediaFileID(sess), sess.MediaFileID); loadErr == nil && file != nil { targetID := playbackProgressTarget(file) if targetID != "" { diff --git a/internal/api/handlers/playback_test.go b/internal/api/handlers/playback_test.go index 30da0cfa..f711869d 100644 --- a/internal/api/handlers/playback_test.go +++ b/internal/api/handlers/playback_test.go @@ -129,6 +129,10 @@ func (failingSessionManager) SetTranscodeNodeURL(string, string) error { return func (failingSessionManager) SetWebSocket(string, bool) error { return nil } +func (failingSessionManager) SetRealtimeConnection(string, bool) error { return nil } + +func (failingSessionManager) SetProgressPersistenceDisabled(string, bool) error { return nil } + func (failingSessionManager) StopSession(string) error { return nil } func (failingSessionManager) GetSession(string) (*playback.Session, error) { return nil, nil } @@ -283,6 +287,64 @@ func TestHandleStartPlayback_UsesExplicitZeroStartPositionInsteadOfStoredResume( } } +func TestPlaybackSessionProgressPersistenceCanBeDisabled(t *testing.T) { + store := newPlaybackTestStore(t) + if err := store.SetProgress(context.Background(), "profile-1", "book-1", 500, 1200, userstore.ProgressThresholds{}); err != nil { + t.Fatalf("seed progress: %v", err) + } + + file := &models.MediaFile{ + ID: 42, + ContentID: "book-1", + FilePath: writePlaybackTestMediaFile(t, "book.m4b"), + Duration: 600, + } + sessionMgr := playback.NewSessionManager(0, 0) + handler := NewPlaybackHandler(sessionMgr, testPlaybackFileResolver{file: file}) + handler.StoreProvider = testUserStoreProvider{store: store} + handler.ItemAccess = allowAllPlaybackItemAccess{} + + req := httptest.NewRequest("POST", "/api/v1/playback/start", strings.NewReader(`{"file_id":42,"profile_id":"profile-1","play_method":"direct","start_position":120,"disable_progress_persistence":true}`)) + req = req.WithContext(newAuthorizedPlaybackContext()) + + rr := httptest.NewRecorder() + handler.HandleStartPlayback(rr, req) + if rr.Code != 201 { + t.Fatalf("start status = %d, body = %s", rr.Code, rr.Body.String()) + } + + var resp playbackSessionResponse + if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil { + t.Fatalf("decode response: %v", err) + } + + progressReq := httptest.NewRequest("POST", "/api/v1/playback/"+resp.SessionID+"/progress", strings.NewReader(`{"position":240,"is_paused":false}`)) + progressReq = progressReq.WithContext(newAuthorizedPlaybackContext()) + progressReq = withPlaybackRouteParam(progressReq, "session_id", resp.SessionID) + progressRR := httptest.NewRecorder() + handler.HandleUpdateProgress(progressRR, progressReq) + if progressRR.Code != 204 { + t.Fatalf("progress status = %d, body = %s", progressRR.Code, progressRR.Body.String()) + } + + stopReq := httptest.NewRequest("DELETE", "/api/v1/playback/"+resp.SessionID, nil) + stopReq = stopReq.WithContext(newAuthorizedPlaybackContext()) + stopReq = withPlaybackRouteParam(stopReq, "session_id", resp.SessionID) + stopRR := httptest.NewRecorder() + handler.HandleStopPlayback(stopRR, stopReq) + if stopRR.Code != 204 { + t.Fatalf("stop status = %d, body = %s", stopRR.Code, stopRR.Body.String()) + } + + progress, err := store.GetProgress(context.Background(), "profile-1", "book-1") + if err != nil { + t.Fatalf("get progress: %v", err) + } + if progress == nil || progress.PositionSeconds != 500 || progress.DurationSeconds != 1200 { + t.Fatalf("progress after disabled session = %+v, want 500/1200", progress) + } +} + func TestBuildTranscodeStartResponse_UnifiedSeekAnywhere(t *testing.T) { file := &models.MediaFile{Duration: 3600} diff --git a/internal/api/handlers/plugins.go b/internal/api/handlers/plugins.go index e4eef106..bd025336 100644 --- a/internal/api/handlers/plugins.go +++ b/internal/api/handlers/plugins.go @@ -169,22 +169,27 @@ type pluginConfigSchemaJSON struct { } type pluginAdminFormJSON struct { - Fields []pluginAdminFormFieldJSON `json:"fields"` - SubmitLabel string `json:"submit_label,omitempty"` + Fields []pluginAdminFormFieldJSON `json:"fields"` + SubmitLabel string `json:"submit_label,omitempty"` + Sections []pluginAdminFormSectionJSON `json:"sections,omitempty"` } type pluginAdminFormFieldJSON struct { - Key string `json:"key"` - Label string `json:"label"` - Description string `json:"description,omitempty"` - Control string `json:"control"` - Placeholder string `json:"placeholder,omitempty"` - Required bool `json:"required"` - Secret bool `json:"secret"` - Multiline bool `json:"multiline"` - DefaultValue any `json:"default_value,omitempty"` - Options []pluginAdminFormOptionJSON `json:"options,omitempty"` - Rows int32 `json:"rows,omitempty"` + Key string `json:"key"` + Label string `json:"label"` + Description string `json:"description,omitempty"` + Control string `json:"control"` + Placeholder string `json:"placeholder,omitempty"` + Required bool `json:"required"` + Secret bool `json:"secret"` + Multiline bool `json:"multiline"` + DefaultValue any `json:"default_value,omitempty"` + Options []pluginAdminFormOptionJSON `json:"options,omitempty"` + Rows int32 `json:"rows,omitempty"` + DynamicOptions bool `json:"dynamic_options,omitempty"` + ShowWhen []pluginAdminFormConditionJSON `json:"show_when,omitempty"` + Validation *pluginAdminFormValidationJSON `json:"validation,omitempty"` + ExclusiveGroupField string `json:"exclusive_group_field,omitempty"` } type pluginAdminFormOptionJSON struct { @@ -193,6 +198,31 @@ type pluginAdminFormOptionJSON struct { Description string `json:"description,omitempty"` } +type pluginAdminFormConditionJSON struct { + Field string `json:"field"` + Equals []string `json:"equals"` +} + +type pluginAdminFormValidationJSON struct { + HasMin bool `json:"has_min,omitempty"` + Min float64 `json:"min,omitempty"` + HasMax bool `json:"has_max,omitempty"` + Max float64 `json:"max,omitempty"` + Pattern string `json:"pattern,omitempty"` + MinLength int32 `json:"min_length,omitempty"` + MaxLength int32 `json:"max_length,omitempty"` +} + +type pluginAdminFormSectionJSON struct { + Key string `json:"key"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + Collapsible bool `json:"collapsible"` + CollapsedDefault bool `json:"collapsed_default"` + FieldKeys []string `json:"field_keys"` + ShowWhen []pluginAdminFormConditionJSON `json:"show_when,omitempty"` +} + type pluginCapabilityJSON struct { Type string `json:"type"` ID string `json:"id"` @@ -660,7 +690,7 @@ func (h *PluginHandler) syncMetadataProviders(ctx context.Context, installation continue } if err := h.chainRepo.AppendProviderToAllChains(ctx, installation.ID, cap.ID, func(level string) int { - return metadata.LookupDefaultPriority(ctx, h.chainRepo.Pool(), installation.ID, level) + return metadata.LookupDefaultPriority(ctx, h.chainRepo.Pool(), installation.ID, cap.ID, level) }); err != nil { slog.Warn("failed to append provider to library chains", "installation_id", installation.ID, @@ -1342,26 +1372,78 @@ func adminFormToJSON(form *pluginv1.AdminFormDescriptor) *pluginAdminFormJSON { if field.GetDefaultValue() != nil { defaultValue = field.GetDefaultValue().AsInterface() } + var validation *pluginAdminFormValidationJSON + if v := field.GetValidation(); v != nil { + validation = &pluginAdminFormValidationJSON{ + HasMin: v.GetHasMin(), + Min: v.GetMin(), + HasMax: v.GetHasMax(), + Max: v.GetMax(), + Pattern: v.GetPattern(), + MinLength: v.GetMinLength(), + MaxLength: v.GetMaxLength(), + } + } fields = append(fields, pluginAdminFormFieldJSON{ - Key: field.GetKey(), - Label: field.GetLabel(), - Description: field.GetDescription(), - Control: strings.TrimPrefix(field.GetControl().String(), "ADMIN_FORM_CONTROL_"), - Placeholder: field.GetPlaceholder(), - Required: field.GetRequired(), - Secret: field.GetSecret(), - Multiline: field.GetMultiline(), - DefaultValue: defaultValue, - Options: options, - Rows: field.GetRows(), + Key: field.GetKey(), + Label: field.GetLabel(), + Description: field.GetDescription(), + Control: strings.TrimPrefix(field.GetControl().String(), "ADMIN_FORM_CONTROL_"), + Placeholder: field.GetPlaceholder(), + Required: field.GetRequired(), + Secret: field.GetSecret(), + Multiline: field.GetMultiline(), + DefaultValue: defaultValue, + Options: options, + Rows: field.GetRows(), + DynamicOptions: field.GetDynamicOptions(), + ShowWhen: adminFormConditionsToJSON(field.GetShowWhen()), + Validation: validation, + ExclusiveGroupField: field.GetExclusiveGroupField(), + }) + } + sections := make([]pluginAdminFormSectionJSON, 0, len(form.GetSections())) + for _, section := range form.GetSections() { + if section == nil { + continue + } + sections = append(sections, pluginAdminFormSectionJSON{ + Key: section.GetKey(), + Title: section.GetTitle(), + Description: section.GetDescription(), + Collapsible: section.GetCollapsible(), + CollapsedDefault: section.GetCollapsedDefault(), + FieldKeys: append([]string(nil), section.GetFieldKeys()...), + ShowWhen: adminFormConditionsToJSON(section.GetShowWhen()), }) } return &pluginAdminFormJSON{ Fields: fields, SubmitLabel: form.GetSubmitLabel(), + Sections: sections, } } +func adminFormConditionsToJSON(conditions []*pluginv1.AdminFormCondition) []pluginAdminFormConditionJSON { + if len(conditions) == 0 { + return nil + } + out := make([]pluginAdminFormConditionJSON, 0, len(conditions)) + for _, condition := range conditions { + if condition == nil { + continue + } + out = append(out, pluginAdminFormConditionJSON{ + Field: condition.GetField(), + Equals: append([]string(nil), condition.GetEquals()...), + }) + } + if len(out) == 0 { + return nil + } + return out +} + func capabilitiesToJSON(descriptors []*pluginv1.CapabilityDescriptor) []pluginCapabilityJSON { response := make([]pluginCapabilityJSON, 0, len(descriptors)) for _, descriptor := range descriptors { diff --git a/internal/api/handlers/plugins_admin_form_test.go b/internal/api/handlers/plugins_admin_form_test.go new file mode 100644 index 00000000..6e4bec2b --- /dev/null +++ b/internal/api/handlers/plugins_admin_form_test.go @@ -0,0 +1,216 @@ +package handlers + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" +) + +// TestAdminFormToJSONSchemaFields locks the bridge between the SDK proto +// AdminForm descriptor and the JSON the frontend SchemaForm consumes. Without +// this, the schema-driven request-connection form silently loses its +// dynamic dropdowns (dynamic_options), conditional visibility (show_when), +// validation, sections, and the MULTI_SELECT control mapping, which makes the +// new form non-functional (required fields can never be set, save stays +// disabled). +func TestAdminFormToJSONSchemaFields(t *testing.T) { + descriptor := &pluginv1.AdminFormDescriptor{ + SubmitLabel: "Connect", + Fields: []*pluginv1.AdminFormField{ + { + Key: "quality_profile_ids", + Label: "Quality Profiles", + Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_MULTI_SELECT, + DynamicOptions: true, + ExclusiveGroupField: "service_kind", + ShowWhen: []*pluginv1.AdminFormCondition{ + {Field: "service_type", Equals: []string{"radarr", "sonarr"}}, + }, + Validation: &pluginv1.AdminFormValidation{ + HasMin: true, + Min: 1, + HasMax: true, + Max: 10, + Pattern: "^[0-9]+$", + MinLength: 1, + MaxLength: 64, + }, + }, + }, + Sections: []*pluginv1.AdminFormSection{ + { + Key: "advanced", + Title: "Advanced", + Description: "Optional tuning", + Collapsible: true, + CollapsedDefault: true, + FieldKeys: []string{"quality_profile_ids"}, + ShowWhen: []*pluginv1.AdminFormCondition{ + {Field: "service_type", Equals: []string{"radarr"}}, + }, + }, + }, + } + + form := adminFormToJSON(descriptor) + if form == nil { + t.Fatal("adminFormToJSON returned nil") + } + + raw, err := json.Marshal(form) + if err != nil { + t.Fatalf("marshal admin form: %v", err) + } + + var decoded struct { + Fields []struct { + Control string `json:"control"` + DynamicOptions bool `json:"dynamic_options"` + ShowWhen []struct { + Field string `json:"field"` + Equals []string `json:"equals"` + } `json:"show_when"` + Validation *struct { + HasMin bool `json:"has_min"` + Min float64 `json:"min"` + HasMax bool `json:"has_max"` + Max float64 `json:"max"` + Pattern string `json:"pattern"` + MinLength int32 `json:"min_length"` + MaxLength int32 `json:"max_length"` + } `json:"validation"` + ExclusiveGroupField string `json:"exclusive_group_field"` + } `json:"fields"` + Sections []struct { + Key string `json:"key"` + Title string `json:"title"` + Description string `json:"description"` + Collapsible bool `json:"collapsible"` + CollapsedDefault bool `json:"collapsed_default"` + FieldKeys []string `json:"field_keys"` + ShowWhen []struct { + Field string `json:"field"` + Equals []string `json:"equals"` + } `json:"show_when"` + } `json:"sections"` + } + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatalf("unmarshal admin form: %v\njson: %s", err, raw) + } + + if len(decoded.Fields) != 1 { + t.Fatalf("expected 1 field, got %d", len(decoded.Fields)) + } + field := decoded.Fields[0] + + // Control must serialize to the SHORT form the frontend switches on. + if field.Control != "MULTI_SELECT" { + t.Errorf("control = %q, want %q", field.Control, "MULTI_SELECT") + } + if !field.DynamicOptions { + t.Error("dynamic_options = false, want true") + } + if field.ExclusiveGroupField != "service_kind" { + t.Errorf("exclusive_group_field = %q, want %q", field.ExclusiveGroupField, "service_kind") + } + if len(field.ShowWhen) != 1 || field.ShowWhen[0].Field != "service_type" || + len(field.ShowWhen[0].Equals) != 2 { + t.Errorf("field show_when not serialized correctly: %+v", field.ShowWhen) + } + if field.Validation == nil { + t.Fatal("validation not serialized") + } + if !field.Validation.HasMin || field.Validation.Min != 1 || + !field.Validation.HasMax || field.Validation.Max != 10 || + field.Validation.Pattern != "^[0-9]+$" || + field.Validation.MinLength != 1 || field.Validation.MaxLength != 64 { + t.Errorf("validation not serialized correctly: %+v", field.Validation) + } + + if len(decoded.Sections) != 1 { + t.Fatalf("expected 1 section, got %d", len(decoded.Sections)) + } + section := decoded.Sections[0] + if section.Key != "advanced" || section.Title != "Advanced" || + section.Description != "Optional tuning" || !section.Collapsible || + !section.CollapsedDefault { + t.Errorf("section scalar fields not serialized correctly: %+v", section) + } + if len(section.FieldKeys) != 1 || section.FieldKeys[0] != "quality_profile_ids" { + t.Errorf("section field_keys not serialized correctly: %+v", section.FieldKeys) + } + if len(section.ShowWhen) != 1 || section.ShowWhen[0].Field != "service_type" || + len(section.ShowWhen[0].Equals) != 1 { + t.Errorf("section show_when not serialized correctly: %+v", section.ShowWhen) + } +} + +// adminFormDTOMappingRenames records intentional proto-field -> DTO-field name +// renames. The DTO field names currently mirror the proto field names 1:1, so +// this is empty; add an entry only when a DTO field is deliberately named +// differently from its proto source (the test then accepts the renamed target). +var adminFormDTOMappingRenames = map[string]string{} + +// protoInternalFields are the protobuf-runtime bookkeeping fields every +// generated message struct carries. They are never serialized to the client +// DTO, so the completeness guard skips them. +var protoInternalFields = map[string]bool{ + "state": true, + "sizeCache": true, + "unknownFields": true, +} + +// TestAdminFormDTOCompleteness is a guard against the C1-class bug: the +// hand-written adminFormToJSON allowlist (pluginAdminFormFieldJSON / +// pluginAdminFormSectionJSON / pluginAdminFormJSON) silently dropping a NEW +// proto field added to AdminFormField / AdminFormDescriptor / AdminFormSection +// later. It reflects over each proto message's exported fields and asserts the +// corresponding JSON DTO struct has a field mapping to it (by identical Go name, +// or via an explicit rename in adminFormDTOMappingRenames). When the next proto +// field lands, this FAILS naming the un-mapped field, forcing the serializer + +// DTO to be updated in lockstep. (We deliberately do not switch the serializer +// to protojson — the short-form control-string MULTI_SELECT mapping makes that +// risky; this guard is the proportionate fix.) +func TestAdminFormDTOCompleteness(t *testing.T) { + cases := []struct { + name string + proto reflect.Type + dto reflect.Type + }{ + {"AdminFormField", reflect.TypeOf(pluginv1.AdminFormField{}), reflect.TypeOf(pluginAdminFormFieldJSON{})}, + {"AdminFormDescriptor", reflect.TypeOf(pluginv1.AdminFormDescriptor{}), reflect.TypeOf(pluginAdminFormJSON{})}, + {"AdminFormSection", reflect.TypeOf(pluginv1.AdminFormSection{}), reflect.TypeOf(pluginAdminFormSectionJSON{})}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dtoFields := map[string]bool{} + for i := 0; i < tc.dto.NumField(); i++ { + dtoFields[tc.dto.Field(i).Name] = true + } + + for i := 0; i < tc.proto.NumField(); i++ { + pf := tc.proto.Field(i) + if !pf.IsExported() { + continue // unexported protobuf-internal state + } + if protoInternalFields[pf.Name] || strings.HasPrefix(pf.Name, "XXX_") { + continue + } + want := pf.Name + if mapped, ok := adminFormDTOMappingRenames[pf.Name]; ok { + want = mapped + } + if !dtoFields[want] { + t.Errorf("proto field %s.%s has no corresponding field %q in the JSON DTO %s; "+ + "adminFormToJSON would silently drop it. Add the field to the DTO + serializer "+ + "(or record an intentional rename in adminFormDTOMappingRenames).", + tc.name, pf.Name, want, tc.dto.Name()) + } + } + }) + } +} diff --git a/internal/api/handlers/rate_limits.go b/internal/api/handlers/rate_limits.go index de2d3fa8..797524f8 100644 --- a/internal/api/handlers/rate_limits.go +++ b/internal/api/handlers/rate_limits.go @@ -29,6 +29,13 @@ type rateLimitConfigResponse struct { IPReqPerMinute float64 `json:"ip_requests_per_minute"` IPBurst int `json:"ip_burst"` AuthEndpoints map[string]authEndpointConfigResponse `json:"auth_endpoints"` + // Active reports whether a limiter is running in this process. The + // limiter is constructed at startup, so config saved while it is absent + // (or a backend change while it is running) needs a restart to apply. + Active bool `json:"active"` + // ActiveBackend is the backend the running limiter actually uses, which + // can differ from Backend until the server restarts. + ActiveBackend string `json:"active_backend,omitempty"` } type tierConfigResponse struct { @@ -64,6 +71,10 @@ func (h *RateLimitHandler) HandleGetConfig(w http.ResponseWriter, r *http.Reques IPReqPerMinute: cfg.IPReqPerMinute, IPBurst: cfg.IPBurst, AuthEndpoints: make(map[string]authEndpointConfigResponse), + Active: h.mw != nil, + } + if h.mw != nil { + resp.ActiveBackend = h.mw.ActiveBackend() } for name, tier := range cfg.Tiers { resp.Tiers[name] = tierConfigResponse{ @@ -151,10 +162,21 @@ func (h *RateLimitHandler) HandleUpdateConfig(w http.ResponseWriter, r *http.Req } } - // Hot-reload: apply new config immediately on this instance - if err := h.mw.Reload(r.Context()); err != nil { - writeError(w, http.StatusInternalServerError, "internal_error", "Config saved but reload failed") - return + // The limiter is constructed at startup, so enabling while it is absent + // or switching backend while it runs only takes effect after a restart. + // Everything else hot-reloads below. + restartRequired := false + if h.mw == nil { + restartRequired = req.Enabled + } else { + if (req.Backend == "memory" || req.Backend == "redis") && req.Backend != h.mw.ActiveBackend() { + restartRequired = true + } + // Hot-reload: apply new config immediately on this instance + if err := h.mw.Reload(r.Context()); err != nil { + writeError(w, http.StatusInternalServerError, "internal_error", "Config saved but reload failed") + return + } } // Publish for multi-instance reload (if EventBus is available/backed by Redis) @@ -165,5 +187,5 @@ func (h *RateLimitHandler) HandleUpdateConfig(w http.ResponseWriter, r *http.Req } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) + json.NewEncoder(w).Encode(map[string]any{"status": "ok", "restart_required": restartRequired}) } diff --git a/internal/api/handlers/rate_limits_test.go b/internal/api/handlers/rate_limits_test.go new file mode 100644 index 00000000..5532750e --- /dev/null +++ b/internal/api/handlers/rate_limits_test.go @@ -0,0 +1,140 @@ +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Silo-Server/silo-server/internal/ratelimit" +) + +type fakeRateLimitStore struct { + values map[string]string +} + +func newFakeRateLimitStore() *fakeRateLimitStore { + return &fakeRateLimitStore{values: make(map[string]string)} +} + +func (s *fakeRateLimitStore) Get(_ context.Context, key string) (string, error) { + return s.values[key], nil +} + +func (s *fakeRateLimitStore) Set(_ context.Context, key, value string) error { + s.values[key] = value + return nil +} + +func (s *fakeRateLimitStore) GetAll(_ context.Context) (map[string]string, error) { + out := make(map[string]string, len(s.values)) + for k, v := range s.values { + out[k] = v + } + return out, nil +} + +func newRunningRateLimitMiddleware(t *testing.T, store ratelimit.SettingsStore) *ratelimit.Middleware { + t.Helper() + perKey := ratelimit.NewMemoryLimiter() + global := ratelimit.NewMemoryLimiter() + t.Cleanup(func() { + perKey.Close() + global.Close() + }) + mw := ratelimit.NewMiddleware(perKey, global, store, true) + if err := mw.Init(context.Background()); err != nil { + t.Fatalf("init middleware: %v", err) + } + return mw +} + +func putRateLimitConfig(t *testing.T, h *RateLimitHandler, body string) map[string]any { + t.Helper() + req := httptest.NewRequest(http.MethodPut, "/admin/rate-limits/config", strings.NewReader(body)) + rec := httptest.NewRecorder() + h.HandleUpdateConfig(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("PUT status = %d, body = %s", rec.Code, rec.Body.String()) + } + var resp map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode PUT response: %v", err) + } + return resp +} + +func getRateLimitConfig(t *testing.T, h *RateLimitHandler) rateLimitConfigResponse { + t.Helper() + req := httptest.NewRequest(http.MethodGet, "/admin/rate-limits/config", nil) + rec := httptest.NewRecorder() + h.HandleGetConfig(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("GET status = %d, body = %s", rec.Code, rec.Body.String()) + } + var resp rateLimitConfigResponse + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode GET response: %v", err) + } + return resp +} + +func TestRateLimitHandlerWithoutRunningLimiter(t *testing.T) { + // The limiter is nil when rate limiting was disabled at boot. The config + // endpoints must still work so an admin can re-enable it from the UI. + store := newFakeRateLimitStore() + store.values["ratelimit.enabled"] = "false" + h := NewRateLimitHandler(store, nil, nil) + + got := getRateLimitConfig(t, h) + if got.Enabled { + t.Error("GET enabled = true, want false") + } + if got.Active { + t.Error("GET active = true, want false when no limiter is running") + } + + resp := putRateLimitConfig(t, h, `{"enabled":true,"backend":"redis"}`) + if resp["restart_required"] != true { + t.Errorf("enabling without a running limiter: restart_required = %v, want true", resp["restart_required"]) + } + if store.values["ratelimit.enabled"] != "true" { + t.Errorf("ratelimit.enabled = %q, want \"true\"", store.values["ratelimit.enabled"]) + } + if store.values["ratelimit.backend"] != "redis" { + t.Errorf("ratelimit.backend = %q, want \"redis\"", store.values["ratelimit.backend"]) + } + + resp = putRateLimitConfig(t, h, `{"enabled":false,"backend":"redis"}`) + if resp["restart_required"] != false { + t.Errorf("saving disabled config without a running limiter: restart_required = %v, want false", resp["restart_required"]) + } +} + +func TestRateLimitHandlerWithRunningLimiter(t *testing.T) { + store := newFakeRateLimitStore() + mw := newRunningRateLimitMiddleware(t, store) + h := NewRateLimitHandler(store, mw, nil) + + got := getRateLimitConfig(t, h) + if !got.Active { + t.Error("GET active = false, want true with a running limiter") + } + if got.ActiveBackend != "memory" { + t.Errorf("GET active_backend = %q, want \"memory\"", got.ActiveBackend) + } + + // Toggling enabled hot-reloads; no restart needed when the backend matches. + resp := putRateLimitConfig(t, h, `{"enabled":false,"backend":"memory"}`) + if resp["restart_required"] != false { + t.Errorf("toggle on running limiter: restart_required = %v, want false", resp["restart_required"]) + } + + // Switching backend only takes effect at boot. + resp = putRateLimitConfig(t, h, `{"enabled":true,"backend":"redis"}`) + if resp["restart_required"] != true { + t.Errorf("backend change: restart_required = %v, want true", resp["restart_required"]) + } +} diff --git a/internal/api/handlers/recommendations.go b/internal/api/handlers/recommendations.go index efa2b322..13cf808a 100644 --- a/internal/api/handlers/recommendations.go +++ b/internal/api/handlers/recommendations.go @@ -48,6 +48,7 @@ type RecommendationsHandler struct { EpisodeRepo *catalog.EpisodeRepository WatchTonightFetcher watchTonightSectionFetcher CastFetcher cardsCastFetcher + EbookProgress EbookReaderProgressLister // RecWorker enqueues asynchronous taste-profile refreshes after writes // (taste seeding). Optional — when nil, refresh is simply skipped. RecWorker ProfileRefreshRequester @@ -643,7 +644,10 @@ func (h *RecommendationsHandler) loadItemEnrichment( if h.storeProvider != nil { store, storeErr := h.storeProvider.ForUser(ctx, userID) if storeErr == nil && store != nil { - states, stateErr := resolveItemUserStates(ctx, store, profileID, h.EpisodeRepo, mediaItems) + states, stateErr := resolveItemUserStatesWithOptions(ctx, store, profileID, h.EpisodeRepo, mediaItems, itemUserStateOptions{ + UserID: userID, + EbookProgressStore: h.EbookProgress, + }) if stateErr == nil { out.states = states } diff --git a/internal/api/handlers/requests.go b/internal/api/handlers/requests.go index 420b3152..1ad95ba1 100644 --- a/internal/api/handlers/requests.go +++ b/internal/api/handlers/requests.go @@ -38,7 +38,7 @@ type RequestService interface { CreateIntegration(ctx context.Context, viewer mediarequests.Viewer, integration mediarequests.Integration) (*mediarequests.Integration, error) UpdateIntegration(ctx context.Context, viewer mediarequests.Viewer, integration mediarequests.Integration) (*mediarequests.Integration, error) DeleteIntegration(ctx context.Context, viewer mediarequests.Viewer, id string) error - LoadIntegrationOptions(ctx context.Context, viewer mediarequests.Viewer, integration mediarequests.Integration) (*mediarequests.IntegrationOptions, error) + LoadIntegrationOptions(ctx context.Context, viewer mediarequests.Viewer, integration mediarequests.Integration) (map[string][]mediarequests.RouterOption, error) ListStudios(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.DiscoverBrandCard, error) ListNetworks(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.DiscoverBrandCard, error) @@ -588,62 +588,36 @@ func parsePositivePathInt(w http.ResponseWriter, r *http.Request, key string) (i } type requestIntegrationResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Kind string `json:"kind"` - Enabled bool `json:"enabled"` - Is4K bool `json:"is_4k"` - IsDefault bool `json:"is_default"` - IsDefault4K bool `json:"is_default_4k"` - AnimeEnabled bool `json:"anime_enabled"` - AnimeQualityProfileID *int `json:"anime_quality_profile_id,omitempty"` - AnimeRootFolder string `json:"anime_root_folder,omitempty"` - AnimeTags []int `json:"anime_tags"` - BaseURL string `json:"base_url"` - HasAPIKey bool `json:"has_api_key"` - RootFolder string `json:"root_folder"` - QualityProfileID *int `json:"quality_profile_id,omitempty"` - Tags []int `json:"tags"` - Options map[string]any `json:"options"` - LastCheckAt *time.Time `json:"last_check_at,omitempty"` - LastCheckStatus string `json:"last_check_status,omitempty"` - LastCheckError string `json:"last_check_error,omitempty"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Name string `json:"name"` + CapabilityID string `json:"capability_id"` + InstallationID *int `json:"installation_id,omitempty"` + SupportedMediaTypes []string `json:"supported_media_types"` + PluginConfig map[string]any `json:"plugin_config"` + Enabled bool `json:"enabled"` + BaseURL string `json:"base_url"` + HasAPIKey bool `json:"has_api_key"` + LastCheckAt *time.Time `json:"last_check_at,omitempty"` + LastCheckStatus string `json:"last_check_status,omitempty"` + LastCheckError string `json:"last_check_error,omitempty"` + UpdatedAt time.Time `json:"updated_at"` } func requestIntegrationResponseFrom(integration mediarequests.Integration) requestIntegrationResponse { - // Normalize nil slices so they serialize as [] (not null); the frontend - // types them as number[] and indexes/maps over them. - tags := integration.Tags - if tags == nil { - tags = []int{} - } - animeTags := integration.AnimeTags - if animeTags == nil { - animeTags = []int{} - } return requestIntegrationResponse{ - ID: integration.ID, - Name: integration.Name, - Kind: integration.Kind, - Enabled: integration.Enabled, - Is4K: integration.Is4K, - IsDefault: integration.IsDefault, - IsDefault4K: integration.IsDefault4K, - AnimeEnabled: integration.AnimeEnabled, - AnimeQualityProfileID: integration.AnimeQualityProfileID, - AnimeRootFolder: integration.AnimeRootFolder, - AnimeTags: animeTags, - BaseURL: integration.BaseURL, - HasAPIKey: strings.TrimSpace(integration.APIKeyRef) != "", - RootFolder: integration.RootFolder, - QualityProfileID: integration.QualityProfileID, - Tags: tags, - Options: integration.Options, - LastCheckAt: integration.LastCheckAt, - LastCheckStatus: integration.LastCheckStatus, - LastCheckError: integration.LastCheckError, - UpdatedAt: integration.UpdatedAt, + ID: integration.ID, + Name: integration.Name, + CapabilityID: integration.CapabilityID, + InstallationID: integration.InstallationID, + SupportedMediaTypes: integration.SupportedMediaTypes, + PluginConfig: integration.PluginConfig, + Enabled: integration.Enabled, + BaseURL: integration.BaseURL, + HasAPIKey: strings.TrimSpace(integration.APIKeyRef) != "", + LastCheckAt: integration.LastCheckAt, + LastCheckStatus: integration.LastCheckStatus, + LastCheckError: integration.LastCheckError, + UpdatedAt: integration.UpdatedAt, } } @@ -656,6 +630,18 @@ func toIntegrationResponses(integrations []mediarequests.Integration) []requestI } func writeRequestServiceError(w http.ResponseWriter, err error) { + // Plugin/instance validation failures carry inline field/form errors; surface + // them as a structured 400 so any handler routing through here renders them + // inline. Checked first because *ValidationError does not wrap a sentinel. + var verr *mediarequests.ValidationError + if errors.As(err, &verr) { + writeJSON(w, http.StatusBadRequest, map[string]any{ + "error": "validation_failed", + "field_errors": verr.FieldErrors, + "form_error": verr.FormError, + }) + return + } var quota mediarequests.QuotaError switch { case errors.As(err, "a): diff --git a/internal/api/handlers/requests_test.go b/internal/api/handlers/requests_test.go index 95ea16b7..855fc835 100644 --- a/internal/api/handlers/requests_test.go +++ b/internal/api/handlers/requests_test.go @@ -139,7 +139,7 @@ func (f *fakeRequestService) DeleteIntegration(context.Context, mediarequests.Vi return nil } -func (f *fakeRequestService) LoadIntegrationOptions(context.Context, mediarequests.Viewer, mediarequests.Integration) (*mediarequests.IntegrationOptions, error) { +func (f *fakeRequestService) LoadIntegrationOptions(context.Context, mediarequests.Viewer, mediarequests.Integration) (map[string][]mediarequests.RouterOption, error) { return nil, nil } diff --git a/internal/api/handlers/sections.go b/internal/api/handlers/sections.go index 58e27e55..ea680a07 100644 --- a/internal/api/handlers/sections.go +++ b/internal/api/handlers/sections.go @@ -34,6 +34,7 @@ type SectionHandler struct { DetailSvc *catalog.DetailService Settings catalog.SettingsStore CollectionRepo *catalog.LibraryCollectionRepository + EbookProgress EbookReaderProgressLister } // NewSectionHandler creates a new SectionHandler. @@ -157,8 +158,11 @@ func validateSectionConfig(sectionType sections.SectionType, config json.RawMess } cfg := sections.ParseConfigFilters(config) - if cfg.FilterType != "" && cfg.FilterType != "movie" && cfg.FilterType != "series" { - return "filter_type must be 'movie' or 'series'", false + if cfg.FilterType != "" && cfg.FilterType != "movie" && cfg.FilterType != "series" && cfg.FilterType != "audiobook" { + return "filter_type must be 'movie', 'series', or 'audiobook'", false + } + if _, err := sections.ParseContinueType(config); err != nil { + return err.Error(), false } var rawLibraryConfig struct { @@ -1377,7 +1381,10 @@ func (h *SectionHandler) listSectionItemUserStates(r *http.Request, items []*mod if err != nil || store == nil { return map[string]*itemUserStateResponse{} } - states, err := resolveItemUserStates(r.Context(), store, profileID, h.EpisodeRepo, items) + states, err := resolveItemUserStatesWithOptions(r.Context(), store, profileID, h.EpisodeRepo, items, itemUserStateOptions{ + UserID: userID, + EbookProgressStore: h.EbookProgress, + }) if err != nil { return map[string]*itemUserStateResponse{} } @@ -1408,8 +1415,8 @@ func (h *SectionHandler) maybeInjectNextUp(ctx context.Context, resolved []secti return resolved } -// injectNextUpSection inserts a synthetic SectionNextUp entry immediately after -// SectionContinueWatching in the resolved sections list. +// injectNextUpSection inserts a synthetic SectionNextUp entry after the +// contiguous continue rows that start with the video Continue Watching row. func injectNextUpSection(resolved []sections.ResolvedSection) []sections.ResolvedSection { nextUp := sections.ResolvedSection{ ID: "system-next-up", @@ -1418,6 +1425,20 @@ func injectNextUpSection(resolved []sections.ResolvedSection) []sections.Resolve ItemLimit: 20, } + for i, s := range resolved { + if s.SectionType == sections.SectionContinueWatching && sections.ContinueTypeFromConfig(s.Config) == sections.ContinueTypeWatching { + insertAt := i + 1 + for insertAt < len(resolved) && resolved[insertAt].SectionType == sections.SectionContinueWatching { + insertAt++ + } + result := make([]sections.ResolvedSection, 0, len(resolved)+1) + result = append(result, resolved[:insertAt]...) + result = append(result, nextUp) + result = append(result, resolved[insertAt:]...) + return result + } + } + for i, s := range resolved { if s.SectionType == sections.SectionContinueWatching { result := make([]sections.ResolvedSection, 0, len(resolved)+1) @@ -1428,7 +1449,7 @@ func injectNextUpSection(resolved []sections.ResolvedSection) []sections.Resolve } } - // If no continue_watching found, prepend + // If no resume row is found, prepend. return append([]sections.ResolvedSection{nextUp}, resolved...) } diff --git a/internal/api/handlers/sections_test.go b/internal/api/handlers/sections_test.go index 982e565c..a9dbd772 100644 --- a/internal/api/handlers/sections_test.go +++ b/internal/api/handlers/sections_test.go @@ -282,6 +282,66 @@ func TestBuildSectionsResponseBatchResolvesImageURLs(t *testing.T) { } } +func TestValidateSectionConfigAcceptsContinueTypes(t *testing.T) { + tests := []string{ + `{"continue_type":"watching"}`, + `{"continue_type":"listening"}`, + `{"continue_type":"reading"}`, + `{"filter_type":"audiobook"}`, + } + + for _, config := range tests { + t.Run(config, func(t *testing.T) { + if msg, ok := validateSectionConfig(sections.SectionContinueWatching, []byte(config)); !ok { + t.Fatalf("validateSectionConfig(%s) rejected config: %s", config, msg) + } + }) + } +} + +func TestValidateSectionConfigRejectsUnknownContinueType(t *testing.T) { + msg, ok := validateSectionConfig(sections.SectionContinueWatching, []byte(`{"continue_type":"scrolling"}`)) + if ok { + t.Fatal("validateSectionConfig accepted unknown continue_type") + } + if msg != "continue_type must be 'watching', 'listening', or 'reading'" { + t.Fatalf("message = %q", msg) + } +} + +func TestInjectNextUpAfterContiguousContinueRows(t *testing.T) { + in := []sections.ResolvedSection{ + { + ID: "cw", + SectionType: sections.SectionContinueWatching, + Title: "Continue Watching", + Config: sections.ContinueTypeConfig(sections.ContinueTypeWatching), + }, + { + ID: "cl", + SectionType: sections.SectionContinueWatching, + Title: "Continue Listening", + Config: sections.ContinueTypeConfig(sections.ContinueTypeListening), + }, + {ID: "recent", SectionType: sections.SectionRecentlyAdded, Title: "Recently Added"}, + } + + got := injectNextUpSection(in) + gotIDs := make([]string, 0, len(got)) + for _, section := range got { + gotIDs = append(gotIDs, section.ID) + } + wantIDs := []string{"cw", "cl", "system-next-up", "recent"} + if len(gotIDs) != len(wantIDs) { + t.Fatalf("section ids = %v, want %v", gotIDs, wantIDs) + } + for i := range wantIDs { + if gotIDs[i] != wantIDs[i] { + t.Fatalf("section ids = %v, want %v", gotIDs, wantIDs) + } + } +} + func TestDropEmptySeasonalSectionsRemovesOnlyEmptySeasonal(t *testing.T) { in := []sections.SectionWithItems{ // empty seasonal — drop diff --git a/internal/api/handlers/settings.go b/internal/api/handlers/settings.go index be2d1883..56d722a6 100644 --- a/internal/api/handlers/settings.go +++ b/internal/api/handlers/settings.go @@ -20,6 +20,7 @@ const subtitleAppearanceSettingKey = "subtitle_appearance" const ( libraryPageStateSettingKey = "ui.library_page_state" rememberLibraryPageStateSettingKey = "ui.remember_library_page_state" + searchMediaScopeSettingKey = "search.media_scope" ) const ( @@ -171,6 +172,14 @@ var settingsRegistry = map[string]settingSpec{ DefaultValue: "true", Validate: validateBoolSetting(rememberLibraryPageStateSettingKey), }, + // Preferred default scope for global/catalog search. "video" keeps + // results to movies and series; "all" mixes audiobooks in. + searchMediaScopeSettingKey: { + Scope: scopeUser, + DefaultValue: "video", + Validate: validateEnumSetting(searchMediaScopeSettingKey, + "all", "video", "audiobook"), + }, "player.hdr_enabled": { Scope: scopeDevice, DefaultValue: "true", diff --git a/internal/api/handlers/taste_seed.go b/internal/api/handlers/taste_seed.go index ef70b37b..21f863f9 100644 --- a/internal/api/handlers/taste_seed.go +++ b/internal/api/handlers/taste_seed.go @@ -181,7 +181,10 @@ func (h *RecommendationsHandler) resolveTasteSeedUserStates(ctx context.Context, if err != nil || store == nil { return nil } - states, err := resolveItemUserStates(ctx, store, profileID, h.EpisodeRepo, mediaItems) + states, err := resolveItemUserStatesWithOptions(ctx, store, profileID, h.EpisodeRepo, mediaItems, itemUserStateOptions{ + UserID: userID, + EbookProgressStore: h.EbookProgress, + }) if err != nil { return nil } diff --git a/internal/api/handlers/user_state.go b/internal/api/handlers/user_state.go index 6857a73f..c947b614 100644 --- a/internal/api/handlers/user_state.go +++ b/internal/api/handlers/user_state.go @@ -14,12 +14,28 @@ type itemUserStateResponse struct { InWatchlist bool `json:"in_watchlist"` } +type itemUserStateOptions struct { + UserID int + EbookProgressStore EbookReaderProgressLister +} + func resolveItemUserStates( ctx context.Context, store userstore.UserStore, profileID string, episodeRepo *catalog.EpisodeRepository, items []*models.MediaItem, +) (map[string]*itemUserStateResponse, error) { + return resolveItemUserStatesWithOptions(ctx, store, profileID, episodeRepo, items, itemUserStateOptions{}) +} + +func resolveItemUserStatesWithOptions( + ctx context.Context, + store userstore.UserStore, + profileID string, + episodeRepo *catalog.EpisodeRepository, + items []*models.MediaItem, + options itemUserStateOptions, ) (map[string]*itemUserStateResponse, error) { result := make(map[string]*itemUserStateResponse, len(items)) if store == nil || profileID == "" || len(items) == 0 { @@ -111,6 +127,10 @@ func resolveItemUserStates( if err != nil { return nil, err } + ebookProgressMap, err := resolveEbookProgressForUserStates(ctx, options, profileID, contentIDs) + if err != nil { + return nil, err + } for _, item := range items { if item == nil || item.ContentID == "" { @@ -125,6 +145,8 @@ func resolveItemUserStates( state.Played = allEpisodesCompleted(seriesEpisodes[item.ContentID], progressMap) case "season": state.Played = allEpisodesCompleted(seasonEpisodes[item.ContentID], progressMap) + case "ebook": + state.Played = ebookProgressMap[item.ContentID].Progress >= models.EbookFinishedProgressThreshold default: state.Played = progressMap[item.ContentID].Completed } @@ -134,6 +156,18 @@ func resolveItemUserStates( return result, nil } +func resolveEbookProgressForUserStates( + ctx context.Context, + options itemUserStateOptions, + profileID string, + contentIDs []string, +) (map[string]EbookReaderProgress, error) { + if options.EbookProgressStore == nil || options.UserID <= 0 || profileID == "" || len(contentIDs) == 0 { + return nil, nil + } + return options.EbookProgressStore.ListByContentIDs(ctx, options.UserID, profileID, contentIDs) +} + func allEpisodesCompleted(episodes []*models.Episode, progressMap map[string]userstore.WatchProgress) bool { if len(episodes) == 0 { return false diff --git a/internal/api/handlers/user_state_test.go b/internal/api/handlers/user_state_test.go new file mode 100644 index 00000000..9ff974b3 --- /dev/null +++ b/internal/api/handlers/user_state_test.go @@ -0,0 +1,96 @@ +package handlers + +import ( + "context" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/models" +) + +func TestResolveItemUserStatesIncludesCompletedEbookReaderProgress(t *testing.T) { + ctx := context.Background() + store := newProfileTestStore(t) + items := []*models.MediaItem{ + {ContentID: "ebook-complete", Type: "ebook", Title: "Complete Ebook"}, + {ContentID: "ebook-progress", Type: "ebook", Title: "Partial Ebook"}, + } + + states, err := resolveItemUserStatesWithOptions(ctx, store, "profile-1", nil, items, itemUserStateOptions{ + UserID: 42, + EbookProgressStore: &fakeEbookReaderProgressLister{ + progress: map[string]EbookReaderProgress{ + "ebook-complete": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-complete", + Progress: 0.95, + UpdatedAt: time.Date(2026, 6, 4, 10, 0, 0, 0, time.UTC), + }, + "ebook-progress": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-progress", + Progress: 0.45, + UpdatedAt: time.Date(2026, 6, 4, 9, 0, 0, 0, time.UTC), + }, + }, + }, + }) + if err != nil { + t.Fatalf("resolveItemUserStatesWithOptions: %v", err) + } + + if states["ebook-complete"] == nil || !states["ebook-complete"].Played { + t.Fatalf("completed ebook state = %+v, want played", states["ebook-complete"]) + } + if states["ebook-progress"] == nil || states["ebook-progress"].Played { + t.Fatalf("partial ebook state = %+v, want not played", states["ebook-progress"]) + } +} + +func TestResolveItemUserStatesExcludesHiddenEbookReaderProgress(t *testing.T) { + ctx := context.Background() + store := newProfileTestStore(t) + updatedAt := time.Date(2026, 6, 4, 10, 0, 0, 0, time.UTC) + items := []*models.MediaItem{ + {ContentID: "ebook-hidden", Type: "ebook", Title: "Hidden Ebook"}, + {ContentID: "ebook-reread", Type: "ebook", Title: "Reread Ebook"}, + } + + states, err := resolveItemUserStatesWithOptions(ctx, store, "profile-1", nil, items, itemUserStateOptions{ + UserID: 42, + EbookProgressStore: &fakeEbookReaderProgressLister{ + progress: map[string]EbookReaderProgress{ + "ebook-hidden": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-hidden", + Progress: 0.95, + UpdatedAt: updatedAt, + }, + "ebook-reread": { + UserID: 42, + ProfileID: "profile-1", + ContentID: "ebook-reread", + Progress: 0.95, + UpdatedAt: updatedAt.Add(2 * time.Hour), + }, + }, + hiddenBefore: map[string]time.Time{ + "ebook-hidden": updatedAt.Add(time.Hour), + "ebook-reread": updatedAt.Add(time.Hour), + }, + }, + }) + if err != nil { + t.Fatalf("resolveItemUserStatesWithOptions: %v", err) + } + + if states["ebook-hidden"] == nil || states["ebook-hidden"].Played { + t.Fatalf("hidden ebook state = %+v, want not played", states["ebook-hidden"]) + } + if states["ebook-reread"] == nil || !states["ebook-reread"].Played { + t.Fatalf("ebook updated after hidden_before = %+v, want played again", states["ebook-reread"]) + } +} diff --git a/internal/api/handlers/watch_tonight.go b/internal/api/handlers/watch_tonight.go index 2cb52821..10c7e997 100644 --- a/internal/api/handlers/watch_tonight.go +++ b/internal/api/handlers/watch_tonight.go @@ -362,7 +362,10 @@ func (h *RecommendationsHandler) enrichItems(r *http.Request, userID int, profil if h.storeProvider != nil { store, storeErr := h.storeProvider.ForUser(ctx, userID) if storeErr == nil && store != nil { - states, stateErr := resolveItemUserStates(ctx, store, profileID, h.EpisodeRepo, mediaItems) + states, stateErr := resolveItemUserStatesWithOptions(ctx, store, profileID, h.EpisodeRepo, mediaItems, itemUserStateOptions{ + UserID: userID, + EbookProgressStore: h.EbookProgress, + }) if stateErr == nil { stateMap = states } diff --git a/internal/api/requests_wiring.go b/internal/api/requests_wiring.go new file mode 100644 index 00000000..7e03d343 --- /dev/null +++ b/internal/api/requests_wiring.go @@ -0,0 +1,36 @@ +package api + +import ( + "context" + "errors" + + "github.com/Silo-Server/silo-server/internal/plugins" + mediarequests "github.com/Silo-Server/silo-server/internal/requests" +) + +// PluginRequestRouterAdapter adapts plugins.Service to +// mediarequests.RouterClientResolver. The concrete *pluginhost.RequestRouterClient +// satisfies mediarequests.RouterClient, so the adapter names the interface as its +// return type (Go has no return-type covariance). +type PluginRequestRouterAdapter struct { + Svc *plugins.Service +} + +func (a PluginRequestRouterAdapter) RequestRouterClient(ctx context.Context, installationID int, capabilityID string) (mediarequests.RouterClient, error) { + if a.Svc == nil { + return nil, errors.New("request router plugin service is not configured") + } + return a.Svc.RequestRouterClient(ctx, installationID, capabilityID) +} + +// AttachRequestRouter wires the plugin-backed router provider onto a requests +// service. Both the HTTP handler and the reconcile task call this so the wiring +// lives in one place. With either dependency absent (e.g. a build without the +// plugin service) it leaves the requests service router-less, so fulfillment +// degrades to the existing "no backend configured" failure instead of panicking. +func AttachRequestRouter(svc *mediarequests.Service, pluginService *plugins.Service) { + if svc == nil || pluginService == nil { + return + } + svc.SetRouterProvider(mediarequests.NewPluginRouterProvider(PluginRequestRouterAdapter{pluginService})) +} diff --git a/internal/api/requests_wiring_test.go b/internal/api/requests_wiring_test.go new file mode 100644 index 00000000..f0d77a01 --- /dev/null +++ b/internal/api/requests_wiring_test.go @@ -0,0 +1,22 @@ +package api + +import ( + "context" + "testing" +) + +// AttachRequestRouter must tolerate absent dependencies (e.g. a build with the +// plugin service disabled) without panicking — fulfillment then degrades to the +// existing "no backend configured" failure rather than crashing the wiring. +func TestAttachRequestRouterNilDependenciesDoesNotPanic(t *testing.T) { + AttachRequestRouter(nil, nil) +} + +// A router adapter installed without a plugin service must return a controlled +// error rather than panic on the request path. +func TestPluginRequestRouterAdapterNilServiceReturnsError(t *testing.T) { + adapter := PluginRequestRouterAdapter{} + if _, err := adapter.RequestRouterClient(context.Background(), 1, "request_router.v1"); err == nil { + t.Fatal("RequestRouterClient with nil Svc = nil error, want a controlled error") + } +} diff --git a/internal/api/router.go b/internal/api/router.go index 9782aa21..e14c0bea 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -48,8 +48,6 @@ import ( "github.com/Silo-Server/silo-server/internal/ratelimit" "github.com/Silo-Server/silo-server/internal/recommendations" mediarequests "github.com/Silo-Server/silo-server/internal/requests" - "github.com/Silo-Server/silo-server/internal/requests/radarr" - "github.com/Silo-Server/silo-server/internal/requests/sonarr" "github.com/Silo-Server/silo-server/internal/s3client" "github.com/Silo-Server/silo-server/internal/scanner" "github.com/Silo-Server/silo-server/internal/scanqueue" @@ -372,7 +370,14 @@ func NewRouter(deps Dependencies) chi.Router { var webhookSyncHandler *handlers.WebhookSyncHandler var requestHandler *handlers.RequestsHandler var autoscanHandler *handlers.AutoscanHandler + var ebookReaderHandler *handlers.EbookReaderHandler + var ebookProgressStore *handlers.PGEbookReaderProgressStore + var ebookConfigStore *handlers.PGEbookReaderConfigStore + var ebookAnnotationStore *handlers.PGEbookReaderAnnotationStore if deps.DB != nil { + ebookProgressStore = handlers.NewPGEbookReaderProgressStore(deps.DB) + ebookConfigStore = handlers.NewPGEbookReaderConfigStore(deps.DB) + ebookAnnotationStore = handlers.NewPGEbookReaderAnnotationStore(deps.DB) browseRepo := catalog.NewBrowseRepository(deps.DB) itemRepo = catalog.NewItemRepository(deps.DB) episodeRepo = catalog.NewEpisodeRepository(deps.DB) @@ -423,6 +428,25 @@ func NewRouter(deps Dependencies) chi.Router { if dispatcher, ok := deps.WatchProviderService.(handlers.LocalWatchEventDispatcher); ok { itemsHandler.SetLocalWatchEventDispatcher(dispatcher) } + if ebookProgressStore != nil { + itemsHandler.SetEbookReaderProgressStore(ebookProgressStore) + } + if deps.FileRepo != nil { + ebookReaderHandler = handlers.NewEbookReaderHandler(&handlers.MediaFileAuthorizer{ + FileResolver: deps.FileRepo, + ItemAccess: itemRepo, + EpisodeLookup: episodeRepo, + }) + if ebookProgressStore != nil { + ebookReaderHandler.ProgressStore = ebookProgressStore + } + if ebookConfigStore != nil { + ebookReaderHandler.ConfigStore = ebookConfigStore + } + if ebookAnnotationStore != nil { + ebookReaderHandler.AnnotationStore = ebookAnnotationStore + } + } catalogResourceHandler = handlers.NewCatalogResourceHandler(itemsHandler) catalogHandler = handlers.NewCatalogHandler( catalog.NewCatalogResolver(browseRepo, itemRepo). @@ -441,7 +465,8 @@ func NewRouter(deps Dependencies) chi.Router { tmdb.NewClient(tmdbAPIKey, 40), mediarequests.NewCatalogPresence(itemRepo, providerIDRepo), ) - requestSvc.SetFulfillmentAdapters(radarr.NewClient(nil), sonarr.NewClient(nil)) + AttachRequestRouter(requestSvc, deps.PluginService) + requestSvc.SetRequesterIdentityResolver(plugins.RequesterIdentityFromLookup(plugins.NewPgUserIdentityLookup(deps.DB))) if viewerResolver != nil { requestSvc.SetEntitlementResolver(mediarequests.NewAccessEntitlements(viewerResolver)) } @@ -500,6 +525,9 @@ func NewRouter(deps Dependencies) chi.Router { if detailSvc != nil { personalDataHandler.SetDetailService(detailSvc) } + if ebookProgressStore != nil { + personalDataHandler.SetEbookReaderProgressStore(ebookProgressStore) + } personalDataHandler.SetEpisodeRepo(episodeRepo) personalDataHandler.SetSeasonRepo(seasonRepo) personalDataHandler.EventsHub = deps.EventsHub @@ -924,6 +952,7 @@ func NewRouter(deps Dependencies) chi.Router { sectionFetcher.StoreProvider = deps.UserStoreProvider sectionFetcher.CollectionRepo = catalog.NewLibraryCollectionRepository(deps.DB) sectionFetcher.NextUpRepo = catalog.NewNextUpRepository(deps.DB, deps.UserStoreProvider) + sectionFetcher.AudiobookNextRepo = catalog.NewAudiobookNextRepository(deps.DB) if deps.DB != nil { sectionFetcher.RecommendationRepo = recommendations.NewRepo(deps.DB) if ratingsRepo != nil { @@ -939,6 +968,9 @@ func NewRouter(deps Dependencies) chi.Router { } sectionHandler.EpisodeRepo = episodeRepo sectionHandler.DetailSvc = detailSvc + if ebookProgressStore != nil { + sectionHandler.EbookProgress = ebookProgressStore + } if userRepo != nil { sectionHandler.UserRepo = userRepo } @@ -1112,6 +1144,7 @@ func NewRouter(deps Dependencies) chi.Router { recsFetcher := sections.NewFetcher(deps.DB) recsFetcher.StoreProvider = deps.UserStoreProvider recsFetcher.NextUpRepo = catalog.NewNextUpRepository(deps.DB, deps.UserStoreProvider) + recsFetcher.AudiobookNextRepo = catalog.NewAudiobookNextRepository(deps.DB) recsHandler.Fetcher = recsFetcher recsHandler.WatchTonightFetcher = recsFetcher } @@ -1120,6 +1153,9 @@ func NewRouter(deps Dependencies) chi.Router { } recsHandler.CalendarRepo = calendarRepo recsHandler.EpisodeRepo = episodeRepo + if ebookProgressStore != nil { + recsHandler.EbookProgress = ebookProgressStore + } if deps.PersonRepo != nil { recsHandler.CastFetcher = deps.PersonRepo } @@ -1420,6 +1456,7 @@ func NewRouter(deps Dependencies) chi.Router { r.Get("/catalog", catalogHandler.HandleGetCatalog) r.Get("/catalog/filters", catalogHandler.HandleGetCatalogFilters) r.Get("/catalog/filters/search", catalogHandler.HandleGetCatalogFacetSearch) + r.Get("/catalog/audiobook-groups", catalogHandler.HandleGetAudiobookGroups) r.Post("/catalog/query", catalogHandler.HandlePostCatalogQuery) if catalogResourceHandler != nil { r.Get("/catalog/items/{id}", catalogResourceHandler.HandleGetItemDetail) @@ -1706,6 +1743,22 @@ func NewRouter(deps Dependencies) chi.Router { }) } + if ebookReaderHandler != nil { + r.Route("/ebooks", func(r chi.Router) { + r.Use(apimw.RequireProfile) + r.Get("/{content_id}/files/{file_id}/read", ebookReaderHandler.HandleReadFile) + r.Head("/{content_id}/files/{file_id}/read", ebookReaderHandler.HandleReadFile) + r.Get("/{content_id}/progress", ebookReaderHandler.HandleGetProgress) + r.Put("/{content_id}/progress", ebookReaderHandler.HandleSaveProgress) + r.Get("/{content_id}/reader-config", ebookReaderHandler.HandleGetConfig) + r.Put("/{content_id}/reader-config", ebookReaderHandler.HandleSaveConfig) + r.Get("/{content_id}/annotations", ebookReaderHandler.HandleListAnnotations) + r.Post("/{content_id}/annotations", ebookReaderHandler.HandleCreateAnnotation) + r.Patch("/{content_id}/annotations/{annotation_id}", ebookReaderHandler.HandleUpdateAnnotation) + r.Delete("/{content_id}/annotations/{annotation_id}", ebookReaderHandler.HandleDeleteAnnotation) + }) + } + // Subtitle search + AI translation routes. if subtitleSearchHandler != nil { if deps.FileRepo != nil && itemRepo != nil { @@ -2157,8 +2210,11 @@ func NewRouter(deps Dependencies) chi.Router { }) } - // Rate limit admin routes - if deps.RateLimitMW != nil && settingsRepo != nil { + // Rate limit admin routes. Mounted even when the limiter is not + // running (deps.RateLimitMW == nil) so admins can always reach the + // config — otherwise disabling rate limiting and restarting would + // permanently lock the settings page out of re-enabling it. + if settingsRepo != nil { rateLimitHandler := handlers.NewRateLimitHandler(settingsRepo, deps.RateLimitMW, deps.EventBus) r.Route("/rate-limits", func(r chi.Router) { r.Get("/config", rateLimitHandler.HandleGetConfig) diff --git a/internal/audiobooks/abs_progress_store.go b/internal/audiobooks/abs_progress_store.go index b7388478..461d4ee6 100644 --- a/internal/audiobooks/abs_progress_store.go +++ b/internal/audiobooks/abs_progress_store.go @@ -38,9 +38,13 @@ func (s *ABSProgressStore) GetProgress(ctx context.Context, userID, profileID, c var completed bool var progressPct *float64 + // Completed rows store position_seconds = 0 (no resume point), so the + // percentage must come from the completed flag, not the position. dbRow := s.Pool.QueryRow(ctx, ` SELECT media_item_id, position_seconds, duration_seconds, completed, - CASE WHEN duration_seconds > 0 THEN position_seconds / duration_seconds ELSE 0 END AS progress_pct, + CASE WHEN completed THEN 1.0 + WHEN duration_seconds > 0 THEN position_seconds / duration_seconds + ELSE 0 END AS progress_pct, updated_at FROM user_watch_progress WHERE user_id = $1 AND profile_id = $2 AND media_item_id = $3`, @@ -85,7 +89,9 @@ func (s *ABSProgressStore) ListProgressForAudiobooks(ctx context.Context, userID wp.position_seconds, wp.duration_seconds, wp.completed, - CASE WHEN wp.duration_seconds > 0 THEN wp.position_seconds / wp.duration_seconds ELSE 0 END, + CASE WHEN wp.completed THEN 1.0 + WHEN wp.duration_seconds > 0 THEN wp.position_seconds / wp.duration_seconds + ELSE 0 END, wp.updated_at FROM user_watch_progress wp JOIN media_items mi ON mi.content_id = wp.media_item_id @@ -138,17 +144,27 @@ func (s *ABSProgressStore) UpsertProgress(ctx context.Context, row abs.ProgressR if updatedAt.IsZero() { updatedAt = time.Now().UTC() } + // Completed rows hold no resume point (position_seconds = 0) — the same + // invariant as the userstore write paths — so finished books can't leak + // into position-based continue-watching/listening queries. A later + // non-finished report (re-listen) moves position forward from 0 while + // the completed latch stays. + position := row.CurrentSeconds + if row.IsFinished { + position = 0 + } _, err = s.Pool.Exec(ctx, ` INSERT INTO user_watch_progress (user_id, profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (user_id, profile_id, media_item_id) DO UPDATE SET - position_seconds = GREATEST(user_watch_progress.position_seconds, EXCLUDED.position_seconds), + position_seconds = CASE WHEN EXCLUDED.completed THEN 0 + ELSE GREATEST(user_watch_progress.position_seconds, EXCLUDED.position_seconds) END, duration_seconds = GREATEST(user_watch_progress.duration_seconds, EXCLUDED.duration_seconds), completed = user_watch_progress.completed OR EXCLUDED.completed, updated_at = GREATEST(user_watch_progress.updated_at, EXCLUDED.updated_at)`, uid, row.ProfileID, row.ContentID, - row.CurrentSeconds, row.DurationSeconds, row.IsFinished, updatedAt, + position, row.DurationSeconds, row.IsFinished, updatedAt, ) if err != nil { return fmt.Errorf("abs_progress_store: upsert progress: %w", err) diff --git a/internal/audiobooks/enrichment.go b/internal/audiobooks/enrichment.go index b9783e2a..8f63bb8f 100644 --- a/internal/audiobooks/enrichment.go +++ b/internal/audiobooks/enrichment.go @@ -44,7 +44,8 @@ const ( // defaultEnrichBatchSize is the maximum number of audiobook items processed // per sweep invocation. Keeps latency bounded for large libraries. - defaultEnrichBatchSize = 50 + // Override with SILO_AUDIOBOOK_ENRICH_BATCH_SIZE. + defaultEnrichBatchSize = 250 // defaultEnrichWorkers is the default fan-out used by Enricher.Run. // Network-bound: each worker holds one provider HTTP call at a time, so // 4 is enough to mask single-request latency without hammering plugins. @@ -52,18 +53,28 @@ const ( defaultEnrichWorkers = 4 ) +// audiobookEnrichBatchSize returns the configured maximum sweep size. +func audiobookEnrichBatchSize() int { + if v := os.Getenv("SILO_AUDIOBOOK_ENRICH_BATCH_SIZE"); v != "" { + if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 { + return parsed + } + } + return defaultEnrichBatchSize +} + // audiobookEnrichWorkers returns the configured number of parallel enrichment -// workers, capped to defaultEnrichBatchSize so workers never outnumber the -// batch they drain. -func audiobookEnrichWorkers() int { +// workers, capped to the active batch size so workers never outnumber the batch +// they drain. +func audiobookEnrichWorkers(batchSize int) int { n := defaultEnrichWorkers if v := os.Getenv("SILO_AUDIOBOOK_ENRICH_WORKERS"); v != "" { if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 { n = parsed } } - if n > defaultEnrichBatchSize { - n = defaultEnrichBatchSize + if batchSize > 0 && n > batchSize { + n = batchSize } return n } @@ -105,6 +116,7 @@ func NewEnricher( personRepo *catalog.PersonRepository, providerIDs *catalog.ProviderIDRepository, ) *Enricher { + batchSize := audiobookEnrichBatchSize() return &Enricher{ pool: pool, chainRepo: chainRepo, @@ -112,8 +124,8 @@ func NewEnricher( itemRepo: itemRepo, personRepo: personRepo, providerIDs: providerIDs, - batchSize: defaultEnrichBatchSize, - workers: audiobookEnrichWorkers(), + batchSize: batchSize, + workers: audiobookEnrichWorkers(batchSize), } } @@ -166,6 +178,32 @@ func (e *Enricher) Run(ctx context.Context) (int, error) { return enriched, nil } +// HasPendingItems reports whether a scheduled sweep has any audiobook rows to +// process. It mirrors claimBatch's eligibility predicate without loading rows +// or provider IDs, so the scheduler can skip no-op executions without adding +// task-history noise. +func (e *Enricher) HasPendingItems(ctx context.Context) (bool, error) { + if e == nil || e.pool == nil || e.chainRepo == nil { + return false, nil + } + + var exists bool + err := e.pool.QueryRow(ctx, ` + SELECT EXISTS( + SELECT 1 + FROM media_items mi + WHERE mi.type = 'audiobook' + AND (mi.poster_path IS NULL OR mi.poster_path = '') + AND mi.last_refreshed IS NULL + LIMIT 1 + ) + `).Scan(&exists) + if err != nil { + return false, fmt.Errorf("checking pending audiobook enrichment: %w", err) + } + return exists, nil +} + // runBatch processes items in parallel using e.workers goroutines. The // enrichFn parameter exists for testing: production calls pass e.enrichItem. // Returns the count of items where enrichFn returned nil. diff --git a/internal/audiobooks/enrichment_test.go b/internal/audiobooks/enrichment_test.go index 6c440da9..903847bc 100644 --- a/internal/audiobooks/enrichment_test.go +++ b/internal/audiobooks/enrichment_test.go @@ -63,6 +63,50 @@ func TestEnricherRunFansOut(t *testing.T) { } } +func TestNewEnricherUsesConfiguredBatchSize(t *testing.T) { + t.Setenv("SILO_AUDIOBOOK_ENRICH_BATCH_SIZE", "123") + t.Setenv("SILO_AUDIOBOOK_ENRICH_WORKERS", "8") + + e := NewEnricher(nil, nil, nil, nil, nil, nil) + + if e.batchSize != 123 { + t.Fatalf("batchSize = %d, want 123", e.batchSize) + } + if e.workers != 8 { + t.Fatalf("workers = %d, want 8", e.workers) + } +} + +func TestNewEnricherCapsWorkersToConfiguredBatchSize(t *testing.T) { + t.Setenv("SILO_AUDIOBOOK_ENRICH_BATCH_SIZE", "50") + t.Setenv("SILO_AUDIOBOOK_ENRICH_WORKERS", "100") + + e := NewEnricher(nil, nil, nil, nil, nil, nil) + + if e.batchSize != 50 { + t.Fatalf("batchSize = %d, want 50", e.batchSize) + } + if e.workers != e.batchSize { + t.Fatalf("workers = %d, want capped batchSize %d", e.workers, e.batchSize) + } +} + +func TestAudiobookEnrichWorkersCapsAtBatchSize(t *testing.T) { + t.Setenv("SILO_AUDIOBOOK_ENRICH_WORKERS", "8") + + if got := audiobookEnrichWorkers(3); got != 3 { + t.Fatalf("audiobookEnrichWorkers(3) = %d, want 3", got) + } +} + +func TestAudiobookEnrichBatchSizeIgnoresInvalidEnv(t *testing.T) { + t.Setenv("SILO_AUDIOBOOK_ENRICH_BATCH_SIZE", "nope") + + if got := audiobookEnrichBatchSize(); got != defaultEnrichBatchSize { + t.Fatalf("audiobookEnrichBatchSize() = %d, want %d", got, defaultEnrichBatchSize) + } +} + func TestCacheRemotePosterCachesProviderURL(t *testing.T) { cacher := &fakeAudiobookImageCacher{} e := &Enricher{imageCacher: cacher} diff --git a/internal/audiobooks/podcastfeed/refresher.go b/internal/audiobooks/podcastfeed/refresher.go index a9fd8d5e..b33add5e 100644 --- a/internal/audiobooks/podcastfeed/refresher.go +++ b/internal/audiobooks/podcastfeed/refresher.go @@ -106,6 +106,23 @@ func (r *Refresher) WithHTTPClient(hc *http.Client) *Refresher { return r } +// HasDue reports whether RefreshDue would attempt at least one feed. The task +// scheduler uses this to avoid recording empty maintenance runs when no podcast +// feeds exist or every feed is still inside its refresh window. +func (r *Refresher) HasDue(ctx context.Context, s Store) (bool, error) { + feeds, err := s.ListPodcastFeeds(ctx) + if err != nil { + return false, fmt.Errorf("list podcast feeds: %w", err) + } + now := time.Now() + for _, f := range feeds { + if shouldRefreshFeed(f, now) { + return true, nil + } + } + return false, nil +} + // RefreshDue walks every podcast_feeds row and refreshes those whose // last_refreshed_at + refresh_interval has elapsed. Per-feed failures are // logged at Warn and do not abort the walk. Returns the count of feeds @@ -118,10 +135,7 @@ func (r *Refresher) RefreshDue(ctx context.Context, s Store) (int, error) { now := time.Now() attempted := 0 for _, f := range feeds { - if f.FeedURL == "" { - continue - } - if !isDue(f, now) { + if !shouldRefreshFeed(f, now) { continue } attempted++ @@ -136,6 +150,10 @@ func (r *Refresher) RefreshDue(ctx context.Context, s Store) (int, error) { return attempted, nil } +func shouldRefreshFeed(f PodcastFeed, now time.Time) bool { + return f.FeedURL != "" && isDue(f, now) +} + // RefreshOne refreshes a single podcast feed. Public so the admin // force-refresh endpoint can call it directly. Records success or failure // in podcast_feeds.last_refresh_error regardless of outcome. diff --git a/internal/audiobooks/service.go b/internal/audiobooks/service.go index cfd15ffb..7553365b 100644 --- a/internal/audiobooks/service.go +++ b/internal/audiobooks/service.go @@ -162,7 +162,7 @@ func (s *Service) BuildABSHandler(deps ABSHandlerDeps) *abs.Handler { if deps.Detail == nil { return "" } - return deps.Detail.PresignURL(ctx, path, variant) + return deps.Detail.PresignImageURL(ctx, path, "poster", variant) }, }) s.ABSHandler = h diff --git a/internal/audiobooks/service_test.go b/internal/audiobooks/service_test.go index 4e2e1321..1ad9d07d 100644 --- a/internal/audiobooks/service_test.go +++ b/internal/audiobooks/service_test.go @@ -3,7 +3,14 @@ package audiobooks import ( "context" "errors" + "reflect" + "strings" "testing" + "unsafe" + + "github.com/Silo-Server/silo-server/internal/audiobooks/abs" + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/scanner" ) type fakeSettingsReader struct { @@ -74,3 +81,50 @@ func TestServiceABSCompatEnabledNilSettingsReturnsFalse(t *testing.T) { t.Fatal("ABSCompatEnabled = true, want false") } } + +func TestBuildABSHandlerCoverResolverUsesPosterVariant(t *testing.T) { + resolver := &recordingImageResolver{} + detail := &catalog.DetailService{} + detail.SetImageResolver(resolver) + + handler := New(nil).BuildABSHandler(ABSHandlerDeps{ + Items: &catalog.ItemRepository{}, + Files: &scanner.FileRepository{}, + Detail: detail, + }) + + coverResolver := absCoverResolverForTest(t, handler) + got := coverResolver(context.Background(), "local/audiobooks/book-1/poster/original.webp", "card") + + if !strings.Contains(got, "/w500.webp") { + t.Fatalf("resolved URL = %q, want w500 poster variant", got) + } + if resolver.variant != "featured" { + t.Fatalf("resolver variant = %q, want featured", resolver.variant) + } +} + +func absCoverResolverForTest(t *testing.T, handler *abs.Handler) func(context.Context, string, string) string { + t.Helper() + field := reflect.ValueOf(handler).Elem().FieldByName("deps").FieldByName("CoverResolver") + return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface().(func(context.Context, string, string) string) +} + +type recordingImageResolver struct { + path string + variant string +} + +func (r *recordingImageResolver) ResolveImageURL(_ context.Context, path string, variant string) string { + r.path = path + r.variant = variant + return "resolved://" + path +} + +func (r *recordingImageResolver) ResolveImageURLs(_ context.Context, paths []string, variant string) map[string]string { + out := make(map[string]string, len(paths)) + for _, path := range paths { + out[path] = r.ResolveImageURL(context.Background(), path, variant) + } + return out +} diff --git a/internal/catalog/access_filter.go b/internal/catalog/access_filter.go index 0f1aa09b..341b9c36 100644 --- a/internal/catalog/access_filter.go +++ b/internal/catalog/access_filter.go @@ -25,6 +25,11 @@ type AccessFilter struct { // given (case-insensitive) prefix. Pushed into the SQL WHERE clause so // the predicate can use idx_media_items_sort_key. NamePrefix string + // ExcludedMediaTypes lists media_items.type values the viewer's surface + // never exposes (e.g. the Jellyfin compat layer excludes "audiobook" and + // "podcast" — they're served by the ABS-compat API instead). Applied by + // every query builder that consumes an AccessFilter. + ExcludedMediaTypes []string } func applyAccessFilter(alias string, filter AccessFilter, conditions *[]string, args *[]any, argIdx *int) { @@ -38,6 +43,11 @@ func applyAccessFilter(alias string, filter AccessFilter, conditions *[]string, *argIdx = *argIdx + 1 } } + if len(filter.ExcludedMediaTypes) > 0 { + *conditions = append(*conditions, fmt.Sprintf("NOT (%s.type = ANY($%d))", alias, *argIdx)) + *args = append(*args, filter.ExcludedMediaTypes) + *argIdx = *argIdx + 1 + } } // ApplySectionAccessFilter applies non-library access constraints to section queries. diff --git a/internal/catalog/audiobook_groups.go b/internal/catalog/audiobook_groups.go new file mode 100644 index 00000000..c6cbbd87 --- /dev/null +++ b/internal/catalog/audiobook_groups.go @@ -0,0 +1,201 @@ +package catalog + +import ( + "context" + "fmt" + "strings" + + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/Silo-Server/silo-server/internal/models" +) + +// AudiobookGroupBy enumerates the supported audiobook grouping axes. +type AudiobookGroupBy string + +const ( + AudiobookGroupByAuthor AudiobookGroupBy = "author" + AudiobookGroupByNarrator AudiobookGroupBy = "narrator" + AudiobookGroupBySeries AudiobookGroupBy = "series" +) + +// ParseAudiobookGroupBy validates a group_by request parameter. +func ParseAudiobookGroupBy(raw string) (AudiobookGroupBy, bool) { + switch AudiobookGroupBy(strings.ToLower(strings.TrimSpace(raw))) { + case AudiobookGroupByAuthor: + return AudiobookGroupByAuthor, true + case AudiobookGroupByNarrator: + return AudiobookGroupByNarrator, true + case AudiobookGroupBySeries: + return AudiobookGroupBySeries, true + default: + return "", false + } +} + +// AudiobookGroupsQuery controls the grouped audiobook browse lookup. +type AudiobookGroupsQuery struct { + LibraryID int + GroupBy AudiobookGroupBy + // Sort is one of "name" (default), "count", "duration". + Sort string + Limit int + Offset int +} + +// AudiobookGroup is one grouped browse row: an author, narrator, or series +// with aggregate stats over the audiobooks visible to the viewer. +type AudiobookGroup struct { + Name string + ItemCount int + TotalDurationSeconds int64 + InProgressCount int + FinishedCount int + // PosterPaths holds up to four raw poster paths for cover stacks; the + // API layer presigns them. + PosterPaths []string +} + +// ListAudiobookGroups returns grouped browse rows for an audiobook library. +// Groups are aggregated per person name (authors/narrators) or per series +// name, matching the case-insensitive semantics of the corresponding catalog +// filters, so a group name can be fed straight back into an author/narrator/ +// series filter rule. Progress counts are scoped to the requesting profile via +// filter.UserID / filter.ProfileID. +func ListAudiobookGroups(ctx context.Context, pool *pgxpool.Pool, q AudiobookGroupsQuery, filter AccessFilter) ([]AudiobookGroup, int, error) { + if pool == nil { + return nil, 0, fmt.Errorf("audiobook groups: no database pool") + } + if q.LibraryID <= 0 { + return nil, 0, fmt.Errorf("audiobook groups: library id is required") + } + + limit := q.Limit + if limit <= 0 { + limit = 200 + } + if limit > 500 { + limit = 500 + } + offset := max(q.Offset, 0) + + args := []any{q.LibraryID} + argIdx := 2 + + conditions := []string{ + "mi.type = 'audiobook'", + "mil.media_folder_id = $1", + } + if !appendAudiobookItemAccessConditions("mi", filter, &conditions, &args, &argIdx) { + return []AudiobookGroup{}, 0, nil + } + + var joinClause, nameExpr, groupExpr, posterOrder string + switch q.GroupBy { + case AudiobookGroupByAuthor, AudiobookGroupByNarrator: + kind := models.PersonKindAuthor + if q.GroupBy == AudiobookGroupByNarrator { + kind = models.PersonKindNarrator + } + joinClause = fmt.Sprintf(`JOIN item_people ip ON ip.content_id = b.content_id AND ip.kind = $%d + JOIN people p ON p.id = ip.person_id`, argIdx) + args = append(args, int(kind)) + argIdx++ + nameExpr = "MIN(BTRIM(p.name))" + groupExpr = "LOWER(BTRIM(p.name))" + posterOrder = "b.sort_title" + case AudiobookGroupBySeries: + joinClause = "JOIN audiobook_series s ON s.content_id = b.content_id" + nameExpr = "MIN(BTRIM(s.series_name))" + groupExpr = "LOWER(BTRIM(s.series_name))" + posterOrder = "s.series_index NULLS LAST, b.sort_title" + default: + return nil, 0, fmt.Errorf("audiobook groups: unsupported group_by %q", q.GroupBy) + } + + userArg := fmt.Sprintf("$%d", argIdx) + args = append(args, filter.UserID) + argIdx++ + profileArg := fmt.Sprintf("$%d", argIdx) + args = append(args, filter.ProfileID) + argIdx++ + + var orderClause string + switch strings.ToLower(strings.TrimSpace(q.Sort)) { + case "", "name": + orderClause = "LOWER(name)" + case "count": + orderClause = "item_count DESC, LOWER(name)" + case "duration": + orderClause = "total_duration_seconds DESC, LOWER(name)" + default: + return nil, 0, fmt.Errorf("audiobook groups: unsupported sort %q", q.Sort) + } + + query := fmt.Sprintf(` + WITH books AS ( + SELECT + mi.content_id, + mi.poster_path, + mi.sort_title, + COALESCE(( + SELECT SUM(mf.duration) + FROM media_files mf + WHERE mf.content_id = mi.content_id AND mf.missing_since IS NULL + ), 0) AS duration_seconds + FROM media_items mi + JOIN media_item_libraries mil ON mil.content_id = mi.content_id + WHERE %s + ), + grouped AS ( + SELECT + %s AS name, + COUNT(*)::int AS item_count, + COALESCE(SUM(b.duration_seconds), 0)::bigint AS total_duration_seconds, + COUNT(*) FILTER (WHERE uwp.media_item_id IS NOT NULL AND NOT uwp.completed)::int AS in_progress_count, + COUNT(*) FILTER (WHERE uwp.completed)::int AS finished_count, + (ARRAY_AGG(b.poster_path ORDER BY %s) FILTER (WHERE NULLIF(b.poster_path, '') IS NOT NULL))[1:4] AS poster_paths + FROM books b + %s + LEFT JOIN user_watch_progress uwp + ON uwp.media_item_id = b.content_id + AND uwp.user_id = %s + AND uwp.profile_id = %s + GROUP BY %s + ) + SELECT name, item_count, total_duration_seconds, in_progress_count, finished_count, poster_paths, + COUNT(*) OVER ()::int AS total_groups + FROM grouped + ORDER BY %s + LIMIT $%d OFFSET $%d`, + strings.Join(conditions, " AND "), + nameExpr, posterOrder, joinClause, userArg, profileArg, groupExpr, orderClause, + argIdx, argIdx+1, + ) + args = append(args, limit, offset) + + rows, err := pool.Query(ctx, query, args...) + if err != nil { + return nil, 0, fmt.Errorf("querying audiobook groups: %w", err) + } + defer rows.Close() + + total := 0 + groups := make([]AudiobookGroup, 0, limit) + for rows.Next() { + var g AudiobookGroup + var posterPaths []string + if err := rows.Scan(&g.Name, &g.ItemCount, &g.TotalDurationSeconds, &g.InProgressCount, &g.FinishedCount, &posterPaths, &total); err != nil { + return nil, 0, fmt.Errorf("scanning audiobook group: %w", err) + } + if posterPaths == nil { + posterPaths = []string{} + } + g.PosterPaths = posterPaths + groups = append(groups, g) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("iterating audiobook groups: %w", err) + } + return groups, total, nil +} diff --git a/internal/catalog/audiobook_next_repo.go b/internal/catalog/audiobook_next_repo.go new file mode 100644 index 00000000..d6b7a424 --- /dev/null +++ b/internal/catalog/audiobook_next_repo.go @@ -0,0 +1,163 @@ +package catalog + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// NextInSeriesQuery controls the audiobook next-in-series lookup. +type NextInSeriesQuery struct { + UserID int + ProfileID string + Limit int + // LibraryID / LibraryIDs / DisabledLibraryIDs scope the candidate books in + // SQL. Without this, a profile whose recently finished series live in a + // different library would consume the LIMIT with candidates the caller + // then filters out, starving a library-scoped section. + LibraryID *int + LibraryIDs []int + DisabledLibraryIDs []int +} + +// NextInSeriesResult is one row from the next-in-series query: the next +// unstarted audiobook in a series the profile has finished a book of. +type NextInSeriesResult struct { + ContentID string + SeriesName string + SeriesIndex *float64 + LastFinishedAt time.Time +} + +// AudiobookNextRepository queries audiobook series progression for the +// next_in_series section. +type AudiobookNextRepository struct { + pool *pgxpool.Pool +} + +// NewAudiobookNextRepository creates an AudiobookNextRepository. +func NewAudiobookNextRepository(pool *pgxpool.Pool) *AudiobookNextRepository { + return &AudiobookNextRepository{pool: pool} +} + +// ListNextInSeries returns, for each audiobook series the profile has finished +// at least one book of, the lowest-indexed later book the profile has not +// started yet. Series surface in most-recently-finished order. Books already +// in progress are excluded — they belong to Continue Listening, not here. +// +// Candidate books are library-scoped in SQL (see NextInSeriesQuery); remaining +// access filtering (content rating) is applied by the caller when resolving +// content IDs to items. +func (r *AudiobookNextRepository) ListNextInSeries(ctx context.Context, q NextInSeriesQuery) ([]NextInSeriesResult, error) { + if r == nil || r.pool == nil || q.UserID <= 0 || q.ProfileID == "" { + return nil, nil + } + + limit := q.Limit + if limit <= 0 { + limit = 20 + } + + args := []any{q.UserID, q.ProfileID} + argIdx := 3 + candidateScope := "" + if q.LibraryID != nil { + candidateScope += fmt.Sprintf(` + AND EXISTS ( + SELECT 1 FROM media_item_libraries mil + WHERE mil.content_id = m.content_id AND mil.media_folder_id = $%d + )`, argIdx) + args = append(args, *q.LibraryID) + argIdx++ + } else if q.LibraryIDs != nil { + if len(q.LibraryIDs) == 0 { + return nil, nil + } + candidateScope += fmt.Sprintf(` + AND EXISTS ( + SELECT 1 FROM media_item_libraries mil + WHERE mil.content_id = m.content_id AND mil.media_folder_id = ANY($%d) + )`, argIdx) + args = append(args, q.LibraryIDs) + argIdx++ + } + if len(q.DisabledLibraryIDs) > 0 { + candidateScope += fmt.Sprintf(` + AND NOT EXISTS ( + SELECT 1 FROM media_item_libraries mil_disabled + WHERE mil_disabled.content_id = m.content_id + AND mil_disabled.media_folder_id = ANY($%d) + )`, argIdx) + args = append(args, q.DisabledLibraryIDs) + argIdx++ + } + + query := fmt.Sprintf(` + WITH finished_series AS ( + SELECT + LOWER(BTRIM(s.series_name)) AS series_key, + MIN(BTRIM(s.series_name)) AS series_name, + MAX(s.series_index) AS max_finished_index, + MAX(uwp.updated_at) AS last_finished_at + FROM user_watch_progress uwp + JOIN audiobook_series s ON s.content_id = uwp.media_item_id + JOIN media_items mi ON mi.content_id = uwp.media_item_id + WHERE uwp.user_id = $1 + AND uwp.profile_id = $2 + AND uwp.completed = TRUE + AND mi.type = 'audiobook' + AND s.series_index IS NOT NULL + GROUP BY LOWER(BTRIM(s.series_name)) + ) + SELECT + next_book.content_id, + fs.series_name, + next_book.series_index, + fs.last_finished_at + FROM finished_series fs + JOIN LATERAL ( + SELECT m.content_id, s2.series_index + FROM audiobook_series s2 + JOIN media_items m ON m.content_id = s2.content_id + WHERE LOWER(BTRIM(s2.series_name)) = fs.series_key + AND s2.series_index IS NOT NULL + AND s2.series_index > fs.max_finished_index + AND m.type = 'audiobook' + AND EXISTS ( + SELECT 1 FROM media_files mf + WHERE mf.content_id = m.content_id AND mf.missing_since IS NULL + ) + AND NOT EXISTS ( + SELECT 1 FROM user_watch_progress uwp2 + WHERE uwp2.user_id = $1 + AND uwp2.profile_id = $2 + AND uwp2.media_item_id = m.content_id + )%s + ORDER BY s2.series_index, LOWER(m.sort_title) + LIMIT 1 + ) next_book ON true + ORDER BY fs.last_finished_at DESC + LIMIT $%d`, candidateScope, argIdx) + args = append(args, limit) + + rows, err := r.pool.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("querying next-in-series audiobooks: %w", err) + } + defer rows.Close() + + var results []NextInSeriesResult + for rows.Next() { + var res NextInSeriesResult + if err := rows.Scan(&res.ContentID, &res.SeriesName, &res.SeriesIndex, &res.LastFinishedAt); err != nil { + return nil, fmt.Errorf("scanning next-in-series row: %w", err) + } + results = append(results, res) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterating next-in-series rows: %w", err) + } + return results, nil +} diff --git a/internal/catalog/browse.go b/internal/catalog/browse.go index 2f0bc859..41cc3b41 100644 --- a/internal/catalog/browse.go +++ b/internal/catalog/browse.go @@ -36,6 +36,7 @@ type BrowseFilters struct { MaxLimit int // optional caller-specific cap; zero keeps the default browse cap Offset int SnapshotAt *time.Time // pagination fence: exclude items created after this timestamp + RequireBackdrop bool // only return items with a non-empty backdrop_path (Jellyfin ImageTypes=Backdrop filter) } // BrowseResult contains the paginated result of a browse query. @@ -295,6 +296,13 @@ func (r *BrowseRepository) buildBrowsePlan(filters BrowseFilters) (browseQueryPl argIdx++ } + // Backdrop presence filter (Jellyfin ImageTypes=Backdrop). Pushed down so + // random/limited selections only ever pick items that actually have a + // backdrop — filtering after the LIMIT would wrongly return empty pages. + if filters.RequireBackdrop { + conditions = append(conditions, "NULLIF(BTRIM(mi.backdrop_path), '') IS NOT NULL") + } + // Library access control: restrict to user's accessible libraries. needsLibJoin := filters.LibraryID > 0 || filters.LibraryIDs != nil || len(filters.DisabledLibraryIDs) > 0 || filters.Sort == "recently_added" needsPersonJoin := filters.PersonID > 0 @@ -759,9 +767,9 @@ func listDistinctPeopleByKindWithSource( } // listDistinctAudiobookSeriesWithSource returns distinct series_name values -// from audiobook_series joined onto the scoped result set. Names are trimmed +// from the active book series table joined onto the scoped result set. Names are trimmed // and case-folded for sort so the picker doesn't show duplicates that differ -// only by whitespace or casing — the literal trimmed series_name is still +// only by whitespace or casing; the literal trimmed series_name is still // returned so existing rules continue to match. // // The DISTINCT happens in an inline subquery (rather than directly on the @@ -783,14 +791,14 @@ func listDistinctAudiobookSeriesWithSource( SELECT name FROM ( SELECT DISTINCT BTRIM(s.series_name) AS name FROM %s - JOIN audiobook_series s ON s.content_id = mi.content_id + JOIN %s s ON s.content_id = mi.content_id %s AND s.series_name IS NOT NULL AND BTRIM(s.series_name) <> '' ) names ORDER BY LOWER(name) ASC LIMIT %d - `, fromClause, browseFilterPrefix(whereClause), catalogFacetMaxValues) + `, fromClause, bookSeriesTableForMediaScope(mediaScope), browseFilterPrefix(whereClause), catalogFacetMaxValues) return queryDistinctStrings(ctx, pool, query, args) } @@ -946,7 +954,7 @@ func searchDistinctAudiobookSeriesWithSource( SELECT name FROM ( SELECT DISTINCT BTRIM(s.series_name) AS name FROM %s - JOIN audiobook_series s ON s.content_id = mi.content_id + JOIN %s s ON s.content_id = mi.content_id %s AND s.series_name IS NOT NULL AND BTRIM(s.series_name) <> '' @@ -954,10 +962,17 @@ func searchDistinctAudiobookSeriesWithSource( ) names ORDER BY LOWER(name) ASC LIMIT %d - `, fromClause, browseFilterPrefix(whereClause), prefixIdx, limit+1) + `, fromClause, bookSeriesTableForMediaScope(mediaScope), browseFilterPrefix(whereClause), prefixIdx, limit+1) return queryFacetSearchResults(ctx, pool, query, args, limit) } +func bookSeriesTableForMediaScope(mediaScope string) string { + if mediaScope == "ebook" { + return "ebook_series" + } + return "audiobook_series" +} + // queryFacetSearchResults executes a facet search query that was built // with LIMIT N+1 and returns the first N rows plus a hasMore flag // indicating whether an additional row was present (i.e. the result set diff --git a/internal/catalog/browse_backdrop_test.go b/internal/catalog/browse_backdrop_test.go new file mode 100644 index 00000000..b5bac9d6 --- /dev/null +++ b/internal/catalog/browse_backdrop_test.go @@ -0,0 +1,31 @@ +package catalog + +import ( + "strings" + "testing" +) + +// TestBuildBrowsePlan_RequireBackdrop asserts the ImageTypes=Backdrop filter +// (BrowseFilters.RequireBackdrop) renders the backdrop-presence predicate into +// the WHERE clause, and is absent otherwise. Guards against a future refactor +// of buildBrowsePlan silently dropping the condition. +func TestBuildBrowsePlan_RequireBackdrop(t *testing.T) { + const predicate = "NULLIF(BTRIM(mi.backdrop_path), '') IS NOT NULL" + repo := &BrowseRepository{} + + plan, earlyEmpty, err := repo.buildBrowsePlan(BrowseFilters{Type: "movie", RequireBackdrop: true}) + if err != nil || earlyEmpty { + t.Fatalf("buildBrowsePlan(RequireBackdrop) err=%v earlyEmpty=%v", err, earlyEmpty) + } + if !strings.Contains(plan.whereClause, predicate) { + t.Fatalf("RequireBackdrop=true: whereClause missing predicate.\ngot: %s", plan.whereClause) + } + + plan, _, err = repo.buildBrowsePlan(BrowseFilters{Type: "movie"}) + if err != nil { + t.Fatalf("buildBrowsePlan err=%v", err) + } + if strings.Contains(plan.whereClause, predicate) { + t.Fatalf("RequireBackdrop unset: predicate should be absent.\ngot: %s", plan.whereClause) + } +} diff --git a/internal/catalog/catalog_parser.go b/internal/catalog/catalog_parser.go index 1c5d97b2..bddd0905 100644 --- a/internal/catalog/catalog_parser.go +++ b/internal/catalog/catalog_parser.go @@ -389,12 +389,11 @@ func normalizeCatalogMatch(raw string) string { } func parseCatalogMediaScope(raw string) string { - switch strings.ToLower(strings.TrimSpace(raw)) { - case "movie", "series", "episode", "audiobook": - return strings.ToLower(strings.TrimSpace(raw)) - default: - return "" + scope := strings.ToLower(strings.TrimSpace(raw)) + if scope != "" && IsValidMediaScope(scope) { + return scope } + return "" } func defaultCatalogQuerySort(searchQuery string) QuerySort { diff --git a/internal/catalog/catalog_parser_test.go b/internal/catalog/catalog_parser_test.go new file mode 100644 index 00000000..8f865ea0 --- /dev/null +++ b/internal/catalog/catalog_parser_test.go @@ -0,0 +1,9 @@ +package catalog + +import "testing" + +func TestParseCatalogMediaScope_AllowsEbook(t *testing.T) { + if got := parseCatalogMediaScope(" ebook "); got != "ebook" { + t.Fatalf("expected ebook media scope, got %q", got) + } +} diff --git a/internal/catalog/catalog_resolver.go b/internal/catalog/catalog_resolver.go index b949bdca..fa26f325 100644 --- a/internal/catalog/catalog_resolver.go +++ b/internal/catalog/catalog_resolver.go @@ -39,10 +39,9 @@ type CatalogFiltersResult struct { Resolutions []string AudioLanguages []string SubtitleLanguages []string - // Authors, Narrators and Series are audiobook-native facets. Populated - // whenever the scope contains audiobook items; empty for video-only - // scopes. They share the item_people / audiobook_series tables so - // callers don't need to switch on media type. + // Authors, Narrators and Series are book-native facets. Authors and + // Narrators use item_people; Series uses the active book series table. + // Video-only scopes return these empty. Authors []string Narrators []string Series []string @@ -64,8 +63,8 @@ type facetFetcher interface { // PeopleByKind returns distinct people names for the given PersonKind // across the scoped result set. Used for Authors / Narrators facets. PeopleByKind(ctx context.Context, kind models.PersonKind, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) - // AudiobookSeries returns distinct series_name values from - // audiobook_series joined onto the scoped result set. + // AudiobookSeries returns distinct series_name values from the active + // book series table joined onto the scoped result set. AudiobookSeries(ctx context.Context, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) // SearchDistinctArrayColumn prefix-searches an array-column facet // (genres, studios, networks, countries). Returns up to limit @@ -710,7 +709,7 @@ func (r *CatalogResolver) ListFiltersWithOptions(ctx context.Context, req Catalo return r.listFiltersForSource(ctx, filters, options, episodeCatalogBaseRelation, req.Query.MediaScope) } - return r.listFiltersForSource(ctx, filters, options, "media_items mi", "") + return r.listFiltersForSource(ctx, filters, options, "media_items mi", req.Query.MediaScope) } // CatalogFacetSearchResult is the typed return for SearchFacet. Matches @@ -798,10 +797,9 @@ func (r *CatalogResolver) SearchFacet(ctx context.Context, req CatalogRequest, a } baseRelation := "media_items mi" - mediaScope := "" + mediaScope := req.Query.MediaScope if isEpisodeCatalogScope(req.Query.MediaScope) { baseRelation = episodeCatalogBaseRelation - mediaScope = req.Query.MediaScope } facets := r.facets @@ -837,6 +835,9 @@ func dispatchFacetSearch(ctx context.Context, facets facetFetcher, facet string, case "author": return facets.SearchPeopleByKind(ctx, models.PersonKindAuthor, filters, baseRelation, mediaScope, prefix, limit) case "narrator": + if mediaScope == "ebook" { + return nil, false, fmt.Errorf("%w: narrator facet is not available for ebook scope", ErrInvalidCatalogRequest) + } return facets.SearchPeopleByKind(ctx, models.PersonKindNarrator, filters, baseRelation, mediaScope, prefix, limit) case "series": return facets.SearchAudiobookSeries(ctx, filters, baseRelation, mediaScope, prefix, limit) @@ -958,14 +959,16 @@ func (r *CatalogResolver) listFiltersForSource( authors = out return nil })) - eg.Go(withLimit(func() error { - out, err := facets.PeopleByKind(gctx, models.PersonKindNarrator, filters, baseRelation, mediaScope) - if err != nil { - return fmt.Errorf("listing catalog narrators: %w", err) - } - narrators = out - return nil - })) + if mediaScope != "ebook" { + eg.Go(withLimit(func() error { + out, err := facets.PeopleByKind(gctx, models.PersonKindNarrator, filters, baseRelation, mediaScope) + if err != nil { + return fmt.Errorf("listing catalog narrators: %w", err) + } + narrators = out + return nil + })) + } eg.Go(withLimit(func() error { out, err := facets.AudiobookSeries(gctx, filters, baseRelation, mediaScope) if err != nil { @@ -1086,12 +1089,8 @@ func validateCatalogExactCollectionRequest(req CatalogRequest) error { } func validateCatalogOverlayQuery(searchQuery string, def QueryDefinition, ruleFields, sortFields map[string]bool, allowRelevance bool) error { - if def.MediaScope != "" && - def.MediaScope != "movie" && - def.MediaScope != "series" && - def.MediaScope != "episode" && - def.MediaScope != "audiobook" { - return fmt.Errorf("%w: media_scope must be 'movie', 'series', 'episode', or 'audiobook'", ErrInvalidCatalogRequest) + if !IsValidMediaScope(def.MediaScope) { + return fmt.Errorf("%w: media_scope must be 'movie', 'series', 'episode', 'audiobook', 'ebook', or 'video'", ErrInvalidCatalogRequest) } if def.Match != "" && def.Match != "all" && def.Match != "any" { return fmt.Errorf("%w: match must be 'all' or 'any'", ErrInvalidCatalogRequest) @@ -1607,12 +1606,7 @@ func catalogSearchAccess(req CatalogRequest, access AccessFilter) (AccessFilter, MaxContentRating: access.MaxContentRating, } - var itemTypes []string - if req.Query.MediaScope != "" { - itemTypes = []string{req.Query.MediaScope} - } - - return searchAccess, itemTypes, false + return searchAccess, MediaScopeItemTypes(req.Query.MediaScope), false } func catalogBrowseFilters(req CatalogRequest, access AccessFilter) (BrowseFilters, bool, error) { @@ -1622,7 +1616,9 @@ func catalogBrowseFilters(req CatalogRequest, access AccessFilter) (BrowseFilter } filters := BrowseFilters{ - Type: req.Query.MediaScope, + // BrowseFilters.Type accepts a comma-separated type list, so group + // scopes like "video" expand here rather than leaking downstream. + Type: strings.Join(MediaScopeItemTypes(req.Query.MediaScope), ","), NamePrefix: req.NamePrefix, DisabledLibraryIDs: effectiveCatalogDisabledLibraryIDs(req.Query.LibraryIDs, access.DisabledLibraryIDs), MaxContentRating: access.MaxContentRating, @@ -1763,7 +1759,7 @@ func filterCatalogItems(items []*models.MediaItem, def QueryDefinition) []*model if item == nil { continue } - if def.MediaScope != "" && !strings.EqualFold(strings.TrimSpace(item.Type), def.MediaScope) { + if !MediaScopeMatchesItemType(def.MediaScope, item.Type) { continue } if catalogDefinitionMatchesItem(item, def) { diff --git a/internal/catalog/catalog_resolver_test.go b/internal/catalog/catalog_resolver_test.go index 6cd46054..ef4f89a2 100644 --- a/internal/catalog/catalog_resolver_test.go +++ b/internal/catalog/catalog_resolver_test.go @@ -39,6 +39,21 @@ func TestValidateCatalogQueryRequest_AllowsEpisodeMediaScope(t *testing.T) { } } +func TestValidateCatalogQueryRequest_AllowsEbookMediaScope(t *testing.T) { + req := CatalogRequest{ + Source: CatalogSourceQuery, + Query: QueryDefinition{ + MediaScope: "ebook", + Match: "all", + Sort: QuerySort{Field: "title", Order: "asc"}, + }, + } + + if err := validateCatalogQueryRequest(req, true); err != nil { + t.Fatalf("expected ebook media scope to be accepted, got %v", err) + } +} + func TestValidateCatalogQueryRequest_AllowsAddedAtFilter(t *testing.T) { req := CatalogRequest{ Source: CatalogSourceQuery, @@ -244,6 +259,67 @@ func (s *stubFacetFetcher) SearchAudiobookSeries(ctx context.Context, filters Br return nil, false, nil } +type recordingFacetFetcher struct { + mu sync.Mutex + peopleKinds []models.PersonKind + searchPeopleKind models.PersonKind + searchMediaScope string +} + +func (s *recordingFacetFetcher) DistinctArrayColumn(ctx context.Context, column string, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + return nil, nil +} + +func (s *recordingFacetFetcher) DistinctScalarColumn(ctx context.Context, column string, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + return nil, nil +} + +func (s *recordingFacetFetcher) Resolutions(ctx context.Context, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + return nil, nil +} + +func (s *recordingFacetFetcher) JSONBLanguages(ctx context.Context, column string, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + return nil, nil +} + +func (s *recordingFacetFetcher) SubtitleLanguages(ctx context.Context, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + return nil, nil +} + +func (s *recordingFacetFetcher) PeopleByKind(ctx context.Context, kind models.PersonKind, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + s.mu.Lock() + s.peopleKinds = append(s.peopleKinds, kind) + s.mu.Unlock() + if kind == models.PersonKindAuthor { + return []string{"Author"}, nil + } + return []string{"Narrator"}, nil +} + +func (s *recordingFacetFetcher) AudiobookSeries(ctx context.Context, filters BrowseFilters, baseRelation string, mediaScope string) ([]string, error) { + return nil, nil +} + +func (s *recordingFacetFetcher) SearchDistinctArrayColumn(ctx context.Context, column string, filters BrowseFilters, baseRelation string, mediaScope string, prefix string, limit int) ([]string, bool, error) { + return nil, false, nil +} + +func (s *recordingFacetFetcher) SearchDistinctScalarColumn(ctx context.Context, column string, filters BrowseFilters, baseRelation string, mediaScope string, prefix string, limit int) ([]string, bool, error) { + return nil, false, nil +} + +func (s *recordingFacetFetcher) SearchPeopleByKind(ctx context.Context, kind models.PersonKind, filters BrowseFilters, baseRelation string, mediaScope string, prefix string, limit int) ([]string, bool, error) { + s.mu.Lock() + s.searchPeopleKind = kind + s.searchMediaScope = mediaScope + s.mu.Unlock() + return []string{"Author"}, false, nil +} + +func (s *recordingFacetFetcher) SearchAudiobookSeries(ctx context.Context, filters BrowseFilters, baseRelation string, mediaScope string, prefix string, limit int) ([]string, bool, error) { + return nil, false, nil +} + // countingExecutor is a previewExecutor stub that records the number of // PreviewPage calls and the AccessFilter passed in (so tests can confirm // NamePrefix made it through). It returns an empty result set so the resolver @@ -327,3 +403,92 @@ func TestListFiltersWithOptions_RunsFacetQueriesConcurrently(t *testing.T) { barrier.assertConcurrent(t) } + +func TestListFiltersWithOptions_EbookScopeSkipsNarratorFacet(t *testing.T) { + facets := &recordingFacetFetcher{} + resolver := &CatalogResolver{ + browseRepo: &BrowseRepository{}, + facets: facets, + } + + result, err := resolver.ListFiltersWithOptions( + context.Background(), + CatalogRequest{ + Source: CatalogSourceQuery, + Query: QueryDefinition{MediaScope: "ebook"}, + }, + AccessFilter{}, + CatalogFilterOptions{}, + ) + if err != nil { + t.Fatalf("ListFiltersWithOptions returned error: %v", err) + } + + facets.mu.Lock() + defer facets.mu.Unlock() + for _, kind := range facets.peopleKinds { + if kind == models.PersonKindNarrator { + t.Fatal("ebook filters should not request narrator facets") + } + } + if len(result.Narrators) != 0 { + t.Fatalf("ebook filters returned narrators = %v, want empty", result.Narrators) + } +} + +func TestSearchFacet_EbookScopeForwardsMediaScope(t *testing.T) { + facets := &recordingFacetFetcher{} + resolver := &CatalogResolver{ + browseRepo: &BrowseRepository{}, + facets: facets, + } + + result, err := resolver.SearchFacet( + context.Background(), + CatalogRequest{ + Source: CatalogSourceQuery, + Query: QueryDefinition{MediaScope: "ebook"}, + }, + AccessFilter{}, + "author", + "Au", + 20, + ) + if err != nil { + t.Fatalf("SearchFacet returned error: %v", err) + } + if len(result.Matches) != 1 || result.Matches[0] != "Author" { + t.Fatalf("matches = %v, want Author", result.Matches) + } + + facets.mu.Lock() + defer facets.mu.Unlock() + if facets.searchPeopleKind != models.PersonKindAuthor { + t.Fatalf("searchPeopleKind = %v, want author", facets.searchPeopleKind) + } + if facets.searchMediaScope != "ebook" { + t.Fatalf("search mediaScope = %q, want ebook", facets.searchMediaScope) + } +} + +func TestSearchFacet_EbookScopeRejectsNarratorFacet(t *testing.T) { + resolver := &CatalogResolver{ + browseRepo: &BrowseRepository{}, + facets: &recordingFacetFetcher{}, + } + + _, err := resolver.SearchFacet( + context.Background(), + CatalogRequest{ + Source: CatalogSourceQuery, + Query: QueryDefinition{MediaScope: "ebook"}, + }, + AccessFilter{}, + "narrator", + "Na", + 20, + ) + if err == nil { + t.Fatal("expected narrator facet to be rejected for ebook scope") + } +} diff --git a/internal/catalog/continue_watching_progress.go b/internal/catalog/continue_watching_progress.go new file mode 100644 index 00000000..a6c162dd --- /dev/null +++ b/internal/catalog/continue_watching_progress.go @@ -0,0 +1,212 @@ +package catalog + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/Silo-Server/silo-server/internal/userstore" +) + +// ProgressLister pages watch-progress rows for one profile. +// userstore.UserStore satisfies it. +type ProgressLister interface { + ListProgress(ctx context.Context, profileID, status string, limit, offset int) ([]userstore.WatchProgress, error) +} + +// ProgressSnapshot pairs a media item with the time its progress row last changed. +type ProgressSnapshot struct { + ContentID string + UpdatedAt time.Time +} + +// ContinueWatchingProgressFilter identifies in-progress entries that Continue +// Watching surfaces should hide: episodes superseded by a later-completed +// episode in the same series. The first-party sections fetcher and the +// jellycompat Resume endpoint share it so both surfaces agree on what "still +// watching" means. +type ContinueWatchingProgressFilter struct { + pool *pgxpool.Pool +} + +// NewContinueWatchingProgressFilter creates a filter. A nil pool disables the +// superseded-episode check, leaving entries unfiltered. +func NewContinueWatchingProgressFilter(pool *pgxpool.Pool) *ContinueWatchingProgressFilter { + return &ContinueWatchingProgressFilter{pool: pool} +} + +const supersededProgressPageSize = 500 + +// SupersededEpisodeProgressIDs returns the content IDs of in-progress entries +// whose series has a later episode completed more recently than the entry's +// own progress. Those entries are stale — the viewer already moved past them. +// Non-episode entries never match. +func (f *ContinueWatchingProgressFilter) SupersededEpisodeProgressIDs(ctx context.Context, store ProgressLister, profileID string, entries []userstore.WatchProgress) (map[string]struct{}, error) { + if f == nil || f.pool == nil { + return map[string]struct{}{}, nil + } + inProgress := ProgressSnapshots(entries) + if len(inProgress) == 0 { + return map[string]struct{}{}, nil + } + + completed, err := CompletedProgressSnapshots(ctx, store, profileID) + if err != nil { + return nil, err + } + if len(completed) == 0 { + return map[string]struct{}{}, nil + } + + inProgressIDs, inProgressUpdatedAts := splitProgressSnapshots(inProgress) + completedIDs, completedUpdatedAts := splitProgressSnapshots(completed) + query := buildSupersededEpisodeProgressQuery() + rows, err := f.pool.Query(ctx, query, inProgressIDs, inProgressUpdatedAts, completedIDs, completedUpdatedAts) + if err != nil { + return nil, fmt.Errorf("querying superseded episode progress: %w", err) + } + defer rows.Close() + + superseded := make(map[string]struct{}) + for rows.Next() { + var mediaItemID string + if err := rows.Scan(&mediaItemID); err != nil { + return nil, fmt.Errorf("scanning superseded episode progress: %w", err) + } + superseded[mediaItemID] = struct{}{} + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterating superseded episode progress: %w", err) + } + return superseded, nil +} + +// CompletedProgressSnapshots pages through all completed progress rows for the +// profile and returns deduplicated snapshots. +func CompletedProgressSnapshots(ctx context.Context, store ProgressLister, profileID string) ([]ProgressSnapshot, error) { + seen := make(map[string]struct{}) + snapshots := make([]ProgressSnapshot, 0) + + for offset := 0; ; offset += supersededProgressPageSize { + entries, err := store.ListProgress(ctx, profileID, "completed", supersededProgressPageSize, offset) + if err != nil { + return nil, fmt.Errorf("listing completed progress for superseded episodes: %w", err) + } + + for _, snapshot := range ProgressSnapshots(entries) { + contentID := snapshot.ContentID + if _, ok := seen[contentID]; ok { + continue + } + seen[contentID] = struct{}{} + snapshots = append(snapshots, snapshot) + } + + if len(entries) < supersededProgressPageSize { + return snapshots, nil + } + } +} + +// ProgressSnapshots converts progress rows to snapshots, dropping rows with a +// blank media item ID or an unparseable timestamp. +func ProgressSnapshots(entries []userstore.WatchProgress) []ProgressSnapshot { + snapshots := make([]ProgressSnapshot, 0, len(entries)) + for _, entry := range entries { + contentID := strings.TrimSpace(entry.MediaItemID) + if contentID == "" { + continue + } + updatedAt, err := time.Parse(time.RFC3339, entry.UpdatedAt) + if err != nil || updatedAt.IsZero() { + continue + } + snapshots = append(snapshots, ProgressSnapshot{ + ContentID: contentID, + UpdatedAt: updatedAt.UTC(), + }) + } + return snapshots +} + +func splitProgressSnapshots(snapshots []ProgressSnapshot) ([]string, []time.Time) { + contentIDs := make([]string, len(snapshots)) + updatedAts := make([]time.Time, len(snapshots)) + for i, snapshot := range snapshots { + contentIDs[i] = snapshot.ContentID + updatedAts[i] = snapshot.UpdatedAt + } + return contentIDs, updatedAts +} + +// The snapshots arrive as unnest arrays instead of joins against +// user_watch_progress because per-user progress may live in a SQLite store +// rather than this Postgres database. +func buildSupersededEpisodeProgressQuery() string { + return ` + WITH in_progress(content_id, updated_at) AS ( + SELECT * FROM unnest($1::text[], $2::timestamptz[]) + ), + completed(content_id, updated_at) AS ( + SELECT * FROM unnest($3::text[], $4::timestamptz[]) + ) + SELECT DISTINCT ip.content_id + FROM in_progress ip_progress + JOIN episodes ip ON ip.content_id = ip_progress.content_id + JOIN episodes done + ON done.series_id = ip.series_id + AND (done.season_number, done.episode_number) > (ip.season_number, ip.episode_number) + JOIN completed done_progress + ON done_progress.content_id = done.content_id + WHERE done_progress.updated_at > ip_progress.updated_at` +} + +// FilterSupersededProgress drops entries whose media item ID is in the +// superseded set. +func FilterSupersededProgress(entries []userstore.WatchProgress, superseded map[string]struct{}) []userstore.WatchProgress { + if len(entries) == 0 || len(superseded) == 0 { + return entries + } + + filtered := make([]userstore.WatchProgress, 0, len(entries)) + for _, entry := range entries { + if _, ok := superseded[entry.MediaItemID]; ok { + continue + } + filtered = append(filtered, entry) + } + return filtered +} + +// HomeDismissalIndex maps media item ID to its dismissal row for one home surface. +type HomeDismissalIndex map[string]userstore.HomeItemDismissal + +// NewHomeDismissalIndex builds an index from dismissal rows. +func NewHomeDismissalIndex(dismissals []userstore.HomeItemDismissal) HomeDismissalIndex { + index := make(HomeDismissalIndex, len(dismissals)) + for _, dismissal := range dismissals { + index[dismissal.MediaItemID] = dismissal + } + return index +} + +// FilterProgress drops entries still covered by a dismissal. A dismissal only +// holds while the entry's progress timestamp matches the one captured when the +// user dismissed it; resuming playback re-surfaces the item. +func (idx HomeDismissalIndex) FilterProgress(entries []userstore.WatchProgress) []userstore.WatchProgress { + if len(entries) == 0 || len(idx) == 0 { + return entries + } + + filtered := make([]userstore.WatchProgress, 0, len(entries)) + for _, entry := range entries { + dismissal, ok := idx[entry.MediaItemID] + if !ok || dismissal.ProgressUpdatedAt == nil || *dismissal.ProgressUpdatedAt != entry.UpdatedAt { + filtered = append(filtered, entry) + } + } + return filtered +} diff --git a/internal/catalog/continue_watching_progress_test.go b/internal/catalog/continue_watching_progress_test.go new file mode 100644 index 00000000..74789a75 --- /dev/null +++ b/internal/catalog/continue_watching_progress_test.go @@ -0,0 +1,189 @@ +package catalog + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/userstore" +) + +func TestFilterSupersededProgressDropsOlderPartialsAfterLaterCompletedEpisode(t *testing.T) { + t.Parallel() + + entries := []userstore.WatchProgress{ + {MediaItemID: "boys-s1e1"}, + {MediaItemID: "boys-s5e3"}, + {MediaItemID: "movie-1"}, + } + superseded := map[string]struct{}{ + "boys-s1e1": {}, + "boys-s5e3": {}, + } + + filtered := FilterSupersededProgress(entries, superseded) + + if len(filtered) != 1 || filtered[0].MediaItemID != "movie-1" { + t.Fatalf("filtered entries = %+v, want only movie-1", filtered) + } +} + +func TestCompletedProgressSnapshotsPagesThroughConfiguredStore(t *testing.T) { + t.Parallel() + + entries := make([]userstore.WatchProgress, supersededProgressPageSize+1) + for i := range entries { + entries[i] = userstore.WatchProgress{ + MediaItemID: "done-" + time.Unix(int64(i), 0).Format("150405"), + UpdatedAt: time.Date(2025, 1, 1, 0, 0, i, 0, time.UTC).Format(time.RFC3339), + } + } + store := &stubProgressLister{entries: entries} + + snapshots, err := CompletedProgressSnapshots(context.Background(), store, "p1") + if err != nil { + t.Fatalf("CompletedProgressSnapshots: %v", err) + } + if len(snapshots) != len(entries) { + t.Fatalf("completed snapshots count = %d, want %d", len(snapshots), len(entries)) + } + if len(store.calls) != 2 { + t.Fatalf("ListProgress calls = %+v, want 2 paged calls", store.calls) + } + if store.calls[0] != (progressListCall{profileID: "p1", status: "completed", limit: supersededProgressPageSize, offset: 0}) { + t.Fatalf("first ListProgress call = %+v", store.calls[0]) + } + if store.calls[1] != (progressListCall{profileID: "p1", status: "completed", limit: supersededProgressPageSize, offset: supersededProgressPageSize}) { + t.Fatalf("second ListProgress call = %+v", store.calls[1]) + } +} + +func TestBuildSupersededEpisodeProgressQueryUsesStoreSnapshotsWithFreshnessGate(t *testing.T) { + t.Parallel() + + query := buildSupersededEpisodeProgressQuery() + expectedFragments := []string{ + "unnest($1::text[], $2::timestamptz[])", + "unnest($3::text[], $4::timestamptz[])", + "FROM in_progress ip_progress", + "done_progress.updated_at > ip_progress.updated_at", + } + for _, fragment := range expectedFragments { + if !strings.Contains(query, fragment) { + t.Fatalf("expected superseded progress query to contain %q, got:\n%s", fragment, query) + } + } + unexpectedFragments := []string{ + "user_watch_progress", + "user_history_hidden_items", + } + for _, fragment := range unexpectedFragments { + if strings.Contains(query, fragment) { + t.Fatalf("superseded progress query contains %q, got:\n%s", fragment, query) + } + } +} + +func TestSupersededEpisodeProgressIDsWithoutPoolReturnsEmptySet(t *testing.T) { + t.Parallel() + + filter := NewContinueWatchingProgressFilter(nil) + entries := []userstore.WatchProgress{{ + MediaItemID: "ep-1", + UpdatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339), + }} + store := &stubProgressLister{} + + superseded, err := filter.SupersededEpisodeProgressIDs(context.Background(), store, "p1", entries) + if err != nil { + t.Fatalf("SupersededEpisodeProgressIDs: %v", err) + } + if len(superseded) != 0 { + t.Fatalf("superseded = %v, want empty set", superseded) + } + if len(store.calls) != 0 { + t.Fatalf("ListProgress calls = %+v, want none without a pool", store.calls) + } +} + +func TestHomeDismissalIndexFilterProgressDropsOnlyMatchingTimestamps(t *testing.T) { + t.Parallel() + + dismissedAt := "2025-01-01T00:00:00Z" + resumedAt := "2025-01-02T00:00:00Z" + idx := NewHomeDismissalIndex([]userstore.HomeItemDismissal{ + {MediaItemID: "still-dismissed", ProgressUpdatedAt: &dismissedAt}, + {MediaItemID: "resumed-since", ProgressUpdatedAt: &dismissedAt}, + {MediaItemID: "no-timestamp"}, + }) + + entries := []userstore.WatchProgress{ + {MediaItemID: "still-dismissed", UpdatedAt: dismissedAt}, + {MediaItemID: "resumed-since", UpdatedAt: resumedAt}, + {MediaItemID: "no-timestamp", UpdatedAt: dismissedAt}, + {MediaItemID: "never-dismissed", UpdatedAt: dismissedAt}, + } + + filtered := idx.FilterProgress(entries) + + got := make([]string, 0, len(filtered)) + for _, entry := range filtered { + got = append(got, entry.MediaItemID) + } + want := []string{"resumed-since", "no-timestamp", "never-dismissed"} + if len(got) != len(want) { + t.Fatalf("filtered = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("filtered = %v, want %v", got, want) + } + } +} + +func TestProgressSnapshotsSkipsBlankIDsAndBadTimestamps(t *testing.T) { + t.Parallel() + + valid := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + entries := []userstore.WatchProgress{ + {MediaItemID: "ok", UpdatedAt: valid.Format(time.RFC3339)}, + {MediaItemID: " ", UpdatedAt: valid.Format(time.RFC3339)}, + {MediaItemID: "bad-time", UpdatedAt: "not-a-time"}, + } + + snapshots := ProgressSnapshots(entries) + + if len(snapshots) != 1 || snapshots[0].ContentID != "ok" || !snapshots[0].UpdatedAt.Equal(valid) { + t.Fatalf("snapshots = %+v, want single valid snapshot for %q", snapshots, "ok") + } +} + +type progressListCall struct { + profileID string + status string + limit int + offset int +} + +type stubProgressLister struct { + entries []userstore.WatchProgress + calls []progressListCall +} + +func (s *stubProgressLister) ListProgress(_ context.Context, profileID, status string, limit, offset int) ([]userstore.WatchProgress, error) { + s.calls = append(s.calls, progressListCall{ + profileID: profileID, + status: status, + limit: limit, + offset: offset, + }) + if offset >= len(s.entries) { + return nil, nil + } + end := offset + limit + if end > len(s.entries) { + end = len(s.entries) + } + return s.entries[offset:end], nil +} diff --git a/internal/catalog/detail.go b/internal/catalog/detail.go index 54a5862e..e46b016a 100644 --- a/internal/catalog/detail.go +++ b/internal/catalog/detail.go @@ -157,6 +157,9 @@ type ItemDetail struct { // Audiobook-specific detail. Present only when Type == "audiobook". Audiobook *AudiobookDetailExtension `json:"audiobook,omitempty"` + + // Ebook-specific detail. Present only when Type == "ebook". + Ebook *EbookDetailExtension `json:"ebook,omitempty"` } type AudiobookDetailExtension struct { @@ -201,6 +204,13 @@ type AudiobookNarration struct { Narrators []string `json:"narrators"` } +type EbookDetailExtension struct { + Authors []AudiobookPerson `json:"authors"` + Publisher string `json:"publisher,omitempty"` + Series *AudiobookSeriesGroup `json:"series,omitempty"` + Related AudiobookRelatedContent `json:"related"` +} + // ItemUserState is per-profile viewer state included in item detail responses. type ItemUserState struct { Played bool `json:"played"` @@ -895,6 +905,9 @@ func (s *DetailService) buildMediaItemDetail(ctx context.Context, item *models.M if item.Type == "audiobook" { detail.Audiobook = s.buildAudiobookExtension(ctx, item, detail.Versions, crewCredits, filter) } + if item.Type == "ebook" { + detail.Ebook = s.buildEbookExtension(ctx, item, crewCredits, filter) + } // Series folder paths from confirmed claims when available, otherwise from // the file links that currently belong to the item. This keeps provisional @@ -1036,6 +1049,26 @@ func (s *DetailService) buildAudiobookExtension( } } +func (s *DetailService) buildEbookExtension( + ctx context.Context, + item *models.MediaItem, + crew []CrewCredit, + filter AccessFilter, +) *EbookDetailExtension { + if item == nil { + return nil + } + return &EbookDetailExtension{ + Authors: audiobookPeopleFromCrew(crew, models.PersonKindAuthor.String()), + Publisher: firstNonEmptyString(item.Studios), + Series: s.fetchEbookSeries(ctx, item.ContentID, filter), + Related: AudiobookRelatedContent{ + AlsoByAuthor: s.fetchEbookAlsoByAuthor(ctx, item.ContentID, filter), + Similar: s.fetchEbookSimilarByGenres(ctx, item.ContentID, filter), + }, + } +} + func audiobookPeopleFromCrew(crew []CrewCredit, job string) []AudiobookPerson { out := make([]AudiobookPerson, 0) for _, credit := range crew { @@ -1061,6 +1094,10 @@ func firstNonEmptyString(values []string) string { return "" } +func (s *DetailService) presignAudiobookPosterURL(ctx context.Context, posterPath string) string { + return s.PresignImageURL(ctx, posterPath, "poster", "") +} + func appendAudiobookItemAccessConditions( alias string, filter AccessFilter, @@ -1103,15 +1140,23 @@ func appendAudiobookItemAccessConditions( } func (s *DetailService) fetchAudiobookAlsoByAuthor(ctx context.Context, contentID string, filter AccessFilter) []AudiobookRelatedItem { + return s.fetchBookAlsoByAuthor(ctx, contentID, "audiobook", filter) +} + +func (s *DetailService) fetchEbookAlsoByAuthor(ctx context.Context, contentID string, filter AccessFilter) []AudiobookRelatedItem { + return s.fetchBookAlsoByAuthor(ctx, contentID, "ebook", filter) +} + +func (s *DetailService) fetchBookAlsoByAuthor(ctx context.Context, contentID string, mediaType string, filter AccessFilter) []AudiobookRelatedItem { if s == nil || s.itemRepo == nil || s.itemRepo.pool == nil { return []AudiobookRelatedItem{} } - args := []any{contentID, models.PersonKindAuthor} - argIdx := 3 + args := []any{contentID, models.PersonKindAuthor, mediaType} + argIdx := 4 conditions := []string{ "ip1.content_id = $1", "ip1.kind = $2", - "m2.type = 'audiobook'", + "m2.type = $3", } if !appendAudiobookItemAccessConditions("m2", filter, &conditions, &args, &argIdx) { return []AudiobookRelatedItem{} @@ -1146,20 +1191,28 @@ func (s *DetailService) fetchAudiobookAlsoByAuthor(ctx context.Context, contentI continue } seen[item.ContentID] = struct{}{} - item.PosterURL = s.PresignURL(ctx, posterPath, "featured") + item.PosterURL = s.presignAudiobookPosterURL(ctx, posterPath) out = append(out, item) } return out } func (s *DetailService) fetchAudiobookSimilarByGenres(ctx context.Context, contentID string, filter AccessFilter) []AudiobookRelatedItem { + return s.fetchBookSimilarByGenres(ctx, contentID, "audiobook", filter) +} + +func (s *DetailService) fetchEbookSimilarByGenres(ctx context.Context, contentID string, filter AccessFilter) []AudiobookRelatedItem { + return s.fetchBookSimilarByGenres(ctx, contentID, "ebook", filter) +} + +func (s *DetailService) fetchBookSimilarByGenres(ctx context.Context, contentID string, mediaType string, filter AccessFilter) []AudiobookRelatedItem { if s == nil || s.itemRepo == nil || s.itemRepo.pool == nil { return []AudiobookRelatedItem{} } - args := []any{contentID, models.PersonKindAuthor} - argIdx := 3 + args := []any{contentID, models.PersonKindAuthor, mediaType} + argIdx := 4 conditions := []string{ - "m.type = 'audiobook'", + "m.type = $3", "m.content_id <> $1", "m.genres && (SELECT array_agg(g) FROM this_genres)", `NOT EXISTS ( @@ -1205,21 +1258,34 @@ func (s *DetailService) fetchAudiobookSimilarByGenres(ctx context.Context, conte if err := rows.Scan(&item.ContentID, &item.Title, &item.Year, &posterPath); err != nil { return []AudiobookRelatedItem{} } - item.PosterURL = s.PresignURL(ctx, posterPath, "featured") + item.PosterURL = s.presignAudiobookPosterURL(ctx, posterPath) out = append(out, item) } return out } func (s *DetailService) fetchAudiobookSeries(ctx context.Context, contentID string, filter AccessFilter) *AudiobookSeriesGroup { + return s.fetchBookSeries(ctx, contentID, "audiobook", "audiobook_series", filter) +} + +func (s *DetailService) fetchEbookSeries(ctx context.Context, contentID string, filter AccessFilter) *AudiobookSeriesGroup { + return s.fetchBookSeries(ctx, contentID, "ebook", "ebook_series", filter) +} + +func (s *DetailService) fetchBookSeries(ctx context.Context, contentID string, mediaType string, tableName string, filter AccessFilter) *AudiobookSeriesGroup { if s == nil || s.itemRepo == nil || s.itemRepo.pool == nil { return nil } - args := []any{contentID} - argIdx := 2 + switch tableName { + case "audiobook_series", "ebook_series": + default: + return nil + } + args := []any{contentID, mediaType} + argIdx := 3 conditions := []string{ "root.content_id = $1", - "m.type = 'audiobook'", + "m.type = $2", } if !appendAudiobookItemAccessConditions("m", filter, &conditions, &args, &argIdx) { return nil @@ -1232,13 +1298,13 @@ func (s *DetailService) fetchAudiobookSeries(ctx context.Context, contentID stri COALESCE(m.poster_path, ''), s.series_name, s.series_index - FROM audiobook_series root - JOIN audiobook_series s ON LOWER(s.series_name) = LOWER(root.series_name) + FROM %s root + JOIN %s s ON LOWER(s.series_name) = LOWER(root.series_name) JOIN media_items m ON m.content_id = s.content_id WHERE %s ORDER BY s.series_index NULLS LAST, LOWER(m.sort_title) LIMIT 30 - `, strings.Join(conditions, " AND ")) + `, tableName, tableName, strings.Join(conditions, " AND ")) rows, err := s.itemRepo.pool.Query(ctx, query, args...) if err != nil { return nil @@ -1266,7 +1332,7 @@ func (s *DetailService) fetchAudiobookSeries(ctx context.Context, contentID stri item.SeriesIndex = &n } } - item.PosterURL = s.PresignURL(ctx, poster, "featured") + item.PosterURL = s.presignAudiobookPosterURL(ctx, poster) entries = append(entries, item) } if len(entries) < 2 { diff --git a/internal/catalog/detail_audiobook_test.go b/internal/catalog/detail_audiobook_test.go index 8228c5f8..6b1b06e8 100644 --- a/internal/catalog/detail_audiobook_test.go +++ b/internal/catalog/detail_audiobook_test.go @@ -1,6 +1,8 @@ package catalog import ( + "context" + "strings" "testing" "github.com/Silo-Server/silo-server/internal/models" @@ -40,3 +42,61 @@ func TestSortAudiobookMediaFilesFallsBackToPath(t *testing.T) { } } } + +func TestPresignAudiobookPosterURLUsesPosterVariant(t *testing.T) { + resolver := &recordingCatalogImageResolver{} + detail := &DetailService{} + detail.SetImageResolver(resolver) + + got := detail.presignAudiobookPosterURL(context.Background(), "local/audiobooks/book/poster/original.webp") + + if !strings.Contains(got, "/w500.webp") { + t.Fatalf("resolved URL = %q, want w500 poster variant", got) + } + if resolver.variant != "featured" { + t.Fatalf("resolver variant = %q, want featured", resolver.variant) + } +} + +type recordingCatalogImageResolver struct { + path string + variant string +} + +func (r *recordingCatalogImageResolver) ResolveImageURL(_ context.Context, path string, variant string) string { + r.path = path + r.variant = variant + return "resolved://" + path +} + +func (r *recordingCatalogImageResolver) ResolveImageURLs(_ context.Context, paths []string, variant string) map[string]string { + out := make(map[string]string, len(paths)) + for _, path := range paths { + out[path] = r.ResolveImageURL(context.Background(), path, variant) + } + return out +} + +func TestBuildEbookExtensionUsesAuthorsAndPublisherWithoutNarrators(t *testing.T) { + crew := []CrewCredit{ + {Name: "Becky Chambers", Job: models.PersonKindAuthor.String(), PersonID: "10"}, + {Name: "Narrator Should Not Apply", Job: models.PersonKindNarrator.String(), PersonID: "20"}, + } + item := &models.MediaItem{ + ContentID: "ebook-1", + Type: "ebook", + Studios: []string{"Tor"}, + } + + extension := (&DetailService{}).buildEbookExtension(nil, item, crew, AccessFilter{}) + + if extension == nil { + t.Fatal("buildEbookExtension returned nil") + } + if len(extension.Authors) != 1 || extension.Authors[0].Name != "Becky Chambers" { + t.Fatalf("Authors = %+v, want Becky Chambers", extension.Authors) + } + if extension.Publisher != "Tor" { + t.Fatalf("Publisher = %q, want Tor", extension.Publisher) + } +} diff --git a/internal/catalog/episode_catalog_entries_executor.go b/internal/catalog/episode_catalog_entries_executor.go index b9a747c2..6eea36ba 100644 --- a/internal/catalog/episode_catalog_entries_executor.go +++ b/internal/catalog/episode_catalog_entries_executor.go @@ -563,7 +563,6 @@ func episodeCatalogUserStateCTE(plan episodeCatalogUserStatePlan) string { AND hhi.media_item_id = uwp.media_item_id WHERE uwp.user_id = $1 AND uwp.profile_id = $2 - AND uwp.completed = FALSE AND uwp.position_seconds > 0%s AND (hhi.media_item_id IS NULL OR uwp.updated_at > hhi.hidden_before) )`, progressRatioGate) diff --git a/internal/catalog/episode_repo.go b/internal/catalog/episode_repo.go index eceeaf33..88658ca8 100644 --- a/internal/catalog/episode_repo.go +++ b/internal/catalog/episode_repo.go @@ -638,6 +638,32 @@ func (r *EpisodeRepository) GetByIDs(ctx context.Context, contentIDs []string) ( return scanEpisodes(rows) } +// HasFilesByIDs reports which of the given episodes are backed by at least one +// live (non-missing) media file. Episodes absent from the result map have no +// file — e.g. provider-metadata-only entries for unaired episodes. +func (r *EpisodeRepository) HasFilesByIDs(ctx context.Context, contentIDs []string) (map[string]bool, error) { + result := make(map[string]bool, len(contentIDs)) + if len(contentIDs) == 0 { + return result, nil + } + query := `SELECT episode_id FROM media_files + WHERE episode_id = ANY($1) AND missing_since IS NULL + GROUP BY episode_id` + rows, err := r.pool.Query(ctx, query, contentIDs) + if err != nil { + return nil, fmt.Errorf("checking episode file presence: %w", err) + } + defer rows.Close() + for rows.Next() { + var episodeID string + if err := rows.Scan(&episodeID); err != nil { + return nil, fmt.Errorf("scanning episode file presence: %w", err) + } + result[episodeID] = true + } + return result, rows.Err() +} + // ListBySeries returns all episodes for a given series, ordered by season and // episode number. func (r *EpisodeRepository) ListBySeries(ctx context.Context, seriesID string) ([]*models.Episode, error) { diff --git a/internal/catalog/favorites_browse.go b/internal/catalog/favorites_browse.go index 0ba2b66b..b54b4ff2 100644 --- a/internal/catalog/favorites_browse.go +++ b/internal/catalog/favorites_browse.go @@ -24,8 +24,9 @@ type BrowseFavoritesFilters struct { AllowedLibraryIDs []int // nil = no allowlist, []int{} = empty result DisabledLibraryIDs []int // user-disabled libraries to exclude MaxContentRating string - SortField string // "added_at" (default), "title"/"sort_title", "year", "release_date" - SortOrder string // "asc" or "desc" (default desc) + ExcludedMediaTypes []string // media types the caller's surface never exposes + SortField string // "added_at" (default), "title"/"sort_title", "year", "release_date" + SortOrder string // "asc" or "desc" (default desc) Limit int Offset int } @@ -230,7 +231,7 @@ func buildBrowseFavoritesPlan(f BrowseFavoritesFilters) (browseFavoritesPlan, er argIdx++ } - applyAccessFilter("mi", AccessFilter{MaxContentRating: f.MaxContentRating}, &conditions, &args, &argIdx) + applyAccessFilter("mi", AccessFilter{MaxContentRating: f.MaxContentRating, ExcludedMediaTypes: f.ExcludedMediaTypes}, &conditions, &args, &argIdx) orderBy := buildBrowseFavoritesOrderBy(f.SortField, f.SortOrder) diff --git a/internal/catalog/item_repo.go b/internal/catalog/item_repo.go index 70889762..015d3be9 100644 --- a/internal/catalog/item_repo.go +++ b/internal/catalog/item_repo.go @@ -35,6 +35,69 @@ func NewItemRepository(pool *pgxpool.Pool) *ItemRepository { return &ItemRepository{pool: pool} } +// GetPoster returns the current poster path and thumbhash for a media item. +// Missing or NULL values are returned as empty strings. +func (r *ItemRepository) GetPoster(ctx context.Context, contentID string) (posterPath string, posterThumbhash string, err error) { + if r == nil || r.pool == nil { + return "", "", ErrItemNotFound + } + if err := r.pool.QueryRow(ctx, ` + SELECT COALESCE(poster_path, ''), COALESCE(poster_thumbhash, '') + FROM media_items + WHERE content_id = $1 + `, contentID).Scan(&posterPath, &posterThumbhash); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return "", "", ErrItemNotFound + } + return "", "", fmt.Errorf("getting media item poster: %w", err) + } + return posterPath, posterThumbhash, nil +} + +// SetLocalPoster records a locally extracted poster without ever overwriting +// provider or manually applied artwork: the row is updated only when the item +// has no poster yet or its current poster lives under localPrefix. The +// condition is evaluated atomically in SQL so concurrent metadata writers +// (e.g. the ebook enrichment sweep) cannot be clobbered between a read and a +// write. Returns true when a row was updated. +func (r *ItemRepository) SetLocalPoster(ctx context.Context, contentID, posterPath, thumbhash, localPrefix string) (bool, error) { + if r == nil || r.pool == nil { + return false, ErrItemNotFound + } + tag, err := r.pool.Exec(ctx, ` + UPDATE media_items + SET poster_path = $2, + poster_thumbhash = NULLIF($3, ''), + updated_at = NOW() + WHERE content_id = $1 + AND (poster_path IS NULL OR poster_path = '' OR poster_path LIKE $4 || '%') + `, contentID, posterPath, thumbhash, localPrefix) + if err != nil { + return false, fmt.Errorf("setting local poster: %w", err) + } + return tag.RowsAffected() > 0, nil +} + +// GetPosterPath returns the current poster path for a media item. Missing or +// NULL poster values are returned as an empty string. +func (r *ItemRepository) GetPosterPath(ctx context.Context, contentID string) (string, error) { + if r == nil || r.pool == nil { + return "", ErrItemNotFound + } + var posterPath string + if err := r.pool.QueryRow(ctx, ` + SELECT COALESCE(poster_path, '') + FROM media_items + WHERE content_id = $1 + `, contentID).Scan(&posterPath); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return "", ErrItemNotFound + } + return "", fmt.Errorf("getting media item poster path: %w", err) + } + return posterPath, nil +} + // overviewMatchFloor is the minimum ts_rank_cd score an overview-only match // must achieve to be returned when no title FTS match exists in the candidate // set. The overview tsvector is built without setweight(), so PostgreSQL's @@ -45,60 +108,87 @@ func NewItemRepository(pool *pgxpool.Pool) *ItemRepository { // incidental one-mention hits that flooded results before. const overviewMatchFloor = 0.15 +// itemColumnNames lists, in scan order, every column selected by media_items +// item queries. Shared by itemColumns, qualifiedItemColumns, +// qualifiedListItemColumns, and qualifiedItemColumnRefs so the select lists +// can never drift from each other or from scanItem. +var itemColumnNames = []string{ + "content_id", "type", "title", "sort_title", "default_metadata_language", "original_title", "year", "genres", + "content_rating", "runtime", "overview", "tagline", + "rating_imdb", "rating_tmdb", "rating_rt_critic", "rating_rt_audience", + "imdb_id", "tmdb_id", "tvdb_id", + "poster_path", "poster_thumbhash", "backdrop_path", "backdrop_thumbhash", "logo_path", + "metadata_s3_path", "metadata_etag", "season_count", + "studios", "networks", "countries", "keywords", "original_language", "release_date::text", "first_air_date", "last_air_date", "air_time", "air_timezone", + "show_status", + "matched_at", "last_refreshed", "refresh_failures", + "episode_metadata_incomplete", "episode_metadata_last_checked_at", "locked_fields", "status", "created_at", "updated_at", +} + +// nullableStringItemColumns are media_items columns that may hold NULL but +// scan into plain (non-pointer) string fields on models.MediaItem, so select +// lists coalesce them to ”. +var nullableStringItemColumns = map[string]bool{ + "poster_path": true, + "poster_thumbhash": true, + "backdrop_path": true, + "backdrop_thumbhash": true, + "logo_path": true, + "metadata_s3_path": true, + "metadata_etag": true, +} + +// itemColumnExpr renders one select-list entry for col, qualified with alias +// when non-empty. Nullable string columns are coalesced to ” and aliased +// back to their own name so queries that wrap the select list in a CTE or +// subquery can still reference the column by name. +func itemColumnExpr(alias, col string) string { + qualified := col + if alias != "" { + qualified = alias + "." + col + } + if nullableStringItemColumns[col] { + return fmt.Sprintf("COALESCE(%s, '') AS %s", qualified, col) + } + return qualified +} + +func joinItemColumns(alias string) string { + exprs := make([]string, len(itemColumnNames)) + for i, col := range itemColumnNames { + exprs[i] = itemColumnExpr(alias, col) + } + return strings.Join(exprs, ", ") +} + // itemColumns is the list of columns returned by all SELECT queries on media_items. -const itemColumns = `content_id, type, title, sort_title, default_metadata_language, original_title, year, genres, - content_rating, runtime, overview, tagline, - rating_imdb, rating_tmdb, rating_rt_critic, rating_rt_audience, - imdb_id, tmdb_id, tvdb_id, - poster_path, poster_thumbhash, backdrop_path, backdrop_thumbhash, logo_path, - metadata_s3_path, metadata_etag, season_count, - studios, networks, countries, keywords, original_language, release_date::text, first_air_date, last_air_date, air_time, air_timezone, - show_status, - matched_at, last_refreshed, refresh_failures, - episode_metadata_incomplete, episode_metadata_last_checked_at, locked_fields, status, created_at, updated_at` +var itemColumns = joinItemColumns("") func qualifiedItemColumns(alias string) string { - cols := []string{ - "content_id", "type", "title", "sort_title", "default_metadata_language", "original_title", "year", "genres", - "content_rating", "runtime", "overview", "tagline", - "rating_imdb", "rating_tmdb", "rating_rt_critic", "rating_rt_audience", - "imdb_id", "tmdb_id", "tvdb_id", - "poster_path", "poster_thumbhash", "backdrop_path", "backdrop_thumbhash", "logo_path", - "metadata_s3_path", "metadata_etag", "season_count", - "studios", "networks", "countries", "keywords", "original_language", "release_date::text", "first_air_date", "last_air_date", "air_time", "air_timezone", - "show_status", - "matched_at", "last_refreshed", "refresh_failures", - "episode_metadata_incomplete", "episode_metadata_last_checked_at", "locked_fields", "status", "created_at", "updated_at", + return joinItemColumns(alias) +} + +// qualifiedItemColumnRefs renders plain alias-qualified column references +// without COALESCE or AS aliases, for contexts like GROUP BY where output +// aliases are invalid. +func qualifiedItemColumnRefs(alias string) string { + refs := make([]string, len(itemColumnNames)) + for i, col := range itemColumnNames { + refs[i] = alias + "." + col } - prefixed := make([]string, len(cols)) - for i, col := range cols { - prefixed[i] = alias + "." + col - } - return strings.Join(prefixed, ", ") + return strings.Join(refs, ", ") } func qualifiedListItemColumns(alias string) string { - cols := []string{ - "content_id", "type", "title", "sort_title", "default_metadata_language", "original_title", "year", "genres", - "content_rating", "runtime", "overview", "tagline", - "rating_imdb", "rating_tmdb", "rating_rt_critic", "rating_rt_audience", - "imdb_id", "tmdb_id", "tvdb_id", - "poster_path", "poster_thumbhash", "backdrop_path", "backdrop_thumbhash", "logo_path", - "metadata_s3_path", "metadata_etag", "season_count", - "studios", "networks", "countries", "keywords", "original_language", "release_date::text", "first_air_date", "last_air_date", "air_time", "air_timezone", - "show_status", - "matched_at", "last_refreshed", "refresh_failures", - "episode_metadata_incomplete", "episode_metadata_last_checked_at", "locked_fields", "status", "created_at", "updated_at", - } - prefixed := make([]string, len(cols)) - for i, col := range cols { + exprs := make([]string, len(itemColumnNames)) + for i, col := range itemColumnNames { if col == "last_air_date" { - prefixed[i] = effectiveLastAirDateExpr(alias) + exprs[i] = effectiveLastAirDateExpr(alias) + " AS last_air_date" continue } - prefixed[i] = alias + "." + col + exprs[i] = itemColumnExpr(alias, col) } - return strings.Join(prefixed, ", ") + return strings.Join(exprs, ", ") } // scanItem scans a single row into a *models.MediaItem. @@ -573,9 +663,9 @@ func (r *ItemRepository) buildGetByIDsWithAccessSQL(contentIDs []string, access argIdx++ } - // Apply MaxContentRating like applyAccessFilter does. + // Apply MaxContentRating and type exclusions like applyAccessFilter does. var ratingConditions []string - applyAccessFilter("mi", AccessFilter{MaxContentRating: access.MaxContentRating}, &ratingConditions, &args, &argIdx) + applyAccessFilter("mi", AccessFilter{MaxContentRating: access.MaxContentRating, ExcludedMediaTypes: access.ExcludedMediaTypes}, &ratingConditions, &args, &argIdx) for _, c := range ratingConditions { sql += " AND " + c } @@ -900,7 +990,7 @@ func (r *ItemRepository) buildSearchSQL(query string, itemTypes []string, limit, if needsLibJoin { fromClause = "media_items mi JOIN media_item_libraries mil ON mi.content_id = mil.content_id" } - applyAccessFilter("mi", AccessFilter{MaxContentRating: filter.MaxContentRating}, &conditions, &args, &argIdx) + applyAccessFilter("mi", AccessFilter{MaxContentRating: filter.MaxContentRating, ExcludedMediaTypes: filter.ExcludedMediaTypes}, &conditions, &args, &argIdx) whereClause := "WHERE " + strings.Join(conditions, " AND ") @@ -956,8 +1046,12 @@ func (r *ItemRepository) buildSearchSQL(query string, itemTypes []string, limit, ) // Use qualified column names inside the CTE to avoid ambiguity when - // the FROM clause includes a JOIN to media_item_libraries. + // the FROM clause includes a JOIN to media_item_libraries. The select + // list aliases coalesced columns back to their own names (poster_path + // etc.) so the outer query can re-reference them; GROUP BY needs the + // raw references because output aliases are invalid there. qualifiedCols := qualifiedItemColumns("mi") + groupByCols := qualifiedItemColumnRefs("mi") scoredCTE := fmt.Sprintf(` WITH scored AS ( SELECT @@ -984,7 +1078,7 @@ func (r *ItemRepository) buildSearchSQL(query string, itemTypes []string, limit, %s GROUP BY %s ) - `, qualifiedCols, exactTitleMatch, contiguousTitleMatch, yearIdx, yearIdx, titleVector, titleQuery, overviewVector, phraseIdx, titleVector, phraseIdx, fromClause, whereClause, qualifiedCols) + `, qualifiedCols, exactTitleMatch, contiguousTitleMatch, yearIdx, yearIdx, titleVector, titleQuery, overviewVector, phraseIdx, titleVector, phraseIdx, fromClause, whereClause, groupByCols) // COUNT(*) OVER () runs after the GROUP BY in the scored CTE collapses // duplicates from the library JOIN, so the window count preserves the @@ -1110,7 +1204,7 @@ func (r *ItemRepository) EnsureAccessible(ctx context.Context, contentID string, fromClause = "media_items mi JOIN media_item_libraries mil ON mi.content_id = mil.content_id" } - applyAccessFilter("mi", AccessFilter{MaxContentRating: filter.MaxContentRating}, &conditions, &args, &argIdx) + applyAccessFilter("mi", AccessFilter{MaxContentRating: filter.MaxContentRating, ExcludedMediaTypes: filter.ExcludedMediaTypes}, &conditions, &args, &argIdx) query := fmt.Sprintf("SELECT 1 FROM %s WHERE %s LIMIT 1", fromClause, strings.Join(conditions, " AND ")) var found int diff --git a/internal/catalog/item_repo_test.go b/internal/catalog/item_repo_test.go index 064bb116..170b3bf6 100644 --- a/internal/catalog/item_repo_test.go +++ b/internal/catalog/item_repo_test.go @@ -2,6 +2,7 @@ package catalog import ( "fmt" + "sort" "strings" "testing" ) @@ -367,3 +368,59 @@ func TestItemRepo_Search_NormalizesTsqueryInput(t *testing.T) { t.Fatalf("overview arm should NOT wrap $1 in normalize_search_text; got:\n%s", sql) } } + +// TestItemRepo_Search_ScoredCTEExposesItemColumnNames guards the contract +// between the scored CTE and the outer SELECT in buildSearchSQL. The CTE +// projects qualifiedItemColumns("mi") and the outer query re-selects those +// columns by name via itemColumns, so every entry must expose its own column +// name as the output name. Postgres names an unaliased expression like +// COALESCE(mi.poster_path, ”) "coalesce", which breaks the outer reference +// with SQLSTATE 42703 (column "poster_path" does not exist) — exactly how +// search returned 500s when the nullable-string COALESCE wrappers first +// landed without AS aliases. +func TestItemRepo_Search_ScoredCTEExposesItemColumnNames(t *testing.T) { + exposed := map[string]bool{} + for _, part := range splitTopLevelSQLCommas(qualifiedItemColumns("mi")) { + exposed[sqlOutputColumnName(part)] = true + } + + var missing []string + for _, col := range itemColumnNames { + name := trailingSQLIdent(col) + if !exposed[name] { + missing = append(missing, name) + } + } + if len(missing) > 0 { + sort.Strings(missing) + t.Fatalf("qualifiedItemColumns(\"mi\") does not expose output columns %v; "+ + "alias each expression back to its column name (e.g. COALESCE(mi.x, '') AS x) "+ + "or the search CTE's outer SELECT fails with SQLSTATE 42703", missing) + } +} + +// TestItemRepo_Search_GroupByHasNoOutputAliases asserts the scored CTE's +// GROUP BY uses raw column references: AS aliases are valid in a select list +// but are a syntax error inside GROUP BY, so the CTE must not reuse the +// aliased projection there. +func TestItemRepo_Search_GroupByHasNoOutputAliases(t *testing.T) { + repo := &ItemRepository{} + dataSQL, countSQL, _ := repo.buildSearchSQL("avatar", []string{"movie"}, 20, 0, AccessFilter{}) + + for _, sql := range []string{dataSQL, countSQL} { + idx := strings.Index(sql, "GROUP BY") + if idx < 0 { + t.Fatalf("expected GROUP BY in scored CTE; got:\n%s", sql) + } + clause := sql[idx:] + if end := strings.Index(clause, ")"); end >= 0 { + clause = clause[:end] + } + if strings.Contains(clause, " AS ") { + t.Fatalf("GROUP BY must not contain output aliases; got:\n%s", clause) + } + if strings.Contains(clause, "COALESCE") { + t.Fatalf("GROUP BY should group by raw columns, not COALESCE expressions; got:\n%s", clause) + } + } +} diff --git a/internal/catalog/media_scope_test.go b/internal/catalog/media_scope_test.go new file mode 100644 index 00000000..b08c8206 --- /dev/null +++ b/internal/catalog/media_scope_test.go @@ -0,0 +1,117 @@ +package catalog + +import ( + "net/url" + "reflect" + "strings" + "testing" +) + +// TestMediaScopeItemTypes pins the expansion of group scopes: "video" covers +// the video-side media_items types so search/browse can offer a +// "Movies & Series vs Audiobooks" split without enumerating types per caller. +func TestMediaScopeItemTypes(t *testing.T) { + cases := []struct { + scope string + want []string + }{ + {"", nil}, + {"movie", []string{"movie"}}, + {"audiobook", []string{"audiobook"}}, + {"video", []string{"movie", "series"}}, + {" Video ", []string{"movie", "series"}}, + } + for _, tc := range cases { + if got := MediaScopeItemTypes(tc.scope); !reflect.DeepEqual(got, tc.want) { + t.Errorf("MediaScopeItemTypes(%q) = %v, want %v", tc.scope, got, tc.want) + } + } +} + +func TestMediaScopeMatchesItemType(t *testing.T) { + cases := []struct { + scope string + itemType string + want bool + }{ + {"", "audiobook", true}, + {"video", "movie", true}, + {"video", "series", true}, + {"video", "audiobook", false}, + {"audiobook", "audiobook", true}, + {"movie", "series", false}, + } + for _, tc := range cases { + if got := MediaScopeMatchesItemType(tc.scope, tc.itemType); got != tc.want { + t.Errorf("MediaScopeMatchesItemType(%q, %q) = %v, want %v", tc.scope, tc.itemType, got, tc.want) + } + } +} + +// TestParseCatalogRequest_VideoMediaScope asserts ?type=video parses into the +// query definition's media scope, and that catalogSearchAccess expands it to +// the video item types for the direct search path. +func TestParseCatalogRequest_VideoMediaScope(t *testing.T) { + req, err := ParseCatalogRequest(url.Values{ + "source": {"query"}, + "q": {"the rookie"}, + "type": {"video"}, + }) + if err != nil { + t.Fatalf("ParseCatalogRequest: %v", err) + } + if req.Query.MediaScope != "video" { + t.Fatalf("expected media scope video, got %q", req.Query.MediaScope) + } + + _, itemTypes, earlyEmpty := catalogSearchAccess(req, AccessFilter{}) + if earlyEmpty { + t.Fatal("unexpected early empty") + } + if !reflect.DeepEqual(itemTypes, []string{"movie", "series"}) { + t.Fatalf("expected video scope to expand to movie+series, got %v", itemTypes) + } +} + +// TestPreviewPage_VideoScopeUsesTypeAny asserts the preview/query-executor +// path renders a multi-type condition for the video group scope. +func TestPreviewPage_VideoScopeUsesTypeAny(t *testing.T) { + sql, args, err := (&QueryExecutor{}).buildPreviewPageSQL( + QueryDefinition{ + MediaScope: "video", + Sort: QuerySort{Field: "title", Order: "asc"}, + }, + AccessFilter{}, + 20, + 0, + true, + ) + if err != nil { + t.Fatalf("buildPreviewPageSQL error: %v", err) + } + if !strings.Contains(sql, "mi.type = ANY(") { + t.Fatalf("expected mi.type = ANY(...) for video scope, got %s", sql) + } + found := false + for _, arg := range args { + if types, ok := arg.([]string); ok && reflect.DeepEqual(types, []string{"movie", "series"}) { + found = true + } + } + if !found { + t.Fatalf("expected movie+series type arg, got %v", args) + } +} + +// TestQueryDefinitionValidate_VideoScope asserts "video" passes definition +// validation alongside the single-type scopes. +func TestQueryDefinitionValidate_VideoScope(t *testing.T) { + def := QueryDefinition{MediaScope: "video"} + if err := def.Validate(); err != nil { + t.Fatalf("expected video media scope to validate, got %v", err) + } + bad := QueryDefinition{MediaScope: "podcast"} + if err := bad.Validate(); err == nil { + t.Fatal("expected invalid media scope to fail validation") + } +} diff --git a/internal/catalog/nextup_repo.go b/internal/catalog/nextup_repo.go index 4ccab104..199b2e37 100644 --- a/internal/catalog/nextup_repo.go +++ b/internal/catalog/nextup_repo.go @@ -142,7 +142,7 @@ func buildListNextUpQuery(q NextUpQuery, limit int) (string, []interface{}) { JOIN episodes e_ip ON e_ip.content_id = uwp_ip.media_item_id WHERE uwp_ip.user_id = $1 AND uwp_ip.profile_id = $2 - AND uwp_ip.completed = FALSE + AND uwp_ip.position_seconds > 0 AND e_ip.series_id = ce.series_id AND uwp_ip.updated_at > ce.updated_at ) @@ -257,7 +257,7 @@ func buildListResumableFirstEpisodesQuery(q NextUpQuery, inProgressIDs []string) JOIN media_items si ON si.content_id = e.series_id WHERE uwp.user_id = $1 AND uwp.profile_id = $2 - AND uwp.completed = FALSE + AND uwp.position_seconds > 0 AND uwp.media_item_id = ANY($3)%s%s ORDER BY e.series_id, uwp.updated_at DESC`, seriesFilter, completedSeriesGate) diff --git a/internal/catalog/nextup_repo_test.go b/internal/catalog/nextup_repo_test.go index 8b60a645..eeec3ae2 100644 --- a/internal/catalog/nextup_repo_test.go +++ b/internal/catalog/nextup_repo_test.go @@ -15,7 +15,7 @@ func TestBuildListNextUpQuery_PrefersRecentCompletedOverOlderPartialProgress(t * expectedFragments := []string{ "eligible_series AS (", - "uwp_ip.completed = FALSE", + "uwp_ip.position_seconds > 0", "e_ip.series_id = ce.series_id", "uwp_ip.updated_at > ce.updated_at", "FROM user_history_hidden_items hhi", diff --git a/internal/catalog/query_builder.go b/internal/catalog/query_builder.go index 6c0043b4..126b30e3 100644 --- a/internal/catalog/query_builder.go +++ b/internal/catalog/query_builder.go @@ -11,6 +11,12 @@ import ( "github.com/Silo-Server/silo-server/internal/models" ) +// EbookFinishedProgressThresholdSQL is models.EbookFinishedProgressThreshold +// rendered as a SQL numeric literal. Queries that classify ebook reader +// progress as finished must interpolate this value instead of hardcoding the +// threshold so the definition of "finished" cannot drift between subsystems. +var EbookFinishedProgressThresholdSQL = strconv.FormatFloat(models.EbookFinishedProgressThreshold, 'f', -1, 64) + type QueryBuilder struct { alias string argIdx int @@ -159,9 +165,10 @@ func (qb *QueryBuilder) UserHistoryCTEArgs() []any { // UserHistoryCTESQL returns the user_last_watched CTE definition. The two // placeholder positions (user_id, profile_id) are bound to the args returned // by UserHistoryCTEArgs and should be the first two entries in the final -// statement's arg list. The CTE aggregates user_watch_history watch_at and -// user_watch_progress.completed=TRUE updated_at into one row per -// media_item_id, filtering out items the user has hidden via +// statement's arg list. The CTE aggregates user_watch_history watched_at, +// user_watch_progress.completed=TRUE updated_at, and completed ebook reader +// progress updated_at into one row per media_item_id, filtering out videos the +// user has hidden via // user_history_hidden_items. The single GROUP BY replaces the two // correlated MAX subqueries the previous lastWatchedExpr emitted (audit // 2026-05-01 §3.1 Pattern B). @@ -187,6 +194,10 @@ func UserHistoryCTESQL(argIdx int) string { SELECT uwp.media_item_id, NULL::timestamptz, uwp.updated_at FROM user_watch_progress uwp WHERE uwp.user_id = $%d AND uwp.profile_id = $%d AND uwp.completed = TRUE + UNION ALL + SELECT erp.content_id AS media_item_id, NULL::timestamptz, erp.updated_at + FROM ebook_reader_progress erp + WHERE erp.user_id = $%d AND erp.profile_id = $%d AND erp.progress >= %s ) src WHERE NOT EXISTS ( SELECT 1 FROM user_history_hidden_items hhi @@ -195,7 +206,7 @@ func UserHistoryCTESQL(argIdx int) string { AND COALESCE(src.uwh_at, src.uwp_at) <= hhi.hidden_before ) GROUP BY src.media_item_id -)`, argIdx, argIdx+1, argIdx, argIdx+1, argIdx, argIdx+1) +)`, argIdx, argIdx+1, argIdx, argIdx+1, argIdx, argIdx+1, EbookFinishedProgressThresholdSQL, argIdx, argIdx+1) } func (qb *QueryBuilder) BuildSortClause(sortConfig QuerySort) (string, []any, error) { @@ -298,7 +309,8 @@ func (qb *QueryBuilder) BuildSortPlan(sortConfig QuerySort) (QuerySortPlan, erro return plan, nil case "series": plan.Joins = []string{fmt.Sprintf( - "LEFT JOIN audiobook_series sort_series ON sort_series.content_id = %s.content_id", + "LEFT JOIN %s sort_series ON sort_series.content_id = %s.content_id", + qb.bookSeriesTable(), qb.alias, )} // Sort by series name primarily, then by series_index so books @@ -385,7 +397,7 @@ func (qb *QueryBuilder) buildRule(rule QueryRule) (string, error) { case "narrator": return qb.buildPersonClause(models.PersonKindNarrator, rule) case "series": - return qb.buildAudiobookSeriesClause(rule) + return qb.buildBookSeriesClause(rule) case "watched": return qb.buildWatchedClause(rule) case "favorited": @@ -502,14 +514,21 @@ func (qb *QueryBuilder) personKindSortJoin(kind models.PersonKind, alias string) ) } -func (qb *QueryBuilder) buildAudiobookSeriesClause(rule QueryRule) (string, error) { +func (qb *QueryBuilder) bookSeriesTable() string { + if qb.mediaScope == "ebook" { + return "ebook_series" + } + return "audiobook_series" +} + +func (qb *QueryBuilder) buildBookSeriesClause(rule QueryRule) (string, error) { qb.args = append(qb.args, rule.Value) clause := fmt.Sprintf(`EXISTS ( SELECT 1 - FROM audiobook_series s + FROM %s s WHERE s.content_id = %s.content_id AND LOWER(BTRIM(s.series_name)) = LOWER(BTRIM($%d)) - )`, qb.alias, qb.argIdx) +)`, qb.bookSeriesTable(), qb.alias, qb.argIdx) qb.argIdx++ if rule.Op == "is_not" { return "NOT (" + clause + ")", nil @@ -947,6 +966,9 @@ func (qb *QueryBuilder) buildInProgressClause(rule QueryRule) (string, error) { if !ok { return "", fmt.Errorf("in_progress requires a boolean value") } + if qb.mediaScope == "ebook" { + return qb.buildEbookInProgressClause(value), nil + } qb.args = append(qb.args, qb.userID, qb.profileID) clause := fmt.Sprintf(`EXISTS ( SELECT 1 @@ -954,7 +976,6 @@ func (qb *QueryBuilder) buildInProgressClause(rule QueryRule) (string, error) { WHERE uwp.user_id = $%d AND uwp.profile_id = $%d AND uwp.media_item_id = %s.content_id - AND uwp.completed = FALSE AND uwp.position_seconds > 0 AND NOT EXISTS ( SELECT 1 @@ -972,6 +993,32 @@ func (qb *QueryBuilder) buildInProgressClause(rule QueryRule) (string, error) { return clause, nil } +func (qb *QueryBuilder) buildEbookInProgressClause(value bool) string { + qb.args = append(qb.args, qb.userID, qb.profileID) + clause := fmt.Sprintf(`EXISTS ( + SELECT 1 + FROM ebook_reader_progress erp + WHERE erp.user_id = $%d + AND erp.profile_id = $%d + AND erp.content_id = %s.content_id + AND erp.progress > 0 + AND erp.progress < %s + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = $%d + AND hhi.profile_id = $%d + AND hhi.media_item_id = erp.content_id + AND erp.updated_at <= hhi.hidden_before + ) + )`, qb.argIdx, qb.argIdx+1, qb.alias, EbookFinishedProgressThresholdSQL, qb.argIdx, qb.argIdx+1) + qb.argIdx += 2 + if !value { + return "NOT (" + clause + ")" + } + return clause +} + func (qb *QueryBuilder) buildLastWatchedClause(rule QueryRule) (string, error) { if err := qb.ensureUserScope(rule.Field); err != nil { return "", err @@ -1070,6 +1117,9 @@ func (qb *QueryBuilder) buildReleaseDateClause(rule QueryRule) (string, error) { } func (qb *QueryBuilder) userStateCompletionClause() string { + if qb.mediaScope == "ebook" { + return qb.ebookUserStateCompletionClause() + } qb.args = append(qb.args, qb.userID, qb.profileID) clause := fmt.Sprintf(`( EXISTS ( @@ -1120,6 +1170,28 @@ func (qb *QueryBuilder) userStateCompletionClause() string { return clause } +func (qb *QueryBuilder) ebookUserStateCompletionClause() string { + qb.args = append(qb.args, qb.userID, qb.profileID) + clause := fmt.Sprintf(`EXISTS ( + SELECT 1 + FROM ebook_reader_progress erp + WHERE erp.user_id = $%d + AND erp.profile_id = $%d + AND erp.content_id = %s.content_id + AND erp.progress >= %s + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = $%d + AND hhi.profile_id = $%d + AND hhi.media_item_id = erp.content_id + AND erp.updated_at <= hhi.hidden_before + ) + )`, qb.argIdx, qb.argIdx+1, qb.alias, EbookFinishedProgressThresholdSQL, qb.argIdx, qb.argIdx+1) + qb.argIdx += 2 + return clause +} + // lastWatchedExpr returns the SQL expression the executor binds to the // per-user history aggregate. It signals the executor (via // requireUserHistoryCTE) that the user_last_watched CTE and a LEFT JOIN @@ -1343,12 +1415,14 @@ func (qb *QueryBuilder) consumeIntArgs(values []int) ([]string, []any) { } func (qb *QueryBuilder) progressSortPlan() (string, []string, []any) { + if qb.mediaScope == "ebook" { + return qb.ebookProgressSortPlan() + } joinSQL := fmt.Sprintf( `LEFT JOIN ( SELECT uwp.media_item_id AS content_id, CASE - WHEN uwp.completed = FALSE - AND uwp.position_seconds > 0 + WHEN uwp.position_seconds > 0 AND COALESCE(uwp.duration_seconds, 0) > 0 AND (hhi.media_item_id IS NULL OR uwp.updated_at > hhi.hidden_before) THEN uwp.position_seconds::double precision / NULLIF(uwp.duration_seconds, 0) @@ -1371,7 +1445,39 @@ func (qb *QueryBuilder) progressSortPlan() (string, []string, []any) { return "sort_progress.progress_ratio", []string{joinSQL}, []any{qb.userID, qb.profileID} } +func (qb *QueryBuilder) ebookProgressSortPlan() (string, []string, []any) { + joinSQL := fmt.Sprintf( + `LEFT JOIN ( + SELECT erp.content_id, + CASE + WHEN erp.progress > 0 + AND erp.progress < %s + AND (hhi.media_item_id IS NULL OR erp.updated_at > hhi.hidden_before) + THEN erp.progress::double precision + ELSE NULL + END AS progress_ratio + FROM ebook_reader_progress erp + LEFT JOIN user_history_hidden_items hhi + ON hhi.user_id = $%d + AND hhi.profile_id = $%d + AND hhi.media_item_id = erp.content_id + WHERE erp.user_id = $%d + AND erp.profile_id = $%d + ) sort_progress ON sort_progress.content_id = %s.content_id`, + EbookFinishedProgressThresholdSQL, + qb.argIdx, + qb.argIdx+1, + qb.argIdx, + qb.argIdx+1, + qb.alias, + ) + return "sort_progress.progress_ratio", []string{joinSQL}, []any{qb.userID, qb.profileID} +} + func (qb *QueryBuilder) dateViewedSortPlan() (string, []string, []any) { + if qb.mediaScope == "ebook" { + return qb.ebookDateViewedSortPlan() + } historyJoin := fmt.Sprintf( `LEFT JOIN ( SELECT uwh.media_item_id AS content_id, MAX(uwh.watched_at) AS viewed_at @@ -1422,7 +1528,38 @@ func (qb *QueryBuilder) dateViewedSortPlan() (string, []string, []any) { return expr, []string{historyJoin, progressJoin}, []any{qb.userID, qb.profileID} } +func (qb *QueryBuilder) ebookDateViewedSortPlan() (string, []string, []any) { + joinSQL := fmt.Sprintf( + `LEFT JOIN ( + SELECT erp.content_id, + CASE + WHEN erp.progress >= %s + AND (hhi.media_item_id IS NULL OR erp.updated_at > hhi.hidden_before) + THEN erp.updated_at + ELSE NULL + END AS viewed_at + FROM ebook_reader_progress erp + LEFT JOIN user_history_hidden_items hhi + ON hhi.user_id = $%d + AND hhi.profile_id = $%d + AND hhi.media_item_id = erp.content_id + WHERE erp.user_id = $%d + AND erp.profile_id = $%d + ) sort_ebook_viewed ON sort_ebook_viewed.content_id = %s.content_id`, + EbookFinishedProgressThresholdSQL, + qb.argIdx, + qb.argIdx+1, + qb.argIdx, + qb.argIdx+1, + qb.alias, + ) + return "sort_ebook_viewed.viewed_at", []string{joinSQL}, []any{qb.userID, qb.profileID} +} + func (qb *QueryBuilder) playsSortPlan() (string, []string, []any) { + if qb.mediaScope == "ebook" { + return qb.ebookPlaysSortPlan() + } historyJoin := fmt.Sprintf( `LEFT JOIN ( SELECT uwh.media_item_id AS content_id, COUNT(*) AS play_count @@ -1473,6 +1610,34 @@ func (qb *QueryBuilder) playsSortPlan() (string, []string, []any) { return expr, []string{historyJoin, progressJoin}, []any{qb.userID, qb.profileID} } +func (qb *QueryBuilder) ebookPlaysSortPlan() (string, []string, []any) { + joinSQL := fmt.Sprintf( + `LEFT JOIN ( + SELECT erp.content_id, + CASE + WHEN erp.progress >= %s + AND (hhi.media_item_id IS NULL OR erp.updated_at > hhi.hidden_before) + THEN 1 + ELSE 0 + END AS completed_play_count + FROM ebook_reader_progress erp + LEFT JOIN user_history_hidden_items hhi + ON hhi.user_id = $%d + AND hhi.profile_id = $%d + AND hhi.media_item_id = erp.content_id + WHERE erp.user_id = $%d + AND erp.profile_id = $%d + ) sort_ebook_plays ON sort_ebook_plays.content_id = %s.content_id`, + EbookFinishedProgressThresholdSQL, + qb.argIdx, + qb.argIdx+1, + qb.argIdx, + qb.argIdx+1, + qb.alias, + ) + return "NULLIF(sort_ebook_plays.completed_play_count, 0)", []string{joinSQL}, []any{qb.userID, qb.profileID} +} + func (qb *QueryBuilder) lastAirDateSortPlan() (string, []string) { expr := fmt.Sprintf("%s.last_air_date_at", qb.alias) return expr, nil // denormalized column — no JOIN required diff --git a/internal/catalog/query_builder_test.go b/internal/catalog/query_builder_test.go index e721b0a3..7b6b20de 100644 --- a/internal/catalog/query_builder_test.go +++ b/internal/catalog/query_builder_test.go @@ -344,7 +344,7 @@ func TestBuildSortClause_PersonalizedComputedSorts(t *testing.T) { }{ { field: "progress", - expectJoin: []string{"FROM user_watch_progress uwp", "uwp.completed = FALSE", "uwp.position_seconds::double precision / NULLIF(uwp.duration_seconds, 0)"}, + expectJoin: []string{"FROM user_watch_progress uwp", "uwp.position_seconds > 0", "uwp.position_seconds::double precision / NULLIF(uwp.duration_seconds, 0)"}, expectExpr: []string{"sort_progress.progress_ratio"}, expectArgN: 2, }, @@ -390,6 +390,161 @@ func TestBuildSortClause_PersonalizedComputedSorts(t *testing.T) { } } +func TestBuildSortClause_EbookPersonalizedComputedSortsUseReaderProgress(t *testing.T) { + builder := NewQueryBuilder("mi").WithMediaScope("ebook").WithUserScope(42, "profile-1") + tests := []struct { + field string + expectJoin []string + expectExpr []string + }{ + { + field: "progress", + expectJoin: []string{ + "FROM ebook_reader_progress erp", + "erp.progress > 0", + "erp.progress < 0.9", + "LEFT JOIN user_history_hidden_items hhi", + "hhi.media_item_id IS NULL OR erp.updated_at > hhi.hidden_before", + }, + expectExpr: []string{"sort_progress.progress_ratio"}, + }, + { + field: "date_viewed", + expectJoin: []string{ + "FROM ebook_reader_progress erp", + "erp.progress >= 0.9", + "LEFT JOIN user_history_hidden_items hhi", + "hhi.media_item_id IS NULL OR erp.updated_at > hhi.hidden_before", + }, + expectExpr: []string{"sort_ebook_viewed.viewed_at"}, + }, + { + field: "plays", + expectJoin: []string{ + "FROM ebook_reader_progress erp", + "erp.progress >= 0.9", + "completed_play_count", + "LEFT JOIN user_history_hidden_items hhi", + "hhi.media_item_id IS NULL OR erp.updated_at > hhi.hidden_before", + }, + expectExpr: []string{"sort_ebook_plays.completed_play_count"}, + }, + } + + for _, tt := range tests { + plan, err := builder.BuildSortPlan(QuerySort{Field: tt.field, Order: "desc"}) + if err != nil { + t.Fatalf("%s: BuildSortPlan returned error: %v", tt.field, err) + } + if len(plan.Args) != 2 || plan.Args[0] != 42 || plan.Args[1] != "profile-1" { + t.Fatalf("%s: expected user scope args, got %v", tt.field, plan.Args) + } + joined := strings.Join(plan.Joins, "\n") + for _, fragment := range tt.expectJoin { + if !strings.Contains(joined, fragment) { + t.Fatalf("%s: expected joins to contain %q, got %q", tt.field, fragment, joined) + } + } + for _, fragment := range tt.expectExpr { + if !strings.Contains(plan.OrderBy, fragment) { + t.Fatalf("%s: expected order by to contain %q, got %q", tt.field, fragment, plan.OrderBy) + } + } + } +} + +func TestBuild_EbookUserStateRulesUseReaderProgress(t *testing.T) { + tests := []struct { + name string + rule QueryRule + expectSQL []string + unexpectSQL []string + }{ + { + name: "watched", + rule: QueryRule{Field: "watched", Op: "is", Value: true}, + expectSQL: []string{ + "FROM ebook_reader_progress erp", + "erp.progress >= 0.9", + "FROM user_history_hidden_items hhi", + "hhi.media_item_id = erp.content_id", + "erp.updated_at <= hhi.hidden_before", + }, + unexpectSQL: []string{"FROM user_watch_progress uwp", "FROM user_watch_history uwh"}, + }, + { + name: "unread", + rule: QueryRule{Field: "watched", Op: "is", Value: false}, + expectSQL: []string{ + "NOT (EXISTS", + "FROM ebook_reader_progress erp", + "erp.progress >= 0.9", + "FROM user_history_hidden_items hhi", + "hhi.media_item_id = erp.content_id", + "erp.updated_at <= hhi.hidden_before", + }, + unexpectSQL: []string{"FROM user_watch_progress uwp", "FROM user_watch_history uwh"}, + }, + { + name: "in progress", + rule: QueryRule{Field: "in_progress", Op: "is", Value: true}, + expectSQL: []string{ + "FROM ebook_reader_progress erp", + "erp.progress > 0", + "erp.progress < 0.9", + "FROM user_history_hidden_items hhi", + "hhi.media_item_id = erp.content_id", + "erp.updated_at <= hhi.hidden_before", + }, + unexpectSQL: []string{"FROM user_watch_progress uwp"}, + }, + { + name: "not in progress", + rule: QueryRule{Field: "in_progress", Op: "is", Value: false}, + expectSQL: []string{ + "NOT (EXISTS", + "FROM ebook_reader_progress erp", + "erp.progress > 0", + "erp.progress < 0.9", + "FROM user_history_hidden_items hhi", + "hhi.media_item_id = erp.content_id", + "erp.updated_at <= hhi.hidden_before", + }, + unexpectSQL: []string{"FROM user_watch_progress uwp"}, + }, + } + + for _, tt := range tests { + clause, args, err := NewQueryBuilder("mi"). + WithMediaScope("ebook"). + WithUserScope(42, "profile-1"). + Build(QueryDefinition{ + MediaScope: "ebook", + Match: "all", + Groups: []QueryGroup{{ + Match: "all", + Rules: []QueryRule{tt.rule}, + }}, + }) + if err != nil { + t.Fatalf("%s: Build returned error: %v", tt.name, err) + } + if len(args) != 2 || args[0] != 42 || args[1] != "profile-1" { + t.Fatalf("%s: expected user scope args, got %v", tt.name, args) + } + for _, fragment := range tt.expectSQL { + if !strings.Contains(clause, fragment) { + t.Fatalf("%s: expected clause to contain %q, got %q", tt.name, fragment, clause) + } + } + for _, fragment := range tt.unexpectSQL { + if strings.Contains(clause, fragment) { + t.Fatalf("%s: did not expect clause to contain %q, got %q", tt.name, fragment, clause) + } + } + } +} + func TestBuild_ResolutionNormalizes4KAndScopesMediaFiles(t *testing.T) { clause, args, err := NewQueryBuilder("mi"). WithLibraryScope([]int{3}). @@ -531,6 +686,26 @@ func TestBuild_SeriesClauseJoinsAudiobookSeries(t *testing.T) { } } +func TestBuild_SeriesClauseJoinsEbookSeriesForEbookScope(t *testing.T) { + clause, args, err := NewQueryBuilder("mi"). + WithMediaScope("ebook"). + Build(QueryDefinition{ + Match: "all", + Groups: []QueryGroup{ + {Match: "all", Rules: []QueryRule{{Field: "series", Op: "is", Value: "Wayfarers"}}}, + }, + }) + if err != nil { + t.Fatalf("Build returned error: %v", err) + } + if len(args) != 1 || args[0] != "Wayfarers" { + t.Fatalf("expected series arg, got %v", args) + } + if !strings.Contains(clause, "FROM ebook_series s") { + t.Fatalf("expected ebook_series join in series clause, got %q", clause) + } +} + func TestBuildSortPlan_AuthorJoinsLateralOnPersonKindAuthor(t *testing.T) { plan, err := NewQueryBuilder("mi").BuildSortPlan(QuerySort{Field: "author", Order: "asc"}) if err != nil { @@ -576,6 +751,18 @@ func TestBuildSortPlan_SeriesOrdersByNameThenIndex(t *testing.T) { } } +func TestBuildSortPlan_SeriesUsesEbookSeriesForEbookScope(t *testing.T) { + plan, err := NewQueryBuilder("mi"). + WithMediaScope("ebook"). + BuildSortPlan(QuerySort{Field: "series", Order: "asc"}) + if err != nil { + t.Fatalf("BuildSortPlan(series) returned error: %v", err) + } + if len(plan.Joins) != 1 || !strings.Contains(plan.Joins[0], "ebook_series sort_series") { + t.Fatalf("expected LEFT JOIN ebook_series, got %v", plan.Joins) + } +} + func TestBuild_SeriesClauseNegationWrapsInNOT(t *testing.T) { clause, _, err := NewQueryBuilder("mi").Build(QueryDefinition{ Match: "all", @@ -713,3 +900,18 @@ func TestQueryBuilder_LastWatched_RequiresCTE(t *testing.T) { t.Fatalf("after lastWatchedExpr, builder should require CTE") } } + +func TestUserHistoryCTESQLIncludesCompletedEbookReaderProgress(t *testing.T) { + sql := UserHistoryCTESQL(1) + expectedFragments := []string{ + "FROM ebook_reader_progress erp", + "erp.progress >= 0.9", + "erp.content_id", + "erp.updated_at", + } + for _, fragment := range expectedFragments { + if !strings.Contains(sql, fragment) { + t.Fatalf("expected CTE to contain %q, got:\n%s", fragment, sql) + } + } +} diff --git a/internal/catalog/query_definition.go b/internal/catalog/query_definition.go index b9fb115f..a3076c45 100644 --- a/internal/catalog/query_definition.go +++ b/internal/catalog/query_definition.go @@ -104,6 +104,53 @@ type QueryDefinition struct { Limit *int `json:"limit,omitempty"` } +// MediaScopeVideo is the group scope covering all video-side item types. It +// is accepted anywhere a single-type media scope is and expands to the +// underlying media_items.type values via MediaScopeItemTypes. +const MediaScopeVideo = "video" + +// IsValidMediaScope reports whether scope (already normalized to lowercase) +// is an accepted media_scope value. Empty means unscoped and is valid. +func IsValidMediaScope(scope string) bool { + switch scope { + case "", "movie", "series", "episode", "audiobook", "ebook", MediaScopeVideo: + return true + default: + return false + } +} + +// MediaScopeItemTypes expands a media scope into the media_items.type values +// it covers. Single-type scopes map to themselves; the empty scope returns +// nil (unscoped). +func MediaScopeItemTypes(scope string) []string { + scope = strings.ToLower(strings.TrimSpace(scope)) + switch scope { + case "": + return nil + case MediaScopeVideo: + return []string{"movie", "series"} + default: + return []string{scope} + } +} + +// MediaScopeMatchesItemType reports whether an item type falls inside scope. +// An empty scope matches every type. +func MediaScopeMatchesItemType(scope, itemType string) bool { + types := MediaScopeItemTypes(scope) + if len(types) == 0 { + return true + } + itemType = strings.ToLower(strings.TrimSpace(itemType)) + for _, t := range types { + if t == itemType { + return true + } + } + return false +} + const ( DefaultSmartCollectionItemLimit = 100 MaxSmartCollectionItemLimit = 500 @@ -182,12 +229,8 @@ func (q QueryDefinition) ValidateWithOptions(allowPersonalizedSorts, allowPerson } } - if normalized.MediaScope != "" && - normalized.MediaScope != "movie" && - normalized.MediaScope != "series" && - normalized.MediaScope != "episode" && - normalized.MediaScope != "audiobook" { - return fmt.Errorf("media_scope must be 'movie', 'series', 'episode', or 'audiobook'") + if !IsValidMediaScope(normalized.MediaScope) { + return fmt.Errorf("media_scope must be 'movie', 'series', 'episode', 'audiobook', 'ebook', or 'video'") } if normalized.Match != "all" && normalized.Match != "any" { @@ -206,6 +249,9 @@ func (q QueryDefinition) ValidateWithOptions(allowPersonalizedSorts, allowPerson if def.personalized && !allowPersonalizedFields { return fmt.Errorf("groups[%d].rules[%d].field %q requires profile scope", i, j, rule.Field) } + if normalized.MediaScope == "ebook" && rule.Field == "narrator" { + return fmt.Errorf("groups[%d].rules[%d].field %q is not supported for ebook media_scope", i, j, rule.Field) + } if !def.validOps[rule.Op] { return fmt.Errorf("groups[%d].rules[%d].op %q is not supported for field %q", i, j, rule.Op, rule.Field) } @@ -220,6 +266,9 @@ func (q QueryDefinition) ValidateWithOptions(allowPersonalizedSorts, allowPerson if def.personalized && !allowPersonalizedSorts { return fmt.Errorf("sort.field %q requires profile scope", normalized.Sort.Field) } + if normalized.MediaScope == "ebook" && normalized.Sort.Field == "narrator" { + return fmt.Errorf("sort.field %q is not supported for ebook media_scope", normalized.Sort.Field) + } } if normalized.Sort.Order != "" && normalized.Sort.Order != "asc" && normalized.Sort.Order != "desc" { return fmt.Errorf("sort.order must be 'asc' or 'desc'") diff --git a/internal/catalog/query_definition_test.go b/internal/catalog/query_definition_test.go index 91629e40..b95762e1 100644 --- a/internal/catalog/query_definition_test.go +++ b/internal/catalog/query_definition_test.go @@ -72,6 +72,45 @@ func TestValidate_AudiobookMediaScope(t *testing.T) { } } +func TestValidate_EbookMediaScope(t *testing.T) { + qd := QueryDefinition{ + MediaScope: "ebook", + Match: "all", + Groups: []QueryGroup{}, + Sort: QuerySort{Field: "title", Order: "asc"}, + } + if err := qd.Validate(); err != nil { + t.Fatalf("expected ebook media scope to be valid, got %v", err) + } +} + +func TestValidate_EbookMediaScopeRejectsNarratorRule(t *testing.T) { + qd := QueryDefinition{ + MediaScope: "ebook", + Match: "all", + Groups: []QueryGroup{{ + Match: "all", + Rules: []QueryRule{{Field: "narrator", Op: "is", Value: "Narrator"}}, + }}, + Sort: QuerySort{Field: "title", Order: "asc"}, + } + if err := qd.Validate(); err == nil { + t.Fatal("expected ebook narrator rule to be rejected") + } +} + +func TestValidate_EbookMediaScopeRejectsNarratorSort(t *testing.T) { + qd := QueryDefinition{ + MediaScope: "ebook", + Match: "all", + Groups: []QueryGroup{}, + Sort: QuerySort{Field: "narrator", Order: "asc"}, + } + if err := qd.Validate(); err == nil { + t.Fatal("expected ebook narrator sort to be rejected") + } +} + func TestValidateWithSortScope_RejectsPersonalizedSortWithoutProfileScope(t *testing.T) { qd := QueryDefinition{ Match: "all", diff --git a/internal/catalog/query_executor.go b/internal/catalog/query_executor.go index e4e00ff6..37950cf8 100644 --- a/internal/catalog/query_executor.go +++ b/internal/catalog/query_executor.go @@ -282,8 +282,14 @@ func (e *QueryExecutor) buildPreviewPagePlan( conditions = append(conditions, filterWhere) } if def.MediaScope != "" && !isEpisodeCatalogScope(def.MediaScope) { - conditions = append(conditions, fmt.Sprintf("mi.type = $%d", argIdx)) - args = append(args, def.MediaScope) + scopeTypes := MediaScopeItemTypes(def.MediaScope) + if len(scopeTypes) == 1 { + conditions = append(conditions, fmt.Sprintf("mi.type = $%d", argIdx)) + args = append(args, scopeTypes[0]) + } else { + conditions = append(conditions, fmt.Sprintf("mi.type = ANY($%d)", argIdx)) + args = append(args, scopeTypes) + } argIdx++ } libScopeWhere, libScopeArgs, hasLibraryScope := "", []any(nil), false diff --git a/internal/compat/zishang520-webtransport-go/compat.go b/internal/compat/zishang520-webtransport-go/compat.go new file mode 100644 index 00000000..2c693039 --- /dev/null +++ b/internal/compat/zishang520-webtransport-go/compat.go @@ -0,0 +1,80 @@ +// Package webtransport preserves the github.com/zishang520/webtransport-go +// API shape expected by github.com/zishang520/engine.io while delegating to +// the maintained github.com/quic-go/webtransport-go module. +package webtransport + +import ( + "net" + "net/http" + "time" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + upstream "github.com/quic-go/webtransport-go" +) + +type ( + Session = upstream.Session + SessionState = upstream.SessionState + SessionError = upstream.SessionError + SessionErrorCode = upstream.SessionErrorCode + Stream = upstream.Stream + ReceiveStream = upstream.ReceiveStream + SendStream = upstream.SendStream + StreamError = upstream.StreamError + StreamErrorCode = upstream.StreamErrorCode + Dialer = upstream.Dialer +) + +const ( + WTBufferedStreamRejectedErrorCode = upstream.WTBufferedStreamRejectedErrorCode + WTSessionGoneErrorCode = upstream.WTSessionGoneErrorCode +) + +// Server matches the older zishang520/webtransport-go shape where H3 was a +// value field. The upstream module changed it to *http3.Server in v0.10.0. +type Server struct { + H3 http3.Server + ApplicationProtocols []string + ReorderingTimeout time.Duration + CheckOrigin func(r *http.Request) bool + + upstream *upstream.Server +} + +func (s *Server) delegate() *upstream.Server { + if s.upstream == nil { + s.upstream = &upstream.Server{H3: &s.H3} + } + s.upstream.ApplicationProtocols = s.ApplicationProtocols + s.upstream.ReorderingTimeout = s.ReorderingTimeout + s.upstream.CheckOrigin = s.CheckOrigin + return s.upstream +} + +func (s *Server) Serve(conn net.PacketConn) error { + return s.delegate().Serve(conn) +} + +func (s *Server) ServeQUICConn(conn *quic.Conn) error { + return s.delegate().ServeQUICConn(conn) +} + +func (s *Server) ListenAndServe() error { + return s.delegate().ListenAndServe() +} + +func (s *Server) ListenAndServeTLS(certFile, keyFile string) error { + return s.delegate().ListenAndServeTLS(certFile, keyFile) +} + +func (s *Server) Close() error { + if s.upstream != nil { + return s.upstream.Close() + } + return s.H3.Close() +} + +func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*Session, error) { + return s.delegate().Upgrade(w, r) +} diff --git a/internal/compat/zishang520-webtransport-go/go.mod b/internal/compat/zishang520-webtransport-go/go.mod new file mode 100644 index 00000000..95eb14c1 --- /dev/null +++ b/internal/compat/zishang520-webtransport-go/go.mod @@ -0,0 +1,17 @@ +module github.com/zishang520/webtransport-go + +go 1.26.4 + +require ( + github.com/quic-go/quic-go v0.60.0 + github.com/quic-go/webtransport-go v0.10.0 +) + +require ( + github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect +) diff --git a/internal/compat/zishang520-webtransport-go/go.sum b/internal/compat/zishang520-webtransport-go/go.sum new file mode 100644 index 00000000..108123a0 --- /dev/null +++ b/internal/compat/zishang520-webtransport-go/go.sum @@ -0,0 +1,28 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/db_loader.go b/internal/config/db_loader.go index c521f118..9b16b074 100644 --- a/internal/config/db_loader.go +++ b/internal/config/db_loader.go @@ -355,9 +355,9 @@ func LoadFromDB(m map[string]string) (*Config, error) { } cfg.Auth.RefreshTokenExpiry = refreshTokenExpiry - // AudiobookshelfCompat — dedicated listener for ABS client apps. Keep the - // listener off unless ABS compatibility is explicitly enabled. - absCompatEnabled, err := boolOr(m, "audiobookshelf_compat.enabled", false) + // AudiobookshelfCompat — dedicated listener for ABS client apps. Docker + // deployments publish :13378, so listen by default unless disabled. + absCompatEnabled, err := boolOr(m, "audiobookshelf_compat.enabled", true) if err != nil { return nil, err } diff --git a/internal/config/db_loader_test.go b/internal/config/db_loader_test.go index 460bc43c..fbd95802 100644 --- a/internal/config/db_loader_test.go +++ b/internal/config/db_loader_test.go @@ -35,7 +35,15 @@ func TestLoadFromDBMetadataPresignExpiryRejectsInvalidDuration(t *testing.T) { } func TestLoadFromDBAudiobookshelfCompatFlagGatesCompatListener(t *testing.T) { - cfg, err := LoadFromDB(map[string]string{"audiobookshelf_compat.enabled": "false"}) + cfg, err := LoadFromDB(map[string]string{}) + if err != nil { + t.Fatalf("LoadFromDB() returned error: %v", err) + } + if cfg.AudiobookshelfCompat.Listen != ":13378" { + t.Fatalf("default audiobooks listener = %q, want default :13378", cfg.AudiobookshelfCompat.Listen) + } + + cfg, err = LoadFromDB(map[string]string{"audiobookshelf_compat.enabled": "false"}) if err != nil { t.Fatalf("LoadFromDB() returned error: %v", err) } diff --git a/internal/ebooks/enrichment.go b/internal/ebooks/enrichment.go new file mode 100644 index 00000000..fe1b8124 --- /dev/null +++ b/internal/ebooks/enrichment.go @@ -0,0 +1,765 @@ +package ebooks + +// Enricher periodically enriches ebook media_items that are missing metadata +// by querying the configured metadata-provider chain for each item's library +// folder. + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + "reflect" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/metadata" + "github.com/Silo-Server/silo-server/internal/models" +) + +const ( + ebookMetadataImageProviderID = "ebook-metadata" + + defaultEnrichBatchSize = 50 + defaultEnrichWorkers = 4 + + // enrichFailureCap is the ebook_enrichment_state.failures count at which + // an ebook stops being claimed for enrichment. Combined with the + // failure-count-first claim ordering this prevents a head-of-line block + // of permanently failing items from starving newer items and hammering + // providers. + enrichFailureCap = 5 +) + +// errEnrichmentSkipped marks an item that could not be attempted at all (no +// library folder linked yet, no providers configured). Skipped items are +// neither stamped as refreshed nor counted against the failure cap, so they +// are retried on every sweep until the missing prerequisite appears. +var errEnrichmentSkipped = errors.New("ebook enrichment skipped") + +func ebookContentType() string { + return "ebook" +} + +func ebookEnrichWorkers() int { + n := defaultEnrichWorkers + if v := os.Getenv("SILO_EBOOK_ENRICH_WORKERS"); v != "" { + if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 { + n = parsed + } + } + if n > defaultEnrichBatchSize { + n = defaultEnrichBatchSize + } + return n +} + +type enrichmentItemRow struct { + ContentID string + Title string + Year int + FolderID int + Language string + Author string + ProviderIDs map[string]string +} + +// Enricher drives the ebook metadata enrichment sweep. +type Enricher struct { + pool *pgxpool.Pool + chainRepo *metadata.ChainRepository + resolver *metadata.PluginResolverAdapter + itemRepo *catalog.ItemRepository + personRepo *catalog.PersonRepository + providerIDs *catalog.ProviderIDRepository + imageCacher metadata.ImageCacher + batchSize int + workers int +} + +func NewEnricher( + pool *pgxpool.Pool, + chainRepo *metadata.ChainRepository, + resolver *metadata.PluginResolverAdapter, + itemRepo *catalog.ItemRepository, + personRepo *catalog.PersonRepository, + providerIDs *catalog.ProviderIDRepository, +) *Enricher { + return &Enricher{ + pool: pool, + chainRepo: chainRepo, + resolver: resolver, + itemRepo: itemRepo, + personRepo: personRepo, + providerIDs: providerIDs, + batchSize: defaultEnrichBatchSize, + workers: ebookEnrichWorkers(), + } +} + +func (e *Enricher) SetImageCacher(cacher metadata.ImageCacher) { + if e == nil { + return + } + e.imageCacher = cacher +} + +func (e *Enricher) Run(ctx context.Context) (int, error) { + if e == nil || e.pool == nil || e.chainRepo == nil { + return 0, nil + } + + items, err := e.claimBatch(ctx) + if err != nil { + return 0, fmt.Errorf("ebook enrichment: claim batch: %w", err) + } + if len(items) == 0 { + return 0, nil + } + + slog.Info("ebook enrichment: sweep started", + "count", len(items), + "workers", e.workers, + ) + + enriched := e.runBatch(ctx, items, e.enrichItem, e.recordEnrichFailure) + + slog.Info("ebook enrichment: sweep complete", + "attempted", len(items), + "enriched", enriched, + ) + return enriched, nil +} + +func (e *Enricher) runBatch( + ctx context.Context, + items []enrichmentItemRow, + enrichFn func(context.Context, enrichmentItemRow) error, + recordFailure func(context.Context, enrichmentItemRow), +) int { + workers := e.workers + if workers <= 0 { + workers = 1 + } + if workers > len(items) { + workers = len(items) + } + + ch := make(chan enrichmentItemRow, workers) + var ( + wg sync.WaitGroup + enriched int64 + ) + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for item := range ch { + if ctx.Err() != nil { + continue + } + if err := enrichFn(ctx, item); err != nil { + if errors.Is(err, errEnrichmentSkipped) { + slog.Debug("ebook enrichment: item skipped", + "content_id", item.ContentID, + "title", item.Title, + "reason", err, + ) + continue + } + slog.Warn("ebook enrichment: item failed", + "content_id", item.ContentID, + "title", item.Title, + "error", err, + ) + // A cancelled sweep says nothing about the item itself, + // so it does not count against the failure cap. + if recordFailure != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { + recordFailure(ctx, item) + } + continue + } + atomic.AddInt64(&enriched, 1) + } + }() + } + for _, item := range items { + if ctx.Err() != nil { + break + } + ch <- item + } + close(ch) + wg.Wait() + return int(enriched) +} + +// claimBatchQuery selects unenriched ebooks. Items with fewer prior failures +// are claimed first and items at/above enrichFailureCap are skipped entirely, +// so a block of permanently failing items cannot occupy every sweep. +const claimBatchQuery = ` + SELECT + mi.content_id, + mi.title, + mi.year, + COALESCE(mil.media_folder_id, 0) AS folder_id, + COALESCE(mf.metadata_language, 'en') AS language, + COALESCE( + (SELECT p.name + FROM item_people ip + JOIN people p ON p.id = ip.person_id + WHERE ip.content_id = mi.content_id + AND ip.kind = 7 + ORDER BY ip.sort_order, ip.id + LIMIT 1), + '' + ) AS author + FROM media_items mi + LEFT JOIN media_item_libraries mil ON mil.content_id = mi.content_id + LEFT JOIN media_folders mf ON mf.id = mil.media_folder_id + LEFT JOIN ebook_enrichment_state ees ON ees.content_id = mi.content_id + WHERE mi.type = 'ebook' + AND (mi.poster_path IS NULL OR mi.poster_path = '') + AND mi.last_refreshed IS NULL + AND COALESCE(ees.failures, 0) < $2 + ORDER BY COALESCE(ees.failures, 0) ASC, mi.created_at ASC + LIMIT $1 +` + +func (e *Enricher) claimBatch(ctx context.Context) ([]enrichmentItemRow, error) { + rows, err := e.pool.Query(ctx, claimBatchQuery, e.batchSize, enrichFailureCap) + if err != nil { + return nil, fmt.Errorf("querying unenriched ebooks: %w", err) + } + defer rows.Close() + + var items []enrichmentItemRow + seen := make(map[string]struct{}) + for rows.Next() { + var item enrichmentItemRow + if err := rows.Scan( + &item.ContentID, + &item.Title, + &item.Year, + &item.FolderID, + &item.Language, + &item.Author, + ); err != nil { + return nil, fmt.Errorf("scanning ebook enrichment row: %w", err) + } + if _, dup := seen[item.ContentID]; dup { + continue + } + seen[item.ContentID] = struct{}{} + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterating ebook enrichment rows: %w", err) + } + + if e.providerIDs != nil { + for i := range items { + pids, err := e.providerIDs.GetByContentID(ctx, items[i].ContentID) + if err == nil { + items[i].ProviderIDs = providerIDMapFromRows(pids) + } + } + } + + return items, nil +} + +func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error { + if item.FolderID == 0 { + // The scanner inserts the library membership after the item upsert, so + // a freshly indexed ebook can be claimed inside that window. Skip it: + // stamping here would terminally mark the item refreshed before any + // provider ever saw it. + return fmt.Errorf("%w: item %s has no library folder yet", errEnrichmentSkipped, item.ContentID) + } + + providers, err := metadata.ResolveChain(ctx, item.FolderID, ebookContentType(), e.chainRepo, e.resolver) + if err != nil { + return fmt.Errorf("resolving ebook chain for folder %d: %w", item.FolderID, err) + } + return e.enrichWithProviders(ctx, item, providers) +} + +// enrichWithProviders runs the provider chain for one claimed item. Outcomes: +// - metadata obtained: persist it and stamp last_refreshed (nil error); +// - providers answered but nothing matched: stamp last_refreshed so the +// item is not re-claimed every sweep (nil error); +// - one or more providers errored and no metadata was obtained: return an +// error so the failure cap/backoff engages, without stamping; +// - no providers configured: skip (no stamp, no failure) so the item is +// retried once a chain exists. +func (e *Enricher) enrichWithProviders(ctx context.Context, item enrichmentItemRow, providers []metadata.Provider) error { + if len(providers) == 0 { + return fmt.Errorf("%w: no metadata providers configured for folder %d", errEnrichmentSkipped, item.FolderID) + } + + accumulator, accumulatedIDs, providerErrs := collectEbookMetadata(ctx, item, providers) + + if !accumulator.HasMetadata && accumulator.PosterPath == "" && accumulator.Overview == "" { + if err := ctx.Err(); err != nil { + // A cancelled sweep says nothing about the item or the providers. + return err + } + if len(providerErrs) > 0 { + // Transient provider trouble must not stamp the item terminally; + // surfacing an error engages the failure cap and backoff instead. + return fmt.Errorf("no metadata obtained, %d provider error(s): %w", + len(providerErrs), errors.Join(providerErrs...)) + } + slog.Info("ebook enrichment: no metadata found", + "content_id", item.ContentID, + "title", item.Title, + ) + return e.stampLastRefreshed(ctx, item.ContentID) + } + + e.cacheRemotePoster(ctx, item.ContentID, accumulator) + + if err := e.persist(ctx, item.ContentID, accumulatedIDs, accumulator); err != nil { + return fmt.Errorf("persisting enrichment for %s: %w", item.ContentID, err) + } + + slog.Info("ebook enrichment: enriched", + "content_id", item.ContentID, + "title", item.Title, + "poster", accumulator.PosterPath != "", + "overview", accumulator.Overview != "", + "people", len(filterEbookPeople(accumulator.People)), + ) + + return nil +} + +// collectEbookMetadata queries every provider in the chain and accumulates +// IDs and metadata. Individual provider failures are collected (not fatal) so +// the caller can distinguish "providers answered, no match" from "providers +// were unreachable". +func collectEbookMetadata(ctx context.Context, item enrichmentItemRow, providers []metadata.Provider) (*metadata.MetadataResult, map[string]string, []error) { + searchQuery, accumulatedIDs := buildEbookSearchQuery(item) + var providerErrs []error + + for _, p := range providers { + sp, ok := p.(metadata.SearchProvider) + if !ok { + continue + } + results, searchErr := sp.Search(ctx, searchQuery) + if searchErr != nil { + slog.Warn("ebook enrichment: search error", + "provider", p.Slug(), + "content_id", item.ContentID, + "error", searchErr, + ) + providerErrs = append(providerErrs, fmt.Errorf("%s search: %w", p.Slug(), searchErr)) + continue + } + if len(results) == 0 { + continue + } + for k, v := range results[0].ProviderIDs { + if v != "" { + if _, exists := accumulatedIDs[k]; !exists { + accumulatedIDs[k] = v + } + } + } + slog.Debug("ebook enrichment: search result", + "provider", p.Slug(), + "content_id", item.ContentID, + "matched_ids", accumulatedIDs, + ) + } + + accumulator := &metadata.MetadataResult{ + ProviderIDs: accumulatedIDs, + } + + for _, p := range providers { + mp, ok := p.(metadata.MetadataProvider) + if !ok { + continue + } + result, getErr := mp.GetMetadata(ctx, buildEbookMetadataRequest(accumulator.ProviderIDs, item.Language)) + if getErr != nil { + slog.Warn("ebook enrichment: GetMetadata error", + "provider", p.Slug(), + "content_id", item.ContentID, + "error", getErr, + ) + providerErrs = append(providerErrs, fmt.Errorf("%s metadata: %w", p.Slug(), getErr)) + continue + } + if result == nil || !result.HasMetadata { + continue + } + mergeEnrichmentProviderIDs(accumulator, result) + metadata.MergeMetadata(result, accumulator, nil, metadata.MergeFillEmpty) + + slog.Debug("ebook enrichment: metadata received", + "provider", p.Slug(), + "content_id", item.ContentID, + "has_poster", result.PosterPath != "", + "has_overview", result.Overview != "", + ) + } + + return accumulator, accumulator.ProviderIDs, providerErrs +} + +func (e *Enricher) cacheRemotePoster(ctx context.Context, contentID string, result *metadata.MetadataResult) { + if e == nil || result == nil || result.PosterPath == "" { + return + } + if !strings.HasPrefix(result.PosterPath, "http://") && !strings.HasPrefix(result.PosterPath, "https://") { + return + } + if isNilImageCacher(e.imageCacher) { + return + } + + cached, err := e.imageCacher.CacheImage(ctx, metadata.CacheImageRequest{ + SourceURL: result.PosterPath, + ProviderID: ebookMetadataImageProviderID, + ContentType: "ebooks", + ContentID: contentID, + ImageType: metadata.ImagePoster, + }) + if err != nil { + slog.Warn("ebook enrichment: poster cache failed, keeping provider URL", + "content_id", contentID, + "url", result.PosterPath, + "error", err, + ) + return + } + if cached == nil { + slog.Warn("ebook enrichment: poster cache returned no result, keeping provider URL", + "content_id", contentID, + "url", result.PosterPath, + ) + return + } + + if storedPath := cachedOriginalImagePath(cached.BasePath, cached.Ext); storedPath != "" { + result.PosterPath = storedPath + } + if cached.Thumbhash != "" { + result.PosterThumbhash = cached.Thumbhash + } +} + +func isNilImageCacher(cacher metadata.ImageCacher) bool { + if cacher == nil { + return true + } + value := reflect.ValueOf(cacher) + switch value.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return value.IsNil() + default: + return false + } +} + +func cachedOriginalImagePath(basePath, ext string) string { + if basePath == "" { + return "" + } + if strings.Contains(basePath, "/original.") { + return basePath + } + if ext == "" { + ext = ".jpg" + } + return strings.TrimRight(basePath, "/") + "/original" + ext +} + +func (e *Enricher) persist(ctx context.Context, contentID string, providerIDs map[string]string, result *metadata.MetadataResult) error { + upd := &catalog.MetadataUpdate{} + + if result.PosterPath != "" { + upd.PosterPath = &result.PosterPath + } + if result.PosterThumbhash != "" { + upd.PosterThumbhash = &result.PosterThumbhash + } + if result.BackdropPath != "" { + upd.BackdropPath = &result.BackdropPath + } + if result.BackdropThumbhash != "" { + upd.BackdropThumbhash = &result.BackdropThumbhash + } + if result.LogoPath != "" { + upd.LogoPath = &result.LogoPath + } + if result.Overview != "" { + upd.Overview = &result.Overview + } + if result.Tagline != "" { + upd.Tagline = &result.Tagline + } + if result.ReleaseDate != "" { + upd.ReleaseDate = &result.ReleaseDate + } + if len(result.Genres) > 0 { + genres := append([]string(nil), result.Genres...) + upd.Genres = &genres + } + if len(result.Studios) > 0 { + studios := append([]string(nil), result.Studios...) + upd.Studios = &studios + } + if result.ContentRating != "" { + upd.ContentRating = &result.ContentRating + } + if result.Runtime > 0 { + upd.Runtime = &result.Runtime + } + if result.Year > 0 { + upd.Year = &result.Year + } + + providerIDs = filterEbookProviderIDs(providerIDs) + if e.providerIDs != nil && len(providerIDs) > 0 { + if err := e.providerIDs.ReplaceByContentID(ctx, contentID, providerIDs); err != nil { + slog.Warn("ebook enrichment: failed to persist provider IDs", + "content_id", contentID, + "error", err, + ) + } + } + + if err := e.updateMetadataAndTimestamps(ctx, contentID, upd); err != nil { + return err + } + + authors := filterEbookPeople(result.People) + if len(authors) > 0 && e.personRepo != nil && e.itemRepo != nil { + if err := e.persistPeople(ctx, contentID, authors); err != nil { + slog.Warn("ebook enrichment: failed to persist people", + "content_id", contentID, + "error", err, + ) + } + } + + return nil +} + +func (e *Enricher) updateMetadataAndTimestamps(ctx context.Context, contentID string, upd *catalog.MetadataUpdate) error { + if e.itemRepo == nil { + return nil + } + if err := e.itemRepo.UpdateMetadata(ctx, contentID, upd); err != nil { + return fmt.Errorf("UpdateMetadata: %w", err) + } + return e.stampLastRefreshed(ctx, contentID) +} + +func (e *Enricher) stampLastRefreshed(ctx context.Context, contentID string) error { + if e.pool == nil { + return nil + } + now := time.Now().UTC() + if _, err := e.pool.Exec(ctx, ` + UPDATE media_items + SET last_refreshed = $1, + matched_at = COALESCE(matched_at, $1), + status = CASE WHEN status = 'pending' THEN 'matched' ELSE status END + WHERE content_id = $2 + `, now, contentID); err != nil { + return err + } + // Success clears the enrichment failure backlog. media_items.refresh_failures + // is intentionally left alone: it belongs to the metadata refresh-debt system. + _, err := e.pool.Exec(ctx, ` + DELETE FROM ebook_enrichment_state WHERE content_id = $1 + `, contentID) + return err +} + +// recordEnrichFailure increments the item's ebook_enrichment_state failure +// counter so claimBatch deprioritizes it on the next sweep and stops claiming +// it at enrichFailureCap. The state is dedicated to ebook enrichment; +// media_items.refresh_failures is owned by the metadata refresh-debt system +// and is never touched here. +func (e *Enricher) recordEnrichFailure(ctx context.Context, item enrichmentItemRow) { + if e == nil || e.pool == nil { + return + } + if _, err := e.pool.Exec(ctx, ` + INSERT INTO ebook_enrichment_state (content_id, failures, updated_at) + VALUES ($1, 1, NOW()) + ON CONFLICT (content_id) DO UPDATE SET + failures = ebook_enrichment_state.failures + 1, + updated_at = NOW() + `, item.ContentID); err != nil { + slog.Warn("ebook enrichment: failed to record enrichment failure", + "content_id", item.ContentID, + "error", err, + ) + } +} + +func (e *Enricher) persistPeople(ctx context.Context, contentID string, people []models.ItemPerson) error { + people = filterEbookPeople(people) + if len(people) == 0 { + return nil + } + + persons := make([]models.Person, len(people)) + for i := range people { + persons[i] = people[i].Person + } + + personIDs, err := e.personRepo.BatchFindOrCreate(ctx, persons) + if err != nil { + return fmt.Errorf("BatchFindOrCreate people: %w", err) + } + + linked := make([]models.ItemPerson, 0, len(people)) + for i := range people { + if i >= len(personIDs) || personIDs[i] == 0 { + continue + } + ip := people[i] + ip.Person.ID = personIDs[i] + linked = append(linked, ip) + } + + if len(linked) == 0 { + return nil + } + + existing, err := e.itemRepo.GetPeople(ctx, contentID) + if err != nil { + return fmt.Errorf("get existing people: %w", err) + } + return e.itemRepo.ReplacePeople(ctx, contentID, mergeEbookAuthorCredits(existing, linked)) +} + +// mergeEbookAuthorCredits mirrors the scanner's mergeEbookPeople semantics: +// the provider authors replace existing author (and stale narrator) credits, +// while every other curated people kind on the item is preserved. +func mergeEbookAuthorCredits(existing []models.ItemPerson, authors []models.ItemPerson) []models.ItemPerson { + merged := make([]models.ItemPerson, 0, len(existing)+len(authors)) + for _, p := range existing { + if p.Kind == models.PersonKindAuthor || p.Kind == models.PersonKindNarrator { + continue + } + p.SortOrder = len(merged) + merged = append(merged, p) + } + for _, a := range authors { + a.SortOrder = len(merged) + merged = append(merged, a) + } + return merged +} + +func filterEbookPeople(people []models.ItemPerson) []models.ItemPerson { + authors := make([]models.ItemPerson, 0, len(people)) + for _, person := range people { + if person.Kind != models.PersonKindAuthor { + continue + } + person.SortOrder = len(authors) + authors = append(authors, person) + } + return authors +} + +func buildEbookSearchQuery(item enrichmentItemRow) (metadata.SearchQuery, map[string]string) { + accumulatedIDs := filterEbookProviderIDs(item.ProviderIDs) + if accumulatedIDs == nil { + accumulatedIDs = map[string]string{} + } + return metadata.SearchQuery{ + Title: item.Title, + Year: item.Year, + ContentType: ebookContentType(), + ProviderIDs: accumulatedIDs, + Language: item.Language, + }, accumulatedIDs +} + +func buildEbookMetadataRequest(providerIDs map[string]string, language string) metadata.MetadataRequest { + return metadata.MetadataRequest{ + ProviderIDs: filterEbookProviderIDs(providerIDs), + ContentType: ebookContentType(), + Language: language, + } +} + +func mergeEnrichmentProviderIDs(dst *metadata.MetadataResult, src *metadata.MetadataResult) { + if src == nil || len(src.ProviderIDs) == 0 { + return + } + if dst.ProviderIDs == nil { + dst.ProviderIDs = make(map[string]string, len(src.ProviderIDs)) + } + for k, v := range filterEbookProviderIDs(src.ProviderIDs) { + if v != "" { + if _, exists := dst.ProviderIDs[k]; !exists { + dst.ProviderIDs[k] = v + } + } + } +} + +func filterEbookProviderIDs(providerIDs map[string]string) map[string]string { + if len(providerIDs) == 0 { + return nil + } + filtered := make(map[string]string, len(providerIDs)) + for provider, providerID := range providerIDs { + provider = strings.TrimSpace(provider) + providerID = strings.TrimSpace(providerID) + if provider == "" || providerID == "" { + continue + } + provider = strings.ToLower(provider) + if isEbookASINProvider(provider) { + continue + } + filtered[provider] = providerID + } + if len(filtered) == 0 { + return nil + } + return filtered +} + +func isEbookASINProvider(provider string) bool { + normalized := strings.ReplaceAll(strings.ReplaceAll(provider, "_", ""), "-", "") + return normalized == "asin" || normalized == "audibleasin" +} + +func providerIDMapFromRows(rows []*models.MediaItemProviderID) map[string]string { + if len(rows) == 0 { + return nil + } + m := make(map[string]string, len(rows)) + for _, r := range rows { + if r != nil { + for provider, providerID := range filterEbookProviderIDs(map[string]string{ + r.Provider: r.ProviderID, + }) { + m[provider] = providerID + } + } + } + return m +} diff --git a/internal/ebooks/enrichment_test.go b/internal/ebooks/enrichment_test.go new file mode 100644 index 00000000..571ec23b --- /dev/null +++ b/internal/ebooks/enrichment_test.go @@ -0,0 +1,573 @@ +package ebooks + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/metadata" + "github.com/Silo-Server/silo-server/internal/models" +) + +func TestEbookContentType(t *testing.T) { + if got := ebookContentType(); got != "ebook" { + t.Fatalf("ebookContentType() = %q, want ebook", got) + } +} + +func TestFilterEbookPeopleKeepsAuthorsOnly(t *testing.T) { + people := []models.ItemPerson{ + {Person: models.Person{Name: "Author One"}, Kind: models.PersonKindAuthor, SortOrder: 7}, + {Person: models.Person{Name: "Narrator One"}, Kind: models.PersonKindNarrator, SortOrder: 8}, + {Person: models.Person{Name: "Writer One"}, Kind: models.PersonKindWriter, SortOrder: 9}, + {Person: models.Person{Name: "Author Two"}, Kind: models.PersonKindAuthor, SortOrder: 10}, + } + + got := filterEbookPeople(people) + + if len(got) != 2 { + t.Fatalf("filtered people len = %d, want 2: %+v", len(got), got) + } + for i, p := range got { + if p.Kind != models.PersonKindAuthor { + t.Fatalf("filtered[%d].Kind = %v, want author", i, p.Kind) + } + if p.SortOrder != i { + t.Fatalf("filtered[%d].SortOrder = %d, want %d", i, p.SortOrder, i) + } + } + if got[0].Person.Name != "Author One" || got[1].Person.Name != "Author Two" { + t.Fatalf("filtered author order = %+v", got) + } +} + +func TestEbookEnrichWorkersFromEnv(t *testing.T) { + t.Setenv("SILO_EBOOK_ENRICH_WORKERS", "12") + if got := ebookEnrichWorkers(); got != 12 { + t.Fatalf("ebookEnrichWorkers() = %d, want 12", got) + } + + t.Setenv("SILO_EBOOK_ENRICH_WORKERS", "0") + if got := ebookEnrichWorkers(); got != defaultEnrichWorkers { + t.Fatalf("ebookEnrichWorkers() with zero = %d, want default %d", got, defaultEnrichWorkers) + } + + t.Setenv("SILO_EBOOK_ENRICH_WORKERS", "999") + if got := ebookEnrichWorkers(); got != defaultEnrichBatchSize { + t.Fatalf("ebookEnrichWorkers() capped = %d, want %d", got, defaultEnrichBatchSize) + } +} + +func TestEnricherRunFansOut(t *testing.T) { + const wantWorkers = 4 + const itemCount = 16 + + items := make([]enrichmentItemRow, itemCount) + for i := range items { + items[i] = enrichmentItemRow{ContentID: "test", Title: "t"} + } + + var inFlight int32 + var maxInFlight int32 + var signaled int32 + var wg sync.WaitGroup + wg.Add(wantWorkers) + gate := make(chan struct{}) + + enrich := func(ctx context.Context, item enrichmentItemRow) error { + cur := atomic.AddInt32(&inFlight, 1) + defer atomic.AddInt32(&inFlight, -1) + for { + prev := atomic.LoadInt32(&maxInFlight) + if cur <= prev || atomic.CompareAndSwapInt32(&maxInFlight, prev, cur) { + break + } + } + if atomic.AddInt32(&signaled, 1) <= wantWorkers { + wg.Done() + } + select { + case <-gate: + case <-time.After(2 * time.Second): + } + return nil + } + + go func() { + wg.Wait() + close(gate) + }() + + e := &Enricher{workers: wantWorkers, batchSize: itemCount} + e.runBatch(context.Background(), items, enrich, nil) + + if got := atomic.LoadInt32(&maxInFlight); got < wantWorkers { + t.Errorf("max in-flight = %d, want >= %d", got, wantWorkers) + } +} + +func TestRunBatchRecordsFailuresForFailedItemsOnly(t *testing.T) { + items := []enrichmentItemRow{ + {ContentID: "ok-1"}, + {ContentID: "bad-1"}, + {ContentID: "ok-2"}, + {ContentID: "bad-2"}, + } + + enrich := func(_ context.Context, item enrichmentItemRow) error { + if strings.HasPrefix(item.ContentID, "bad") { + return errors.New("provider exploded") + } + return nil + } + + var mu sync.Mutex + var recorded []string + record := func(_ context.Context, item enrichmentItemRow) { + mu.Lock() + defer mu.Unlock() + recorded = append(recorded, item.ContentID) + } + + e := &Enricher{workers: 2, batchSize: len(items)} + enriched := e.runBatch(context.Background(), items, enrich, record) + + if enriched != 2 { + t.Fatalf("enriched = %d, want 2", enriched) + } + sort.Strings(recorded) + if strings.Join(recorded, ",") != "bad-1,bad-2" { + t.Fatalf("recorded failures = %v, want exactly the failing items", recorded) + } +} + +func TestRunBatchSkipsFailureRecordingOnCancellation(t *testing.T) { + items := []enrichmentItemRow{{ContentID: "cancelled-1"}} + + enrich := func(context.Context, enrichmentItemRow) error { + return fmt.Errorf("search aborted: %w", context.Canceled) + } + + var recorded int32 + record := func(context.Context, enrichmentItemRow) { + atomic.AddInt32(&recorded, 1) + } + + e := &Enricher{workers: 1, batchSize: len(items)} + if enriched := e.runBatch(context.Background(), items, enrich, record); enriched != 0 { + t.Fatalf("enriched = %d, want 0", enriched) + } + if got := atomic.LoadInt32(&recorded); got != 0 { + t.Fatalf("failure recordings = %d, want 0: cancellation must not count against the cap", got) + } +} + +func TestClaimBatchQueryAppliesFailureBackoffAndCap(t *testing.T) { + // Pin the starvation guard: failing items are deprioritized, not retried + // at the head of every sweep, and capped items are never re-claimed. + if !strings.Contains(claimBatchQuery, "LEFT JOIN ebook_enrichment_state ees ON ees.content_id = mi.content_id") { + t.Fatalf("claimBatchQuery must read dedicated ebook enrichment failure state:\n%s", claimBatchQuery) + } + if !strings.Contains(claimBatchQuery, "COALESCE(ees.failures, 0) < $2") { + t.Fatalf("claimBatchQuery must exclude items at the failure cap:\n%s", claimBatchQuery) + } + if !strings.Contains(claimBatchQuery, "ORDER BY COALESCE(ees.failures, 0) ASC, mi.created_at ASC") { + t.Fatalf("claimBatchQuery must claim least-failed items first:\n%s", claimBatchQuery) + } + if strings.Contains(claimBatchQuery, "refresh_failures") { + t.Fatalf("claimBatchQuery must not read media_items.refresh_failures (owned by the metadata refresh-debt system):\n%s", claimBatchQuery) + } + if enrichFailureCap < 1 { + t.Fatalf("enrichFailureCap = %d, want >= 1", enrichFailureCap) + } +} + +func TestEnrichItemSkipsItemWithoutLibraryFolder(t *testing.T) { + // Membership rows are inserted after the item upsert, so a scan-window + // race can claim an item before its folder link exists. The item must be + // skipped (retried next sweep), never stamped or counted as a failure. + e := &Enricher{} + err := e.enrichItem(context.Background(), enrichmentItemRow{ContentID: "no-folder", Title: "t"}) + if !errors.Is(err, errEnrichmentSkipped) { + t.Fatalf("enrichItem error = %v, want errEnrichmentSkipped", err) + } +} + +func TestEnrichWithProvidersSkipsWhenNoProvidersConfigured(t *testing.T) { + e := &Enricher{} + err := e.enrichWithProviders(context.Background(), enrichmentItemRow{ContentID: "c1", FolderID: 7}, nil) + if !errors.Is(err, errEnrichmentSkipped) { + t.Fatalf("enrichWithProviders error = %v, want errEnrichmentSkipped", err) + } +} + +func TestEnrichWithProvidersReturnsFailureWhenAllProvidersError(t *testing.T) { + providerErr := errors.New("provider exploded") + providers := []metadata.Provider{ + &fakeEbookMetadataProvider{slug: "p1", searchErr: providerErr, getErr: providerErr}, + &fakeEbookMetadataProvider{slug: "p2", searchErr: providerErr, getErr: providerErr}, + } + + e := &Enricher{} + err := e.enrichWithProviders(context.Background(), enrichmentItemRow{ContentID: "c1", FolderID: 7, Title: "t"}, providers) + if err == nil { + t.Fatal("enrichWithProviders = nil, want error so the failure cap engages instead of stamping") + } + if errors.Is(err, errEnrichmentSkipped) { + t.Fatalf("enrichWithProviders error = %v, want a recordable failure, not a skip", err) + } + if !errors.Is(err, providerErr) { + t.Fatalf("enrichWithProviders error = %v, want wrapped provider error", err) + } +} + +func TestEnrichWithProvidersStampsWhenProvidersAnswerWithNoMatch(t *testing.T) { + // Providers reachable, genuinely nothing found: nil means the no-match + // path ran and the item was stamped so it is not re-claimed every sweep. + providers := []metadata.Provider{ + &fakeEbookMetadataProvider{slug: "p1"}, + } + + e := &Enricher{} + err := e.enrichWithProviders(context.Background(), enrichmentItemRow{ContentID: "c1", FolderID: 7, Title: "t"}, providers) + if err != nil { + t.Fatalf("enrichWithProviders = %v, want nil for a genuine no-match", err) + } +} + +func TestEnrichWithProvidersReturnsContextErrorOverProviderFailure(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + providers := []metadata.Provider{ + &fakeEbookMetadataProvider{slug: "p1", searchErr: ctx.Err(), getErr: ctx.Err()}, + } + + e := &Enricher{} + err := e.enrichWithProviders(ctx, enrichmentItemRow{ContentID: "c1", FolderID: 7}, providers) + if !errors.Is(err, context.Canceled) { + t.Fatalf("enrichWithProviders error = %v, want context.Canceled so cancellation never counts against the cap", err) + } +} + +func TestCollectEbookMetadataAccumulatesProviderErrors(t *testing.T) { + searchErr := errors.New("search down") + getErr := errors.New("metadata down") + providers := []metadata.Provider{ + &fakeEbookMetadataProvider{slug: "broken", searchErr: searchErr, getErr: getErr}, + &fakeEbookMetadataProvider{ + slug: "working", + results: []metadata.SearchResult{{ProviderIDs: map[string]string{"openlibrary": "OL1M"}}}, + result: &metadata.MetadataResult{HasMetadata: true, Overview: "found"}, + }, + } + + accumulator, ids, errs := collectEbookMetadata(context.Background(), enrichmentItemRow{ContentID: "c1", Title: "t"}, providers) + + if len(errs) != 2 || !errors.Is(errs[0], searchErr) || !errors.Is(errs[1], getErr) { + t.Fatalf("provider errors = %v, want both broken-provider errors", errs) + } + if accumulator.Overview != "found" { + t.Fatalf("accumulator overview = %q, want metadata from the working provider", accumulator.Overview) + } + if ids["openlibrary"] != "OL1M" { + t.Fatalf("accumulated IDs = %v, want search-result openlibrary ID", ids) + } +} + +func TestRunBatchDoesNotRecordFailuresForSkippedItems(t *testing.T) { + items := []enrichmentItemRow{{ContentID: "skipped-1"}} + + enrich := func(context.Context, enrichmentItemRow) error { + return fmt.Errorf("%w: no providers", errEnrichmentSkipped) + } + + var recorded int32 + record := func(context.Context, enrichmentItemRow) { + atomic.AddInt32(&recorded, 1) + } + + e := &Enricher{workers: 1, batchSize: len(items)} + if enriched := e.runBatch(context.Background(), items, enrich, record); enriched != 0 { + t.Fatalf("enriched = %d, want 0", enriched) + } + if got := atomic.LoadInt32(&recorded); got != 0 { + t.Fatalf("failure recordings = %d, want 0: skips must not count against the cap", got) + } +} + +func TestMergeEbookAuthorCreditsPreservesOtherPeopleKinds(t *testing.T) { + existing := []models.ItemPerson{ + {Person: models.Person{ID: 10, Name: "Old Author"}, Kind: models.PersonKindAuthor, SortOrder: 0}, + {Person: models.Person{ID: 20, Name: "Manual Writer"}, Kind: models.PersonKindWriter, SortOrder: 1, Character: "essay"}, + {Person: models.Person{ID: 30, Name: "Stale Narrator"}, Kind: models.PersonKindNarrator, SortOrder: 2}, + } + authors := []models.ItemPerson{ + {Person: models.Person{ID: 40, Name: "Provider Author"}, Kind: models.PersonKindAuthor}, + } + + got := mergeEbookAuthorCredits(existing, authors) + + if len(got) != 2 { + t.Fatalf("merged people len = %d, want 2: %+v", len(got), got) + } + if got[0].Person.ID != 20 || got[0].Kind != models.PersonKindWriter || got[0].Character != "essay" || got[0].SortOrder != 0 { + t.Fatalf("preserved non-author credit = %+v", got[0]) + } + if got[1].Person.ID != 40 || got[1].Kind != models.PersonKindAuthor || got[1].SortOrder != 1 { + t.Fatalf("provider author credit = %+v", got[1]) + } +} + +func TestCacheRemotePosterCachesProviderURL(t *testing.T) { + cacher := &fakeEbookImageCacher{} + e := &Enricher{imageCacher: cacher} + result := &metadata.MetadataResult{ + PosterPath: "https://example.test/book.jpg", + } + + e.cacheRemotePoster(context.Background(), "content-1", result) + + if cacher.calls != 1 { + t.Fatalf("CacheImage calls = %d, want 1", cacher.calls) + } + if cacher.req.ProviderID != ebookMetadataImageProviderID { + t.Fatalf("ProviderID = %q, want %q", cacher.req.ProviderID, ebookMetadataImageProviderID) + } + if cacher.req.ContentType != "ebooks" || cacher.req.ContentID != "content-1" { + t.Fatalf("cache target = %q/%q", cacher.req.ContentType, cacher.req.ContentID) + } + if result.PosterPath != "ebook-metadata/ebooks/content-1/poster/original.webp" { + t.Fatalf("PosterPath = %q", result.PosterPath) + } + if result.PosterThumbhash != "thumb" { + t.Fatalf("PosterThumbhash = %q", result.PosterThumbhash) + } +} + +func TestCacheRemotePosterSkipsNilCacher(t *testing.T) { + e := &Enricher{} + result := &metadata.MetadataResult{ + PosterPath: "https://example.test/book.jpg", + } + + e.cacheRemotePoster(context.Background(), "content-1", result) + + if result.PosterPath != "https://example.test/book.jpg" { + t.Fatalf("PosterPath = %q, want provider URL preserved", result.PosterPath) + } +} + +func TestCacheRemotePosterSkipsTypedNilCacher(t *testing.T) { + var cacher *fakeEbookImageCacher + e := &Enricher{imageCacher: cacher} + result := &metadata.MetadataResult{ + PosterPath: "https://example.test/book.jpg", + } + + e.cacheRemotePoster(context.Background(), "content-1", result) + + if result.PosterPath != "https://example.test/book.jpg" { + t.Fatalf("PosterPath = %q, want provider URL preserved", result.PosterPath) + } +} + +func TestCacheRemotePosterSkipsAlreadyCachedPath(t *testing.T) { + cacher := &fakeEbookImageCacher{} + e := &Enricher{imageCacher: cacher} + result := &metadata.MetadataResult{ + PosterPath: "local/ebooks/content-1/poster/original.webp", + } + + e.cacheRemotePoster(context.Background(), "content-1", result) + + if cacher.calls != 0 { + t.Fatalf("CacheImage calls = %d, want 0", cacher.calls) + } + if result.PosterPath != "local/ebooks/content-1/poster/original.webp" { + t.Fatalf("PosterPath = %q", result.PosterPath) + } +} + +func TestCacheRemotePosterPreservesProviderURLOnCacheError(t *testing.T) { + cacher := &fakeEbookImageCacher{err: errors.New("cache failed")} + e := &Enricher{imageCacher: cacher} + result := &metadata.MetadataResult{ + PosterPath: "https://example.test/book.jpg", + } + + e.cacheRemotePoster(context.Background(), "content-1", result) + + if cacher.calls != 1 { + t.Fatalf("CacheImage calls = %d, want 1", cacher.calls) + } + if result.PosterPath != "https://example.test/book.jpg" { + t.Fatalf("PosterPath = %q, want provider URL preserved", result.PosterPath) + } +} + +func TestCacheRemotePosterPreservesProviderURLOnNilCacheResult(t *testing.T) { + cacher := &fakeEbookImageCacher{returnNil: true} + e := &Enricher{imageCacher: cacher} + result := &metadata.MetadataResult{ + PosterPath: "https://example.test/book.jpg", + } + + e.cacheRemotePoster(context.Background(), "content-1", result) + + if cacher.calls != 1 { + t.Fatalf("CacheImage calls = %d, want 1", cacher.calls) + } + if result.PosterPath != "https://example.test/book.jpg" { + t.Fatalf("PosterPath = %q, want provider URL preserved", result.PosterPath) + } +} + +func TestMergeEnrichmentProviderIDsKeepsExistingIDs(t *testing.T) { + dst := &metadata.MetadataResult{ProviderIDs: map[string]string{"isbn": "9780306406157"}} + src := &metadata.MetadataResult{ProviderIDs: map[string]string{"isbn": "new", "openlibrary": "OL1M", "empty": ""}} + + mergeEnrichmentProviderIDs(dst, src) + + if got := dst.ProviderIDs["isbn"]; got != "9780306406157" { + t.Fatalf("isbn = %q, want original", got) + } + if got := dst.ProviderIDs["openlibrary"]; got != "OL1M" { + t.Fatalf("openlibrary = %q, want OL1M", got) + } + if _, exists := dst.ProviderIDs["empty"]; exists { + t.Fatal("empty provider ID should not be merged") + } +} + +func TestMergeEnrichmentProviderIDsDropsAsinIDs(t *testing.T) { + dst := &metadata.MetadataResult{ProviderIDs: map[string]string{"isbn": "9780306406157"}} + src := &metadata.MetadataResult{ + ProviderIDs: map[string]string{ + "ASIN": "B00TEST", + "audible_asin": "B00AUDIO", + "openlibrary": "OL1M", + }, + } + + mergeEnrichmentProviderIDs(dst, src) + + if _, exists := dst.ProviderIDs["ASIN"]; exists { + t.Fatal("ASIN provider ID should not be merged for ebooks") + } + if _, exists := dst.ProviderIDs["audible_asin"]; exists { + t.Fatal("audible_asin provider ID should not be merged for ebooks") + } + if got := dst.ProviderIDs["openlibrary"]; got != "OL1M" { + t.Fatalf("openlibrary = %q, want OL1M", got) + } +} + +func TestBuildEbookSearchQueryUsesFilteredScannerISBN(t *testing.T) { + item := enrichmentItemRow{ + Title: "Tagged Ebook", + Year: 2024, + Language: "en", + ProviderIDs: map[string]string{ + " ISBN ": " 9780306406157 ", + "ASIN": "B00TEST", + "audible_asin": "B00AUDIO", + }, + } + + query, ids := buildEbookSearchQuery(item) + + if query.Title != "Tagged Ebook" || query.Year != 2024 || query.Language != "en" || query.ContentType != "ebook" { + t.Fatalf("query basics = %+v", query) + } + if got := query.ProviderIDs["isbn"]; got != "9780306406157" { + t.Fatalf("query isbn = %q, want scanner ISBN", got) + } + if _, exists := query.ProviderIDs["ASIN"]; exists { + t.Fatal("query should not include ASIN for ebooks") + } + if got := ids["isbn"]; got != "9780306406157" { + t.Fatalf("accumulated isbn = %q, want scanner ISBN", got) + } +} + +func TestFilterEbookProviderIDsDropsAsinAliases(t *testing.T) { + got := filterEbookProviderIDs(map[string]string{ + "ASIN": "B00TEST", + "audibleASIN": "B00AUDIO", + "audible-asin": "B00AUDIO2", + "audible_asin": "B00AUDIO3", + " ISBN ": " 9780306406157 ", + " OpenLibraryID": " OL1M ", + }) + + if got["asin"] != "" || got["audibleasin"] != "" || got["audible-asin"] != "" || got["audible_asin"] != "" { + t.Fatalf("ASIN aliases should be filtered, got %+v", got) + } + if got["isbn"] != "9780306406157" || got["openlibraryid"] != "OL1M" { + t.Fatalf("filtered IDs = %+v, want ISBN and OpenLibraryID only", got) + } +} + +func TestBuildEbookMetadataRequestCarriesAccumulatedISBN(t *testing.T) { + req := buildEbookMetadataRequest(map[string]string{ + " ISBN ": " 9780306406157 ", + "openlibrary": "OL1M", + }, "fr") + + if req.ContentType != "ebook" || req.Language != "fr" { + t.Fatalf("request basics = %+v", req) + } + if got := req.ProviderIDs["isbn"]; got != "9780306406157" { + t.Fatalf("request isbn = %q, want normalized key/value", got) + } + if got := req.ProviderIDs["openlibrary"]; got != "OL1M" { + t.Fatalf("request openlibrary = %q, want OL1M", got) + } +} + +type fakeEbookMetadataProvider struct { + slug string + searchErr error + results []metadata.SearchResult + getErr error + result *metadata.MetadataResult +} + +func (f *fakeEbookMetadataProvider) Slug() string { return f.slug } +func (f *fakeEbookMetadataProvider) Name() string { return f.slug } +func (f *fakeEbookMetadataProvider) ForTypes() []string { return []string{"ebook"} } +func (f *fakeEbookMetadataProvider) Search(context.Context, metadata.SearchQuery) ([]metadata.SearchResult, error) { + return f.results, f.searchErr +} +func (f *fakeEbookMetadataProvider) GetMetadata(context.Context, metadata.MetadataRequest) (*metadata.MetadataResult, error) { + return f.result, f.getErr +} + +type fakeEbookImageCacher struct { + calls int + req metadata.CacheImageRequest + err error + returnNil bool +} + +func (f *fakeEbookImageCacher) CacheImage(_ context.Context, req metadata.CacheImageRequest) (*metadata.CacheImageResult, error) { + f.calls++ + f.req = req + if f.err != nil { + return nil, f.err + } + if f.returnNil { + return nil, nil + } + return &metadata.CacheImageResult{ + BasePath: req.ProviderID + "/" + req.ContentType + "/" + req.ContentID + "/poster", + Thumbhash: "thumb", + Ext: ".webp", + }, nil +} diff --git a/internal/historyimport/repo.go b/internal/historyimport/repo.go index 36ce17bd..ad166ea6 100644 --- a/internal/historyimport/repo.go +++ b/internal/historyimport/repo.go @@ -869,8 +869,11 @@ func (r *Repository) UpsertImportedProgress( if positionSeconds < 0 { positionSeconds = 0 } - if completed && durationSeconds > 0 { - positionSeconds = durationSeconds + // Completed rows hold no resume point and `completed` is a one-way watched + // latch — mirrors the userstore write paths so imported rows can't surface + // as phantom Continue Watching entries. + if completed { + positionSeconds = 0 } _, err := r.pool.Exec(ctx, ` INSERT INTO user_watch_progress ( @@ -879,7 +882,7 @@ func (r *Repository) UpsertImportedProgress( ON CONFLICT (user_id, profile_id, media_item_id) DO UPDATE SET position_seconds = excluded.position_seconds, duration_seconds = excluded.duration_seconds, - completed = excluded.completed, + completed = user_watch_progress.completed OR excluded.completed, updated_at = excluded.updated_at`, userID, profileID, mediaItemID, positionSeconds, durationSeconds, completed, updatedAt, ) diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index 79472ad2..f4eb3171 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -114,6 +114,22 @@ func (c *Cacher) CacheAudiobookCover(ctx context.Context, data []byte, contentID return res.BasePath, res.Ext, res.Thumbhash, nil } +// CacheEbookCover stores an embedded ebook cover under +// "local/ebooks/{contentID}/poster/..." using the same poster variants as +// provider-hosted book artwork. +func (c *Cacher) CacheEbookCover(ctx context.Context, data []byte, contentID string) (basePath string, ext string, thumbhash string, err error) { + res, err := c.CacheBytes(ctx, data, CacheRequest{ + ProviderID: "local", + ContentType: "ebooks", + ContentID: contentID, + ImageType: metadata.ImagePoster, + }) + if err != nil { + return "", "", "", err + } + return res.BasePath, res.Ext, res.Thumbhash, nil +} + // CacheBytes performs the same variant generation, thumbhash, and S3 upload as // Cache but starts from raw image bytes already in hand. Used by the // audiobook scanner to push embedded M4B cover art into S3 without round- diff --git a/internal/jellycompat/audiobook_exclusion_test.go b/internal/jellycompat/audiobook_exclusion_test.go new file mode 100644 index 00000000..fec1927a --- /dev/null +++ b/internal/jellycompat/audiobook_exclusion_test.go @@ -0,0 +1,200 @@ +package jellycompat + +import ( + "context" + "net/url" + "testing" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +func TestCompatScopedTypes(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"", compatVideoTypes}, + {"movie", "movie"}, + {"movie,series", "movie,series"}, + {"audiobook", compatNoMatchType}, + {"movie,audiobook", "movie"}, + {" Audiobook ", compatNoMatchType}, + {"podcast", compatNoMatchType}, + {"movie,podcast", "movie"}, + // Non-video types outside the allowlist are dropped, not passed through. + {"musicalbum", compatNoMatchType}, + {"movie,musicalbum", "movie"}, + } + for _, tc := range cases { + if got := compatScopedTypes(tc.in); got != tc.want { + t.Errorf("compatScopedTypes(%q) = %q, want %q", tc.in, got, tc.want) + } + } +} + +func TestCompatScopedSearchTypes(t *testing.T) { + if got := compatScopedSearchTypes(nil); len(got) != 3 { + t.Fatalf("expected video type default, got %v", got) + } + got := compatScopedSearchTypes([]string{"movie", "audiobook"}) + if len(got) != 1 || got[0] != "movie" { + t.Fatalf("expected [movie], got %v", got) + } + got = compatScopedSearchTypes([]string{"audiobook"}) + if len(got) != 1 || got[0] != compatNoMatchType { + t.Fatalf("expected no-match sentinel, got %v", got) + } +} + +func TestIsCompatHiddenLibraryType(t *testing.T) { + for _, val := range []string{"audiobooks", "audiobook", " Audiobooks ", "podcast", "podcasts"} { + if !isCompatHiddenLibraryType(val) { + t.Errorf("expected %q to be hidden from the compat surface", val) + } + } + for _, val := range []string{"movies", "series", "mixed", ""} { + if isCompatHiddenLibraryType(val) { + t.Errorf("expected %q to be visible on the compat surface", val) + } + } +} + +func TestCompatAccessFilterResolverStampsExclusions(t *testing.T) { + resolved := compatAccessFilterResolver(nil)(context.Background(), 1, "p1") + if len(resolved.ExcludedMediaTypes) == 0 { + t.Fatalf("expected exclusions on nil base resolver, got %+v", resolved) + } + + base := func(context.Context, int, string) catalog.AccessFilter { + return catalog.AccessFilter{MaxContentRating: "PG-13"} + } + resolved = compatAccessFilterResolver(base)(context.Background(), 1, "p1") + if resolved.MaxContentRating != "PG-13" { + t.Fatalf("expected base filter fields preserved, got %+v", resolved) + } + for _, want := range []string{"audiobook", "podcast"} { + found := false + for _, got := range resolved.ExcludedMediaTypes { + if got == want { + found = true + } + } + if !found { + t.Fatalf("expected %q in ExcludedMediaTypes, got %v", want, resolved.ExcludedMediaTypes) + } + } + + // Pre-existing exclusions from the base resolver are preserved AND the + // compat exclusions are merged in (not conditionally skipped). + withExisting := func(context.Context, int, string) catalog.AccessFilter { + return catalog.AccessFilter{ExcludedMediaTypes: []string{"livetv"}} + } + resolved = compatAccessFilterResolver(withExisting)(context.Background(), 1, "p1") + for _, want := range []string{"livetv", "audiobook", "podcast"} { + found := false + for _, got := range resolved.ExcludedMediaTypes { + if got == want { + found = true + } + } + if !found { + t.Fatalf("expected %q in merged ExcludedMediaTypes, got %v", want, resolved.ExcludedMediaTypes) + } + } +} + +// scopeFakeFolderRepo returns a fixed folder list. +type scopeFakeFolderRepo struct { + folders []*models.MediaFolder +} + +func (f *scopeFakeFolderRepo) GetEnabled(context.Context) ([]*models.MediaFolder, error) { + return f.folders, nil +} + +func (f *scopeFakeFolderRepo) ListByIDs(_ context.Context, ids []int) ([]*models.MediaFolder, error) { + out := make([]*models.MediaFolder, 0, len(ids)) + for _, folder := range f.folders { + for _, id := range ids { + if folder.ID == id { + out = append(out, folder) + } + } + } + return out, nil +} + +func TestListUserLibrariesExcludesABSFolders(t *testing.T) { + svc := &directContentService{ + folderRepo: &scopeFakeFolderRepo{folders: []*models.MediaFolder{ + {ID: 1, Name: "Movies", Type: "movies", Enabled: true}, + {ID: 2, Name: "Audiobooks", Type: "audiobooks", Enabled: true}, + {ID: 3, Name: "Shows", Type: "series", Enabled: true}, + {ID: 4, Name: "Legacy Books", Type: "audiobook", Enabled: true}, + {ID: 5, Name: "Podcasts", Type: "podcasts", Enabled: true}, + }}, + } + + libraries, err := svc.ListUserLibraries(context.Background(), &Session{StreamAppUserID: 1, ProfileID: "p1"}) + if err != nil { + t.Fatalf("ListUserLibraries: %v", err) + } + if len(libraries) != 2 { + t.Fatalf("expected 2 libraries, got %d: %+v", len(libraries), libraries) + } + for _, lib := range libraries { + if lib.Name != "Movies" && lib.Name != "Shows" { + t.Fatalf("ABS-surface library %q leaked into compat views", lib.Name) + } + } +} + +// scopeFakeBrowseSource captures the filters passed to BrowsePage. +type scopeFakeBrowseSource struct { + lastFilters catalog.BrowseFilters +} + +func (f *scopeFakeBrowseSource) BrowsePage(_ context.Context, filters catalog.BrowseFilters, _ bool) (*catalog.BrowseResult, error) { + f.lastFilters = filters + return &catalog.BrowseResult{Items: []*models.MediaItem{}, Total: 0, HasMore: false}, nil +} + +func (f *scopeFakeBrowseSource) ListGenres(_ context.Context, filters catalog.BrowseFilters) ([]string, error) { + f.lastFilters = filters + return []string{}, nil +} + +func TestBrowseItemsDefaultsToVideoTypeScope(t *testing.T) { + browse := &scopeFakeBrowseSource{} + svc := &directContentService{browseRepo: browse} + session := &Session{StreamAppUserID: 1, ProfileID: "p1"} + + if _, err := svc.BrowseItems(context.Background(), session, url.Values{}); err != nil { + t.Fatalf("BrowseItems: %v", err) + } + if browse.lastFilters.Type != compatVideoTypes { + t.Fatalf("expected default type scope %q, got %q", compatVideoTypes, browse.lastFilters.Type) + } + + params := url.Values{} + params.Set("type", "movie") + if _, err := svc.BrowseItems(context.Background(), session, params); err != nil { + t.Fatalf("BrowseItems: %v", err) + } + if browse.lastFilters.Type != "movie" { + t.Fatalf("expected explicit type to pass through, got %q", browse.lastFilters.Type) + } +} + +func TestListItemFiltersDefaultsToVideoTypeScope(t *testing.T) { + browse := &scopeFakeBrowseSource{} + svc := &directContentService{browseRepo: browse} + + if _, err := svc.ListItemFilters(context.Background(), &Session{StreamAppUserID: 1, ProfileID: "p1"}, url.Values{}); err != nil { + t.Fatalf("ListItemFilters: %v", err) + } + if browse.lastFilters.Type != compatVideoTypes { + t.Fatalf("expected genre listing scoped to %q, got %q", compatVideoTypes, browse.lastFilters.Type) + } +} diff --git a/internal/jellycompat/auth.go b/internal/jellycompat/auth.go index 10b99237..1b7531b5 100644 --- a/internal/jellycompat/auth.go +++ b/internal/jellycompat/auth.go @@ -62,7 +62,8 @@ func ExtractToken(r *http.Request) (string, bool) { if token := strings.TrimSpace(r.Header.Get("X-Mediabrowser-Token")); token != "" { return token, true } - if token := strings.TrimSpace(r.URL.Query().Get("api_key")); token != "" { + // Case-insensitive: Jellyfin clients vary the casing (api_key / Api_Key / API_KEY). + if token := strings.TrimSpace(newCaseInsensitiveQuery(r.URL.Query()).Get("api_key")); token != "" { return token, true } @@ -185,7 +186,12 @@ func PlaybackSessionAuth(sessions *SessionStore, playbackStore *PlaybackSessionS // no auth header or api_key. Resolve the negotiated session's // CompatToken — which for an API-key stream is itself the sa_ key, // so it must go through the same session-or-API-key resolution. - if playSessionID := firstNonEmpty(r.URL.Query().Get("PlaySessionId"), r.URL.Query().Get("PlaySessionID")); playSessionID != "" { + // + // The lookup must be case-insensitive: Wholphin's jellyfin-sdk-kotlin + // builds its own direct-play URL with a lowercase "playSessionId" + // (and no api_key / auth header), so a case-sensitive match would + // miss it and 401 the stream — forcing a needless transcode fallback. + if playSessionID := newCaseInsensitiveQuery(r.URL.Query()).Get("PlaySessionId"); playSessionID != "" { if playSession, found := playbackStore.Get(playSessionID); found { if session, ok := resolveCompatToken(r.Context(), sessions, keyAuth, playSession.CompatToken); ok { serveWithSession(next, w, r, session) diff --git a/internal/jellycompat/auth_test.go b/internal/jellycompat/auth_test.go index bef63ea0..8e855d5d 100644 --- a/internal/jellycompat/auth_test.go +++ b/internal/jellycompat/auth_test.go @@ -119,6 +119,60 @@ func TestRequireSession_NoAuthService_PassesThroughExpiredStreamAppToken(t *test } } +func TestPlaybackSessionAuth_CaseInsensitivePlaySessionId(t *testing.T) { + now := fixedNow() + clock := func() time.Time { return now } + sessions := NewSessionStore(30*24*time.Hour, clock) + _ = sessions.Put(Session{Token: "compat-tok", StreamAppUserID: 1}) + playbackStore := NewPlaybackSessionStore(time.Hour, clock) + playbackStore.Put(PlaybackSession{ID: "ps-abc", CompatToken: "compat-tok"}) + + mw := PlaybackSessionAuth(sessions, playbackStore, nil) + + cases := []struct { + name string + rawQuery string + wantCode int + }{ + // Wholphin's jellyfin-sdk-kotlin direct-play URL: lowercase playSessionId, + // no api_key and no auth header. Previously 401'd (case-sensitive lookup). + {"lowercase playSessionId (Wholphin)", "static=true&mediaSourceId=x&playSessionId=ps-abc", http.StatusOK}, + {"canonical PlaySessionId", "PlaySessionId=ps-abc", http.StatusOK}, + {"legacy PlaySessionID", "PlaySessionID=ps-abc", http.StatusOK}, + {"unknown play session", "playSessionId=does-not-exist", http.StatusUnauthorized}, + {"no auth at all", "static=true", http.StatusUnauthorized}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/Videos/itm/stream?"+tc.rawQuery, nil) + rec := httptest.NewRecorder() + gotSession := false + mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if SessionFromContext(r.Context()) != nil { + gotSession = true + } + w.WriteHeader(http.StatusOK) + })).ServeHTTP(rec, req) + + if rec.Code != tc.wantCode { + t.Fatalf("status = %d, want %d (body: %s)", rec.Code, tc.wantCode, rec.Body.String()) + } + if tc.wantCode == http.StatusOK && !gotSession { + t.Fatal("expected authenticated session in context") + } + }) + } +} + +func TestExtractToken_CaseInsensitiveAPIKey(t *testing.T) { + for _, key := range []string{"api_key", "Api_Key", "API_KEY"} { + req := httptest.NewRequest("GET", "/Videos/itm/stream?"+key+"=tok123", nil) + if got, ok := ExtractToken(req); !ok || got != "tok123" { + t.Fatalf("%s: ExtractToken = (%q, %v), want (tok123, true)", key, got, ok) + } + } +} + func TestRequireAdminAPIKey_AcceptsAdminKey(t *testing.T) { authn := newAdminAPIKeyAuthForTest( &fakeAPIKeyValidator{key: &models.APIKey{ID: 1, UserID: 2, Key: "sa_test"}}, diff --git a/internal/jellycompat/batch_loaders.go b/internal/jellycompat/batch_loaders.go index 23001a22..d8661ba0 100644 --- a/internal/jellycompat/batch_loaders.go +++ b/internal/jellycompat/batch_loaders.go @@ -38,6 +38,7 @@ type itemRepoForBatchLoader interface { // gated through the parent series item via GetByIDsWithAccess on media_items. type episodeRepoForBatchLoader interface { GetByIDs(ctx context.Context, contentIDs []string) ([]*models.Episode, error) + HasFilesByIDs(ctx context.Context, contentIDs []string) (map[string]bool, error) ListBySeason(ctx context.Context, seriesID string, seasonNum int) ([]*models.Episode, error) ListBySeries(ctx context.Context, seriesID string) ([]*models.Episode, error) } @@ -217,7 +218,11 @@ func (h *ItemsHandler) fetchCompatEpisodeTargetsByContentIDs(ctx context.Context COALESCE(si.backdrop_thumbhash, ''), si.logo_path, si.status, - si.updated_at + si.updated_at, + EXISTS ( + SELECT 1 FROM media_files mf + WHERE mf.episode_id = e.content_id AND mf.missing_since IS NULL + ) FROM %s WHERE %s ORDER BY e.content_id @@ -255,6 +260,7 @@ func (h *ItemsHandler) fetchCompatEpisodeTargetsByContentIDs(ctx context.Context seriesLogoPath string status string seriesUpdatedAt time.Time + hasMediaFiles bool ) if err := rows.Scan( &contentID, @@ -280,6 +286,7 @@ func (h *ItemsHandler) fetchCompatEpisodeTargetsByContentIDs(ctx context.Context &seriesLogoPath, &status, &seriesUpdatedAt, + &hasMediaFiles, ); err != nil { return nil, fmt.Errorf("scanning compat episode target: %w", err) } @@ -310,6 +317,7 @@ func (h *ItemsHandler) fetchCompatEpisodeTargetsByContentIDs(ctx context.Context SeasonNumber: intPtr(seasonNumber), EpisodeNumber: intPtr(episodeNumber), Runtime: runtime, + HasMediaFiles: &hasMediaFiles, } if airDate != nil { listItem.AirDate = airDate.Format(time.DateOnly) @@ -357,6 +365,10 @@ func (h *ItemsHandler) fetchCompatEpisodeTargetsByContentIDsFallback(ctx context if err != nil { return nil, err } + hasFiles, err := h.episodeRepo.HasFilesByIDs(ctx, filteredContentIDs) + if err != nil { + return nil, err + } seriesIDs := make([]string, 0, len(episodes)) seenSeries := make(map[string]struct{}, len(episodes)) @@ -417,6 +429,7 @@ func (h *ItemsHandler) fetchCompatEpisodeTargetsByContentIDsFallback(ctx context SeasonNumber: intPtr(episode.SeasonNumber), EpisodeNumber: intPtr(episode.EpisodeNumber), Runtime: episode.Runtime, + HasMediaFiles: boolPtr(hasFiles[episode.ContentID]), } if episode.AirDate != nil { listItem.AirDate = episode.AirDate.Format(time.DateOnly) diff --git a/internal/jellycompat/batch_loaders_test.go b/internal/jellycompat/batch_loaders_test.go index 9feb68af..02c72434 100644 --- a/internal/jellycompat/batch_loaders_test.go +++ b/internal/jellycompat/batch_loaders_test.go @@ -78,6 +78,7 @@ func (r *countingItemRepo) GetItemsInLibrary(_ context.Context, contentIDs []str // countingEpisodeRepo is an in-memory episodeRepoForBatchLoader fake. type countingEpisodeRepo struct { episodesByID map[string]*models.Episode + hasFilesByID map[string]bool getByIDsCalls int } @@ -92,6 +93,16 @@ func (r *countingEpisodeRepo) GetByIDs(_ context.Context, contentIDs []string) ( return out, nil } +func (r *countingEpisodeRepo) HasFilesByIDs(_ context.Context, contentIDs []string) (map[string]bool, error) { + out := make(map[string]bool, len(contentIDs)) + for _, id := range contentIDs { + if r.hasFilesByID[id] { + out[id] = true + } + } + return out, nil +} + func (r *countingEpisodeRepo) ListBySeason(context.Context, string, int) ([]*models.Episode, error) { return nil, errors.New("ListBySeason not used in fallback test") } diff --git a/internal/jellycompat/batch_progress_test.go b/internal/jellycompat/batch_progress_test.go index 33fddd5c..44f7696c 100644 --- a/internal/jellycompat/batch_progress_test.go +++ b/internal/jellycompat/batch_progress_test.go @@ -68,6 +68,10 @@ func (m *mockUserDataService) ListProgress(context.Context, *Session, string, in panic("unused") } +func (m *mockUserDataService) FilterResumeProgress(_ context.Context, _ *Session, entries []upstreamProgress) ([]upstreamProgress, error) { + return entries, nil +} + func (m *mockUserDataService) MarkPlayed(context.Context, *Session, string) error { panic("unused") } diff --git a/internal/jellycompat/content_direct.go b/internal/jellycompat/content_direct.go index 1b30d730..f8e0f882 100644 --- a/internal/jellycompat/content_direct.go +++ b/internal/jellycompat/content_direct.go @@ -36,6 +36,125 @@ const ( compatBrowseMaxLimit = 1000 ) +// compatVideoTypes is the media_items type scope the Jellyfin compat surface +// exposes when a request carries no explicit type filter. Silo serves +// audiobooks and podcasts through the audiobookshelf-compat API instead, so +// those rows must never leak into jellycompat responses. +const compatVideoTypes = "movie,series,episode" + +// compatVideoTypeList is compatVideoTypes as a slice for APIs that take +// []string (e.g. ItemRepository.Search). +var compatVideoTypeList = strings.Split(compatVideoTypes, ",") + +// compatExcludedMediaTypes lists the media_items.type values the Jellyfin +// compat surface never exposes. Injected into every catalog.AccessFilter the +// compat layer resolves (see withCompatAccessExclusions) so access-filtered +// queries — search, favorites, recommendations, detail, batch hydration — +// inherit the exclusion without per-call-site guards. +var compatExcludedMediaTypes = []string{"audiobook", "podcast"} + +func isCompatExcludedMediaType(mediaType string) bool { + for _, excluded := range compatExcludedMediaTypes { + if strings.EqualFold(strings.TrimSpace(mediaType), excluded) { + return true + } + } + return false +} + +// isCompatHiddenLibraryType reports whether a media folder type marks a +// library the Jellyfin compat surface hides. Audiobook ('audiobooks' +// canonical, 'audiobook' legacy — mirrors internal/audiobooks/media_store.go) +// and podcast libraries are served by the ABS-compat API instead. +func isCompatHiddenLibraryType(folderType string) bool { + switch strings.ToLower(strings.TrimSpace(folderType)) { + case "audiobooks", "audiobook", "podcasts", "podcast": + return true + } + return false +} + +// withCompatAccessExclusions merges the compat surface's media-type +// exclusions into a resolved access filter, preserving any exclusions the +// base resolver already supplied. Centralizing the exclusion here lets +// catalog query builders enforce it everywhere an AccessFilter flows. +func withCompatAccessExclusions(filter catalog.AccessFilter) catalog.AccessFilter { + merged := make([]string, 0, len(filter.ExcludedMediaTypes)+len(compatExcludedMediaTypes)) + merged = append(merged, filter.ExcludedMediaTypes...) + for _, excluded := range compatExcludedMediaTypes { + present := false + for _, existing := range merged { + if strings.EqualFold(existing, excluded) { + present = true + break + } + } + if !present { + merged = append(merged, excluded) + } + } + filter.ExcludedMediaTypes = merged + return filter +} + +// compatAccessFilterResolver wraps an AccessFilterResolver so every filter it +// returns carries the compat media-type exclusions. A nil base resolver still +// yields exclusion-only filters. +func compatAccessFilterResolver(base AccessFilterResolver) AccessFilterResolver { + return func(ctx context.Context, userID int, profileID string) catalog.AccessFilter { + if base == nil { + return withCompatAccessExclusions(catalog.AccessFilter{}) + } + return withCompatAccessExclusions(base(ctx, userID, profileID)) + } +} + +// compatNoMatchType is a sentinel media type that matches no media_items row. +// Used when a caller's explicit type filter reduces to nothing exposable so +// the query returns empty instead of silently broadening to all video types. +const compatNoMatchType = "__compat_none__" + +// compatAllowedTypeSet is the closed set of catalog types an explicit compat +// type filter may pass through ("season" matches no media_items row but is +// kept so season-typed filters keep their empty-result semantics). +var compatAllowedTypeSet = map[string]struct{}{ + "movie": {}, + "series": {}, + "episode": {}, + "season": {}, +} + +// compatScopedSearchTypes clamps an item-type filter to the types the compat +// surface exposes: empty defaults to all video types, explicit filters keep +// only allowlisted entries, and a filter that reduces to nothing returns the +// no-match sentinel. +func compatScopedSearchTypes(itemTypes []string) []string { + if len(itemTypes) == 0 { + return compatVideoTypeList + } + kept := make([]string, 0, len(itemTypes)) + for _, t := range itemTypes { + t = strings.ToLower(strings.TrimSpace(t)) + if _, ok := compatAllowedTypeSet[t]; !ok { + continue + } + kept = append(kept, t) + } + if len(kept) == 0 { + return []string{compatNoMatchType} + } + return kept +} + +// compatScopedTypes is compatScopedSearchTypes for comma-separated filters. +func compatScopedTypes(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return compatVideoTypes + } + return strings.Join(compatScopedSearchTypes(strings.Split(raw, ",")), ",") +} + // itemAccessSource is the subset of *catalog.ItemRepository that // directContentService season/episode handlers rely on. Defined as an // interface so tests can substitute a stub without standing up a Postgres @@ -110,9 +229,9 @@ func newDirectContentService( func (s *directContentService) resolveFilter(ctx context.Context, session *Session) catalog.AccessFilter { if s.accessFilter != nil { - return s.accessFilter(ctx, session.StreamAppUserID, session.ProfileID) + return withCompatAccessExclusions(s.accessFilter(ctx, session.StreamAppUserID, session.ProfileID)) } - return catalog.AccessFilter{} + return withCompatAccessExclusions(catalog.AccessFilter{}) } func applyCompatPresentationLibrary(filter catalog.AccessFilter, libraryID *int) catalog.AccessFilter { @@ -168,6 +287,11 @@ func (s *directContentService) ListUserLibraries(ctx context.Context, session *S if _, ok := disabled[f.ID]; ok { continue } + // Audiobook and podcast libraries are served by the ABS-compat API, + // never here. + if isCompatHiddenLibraryType(f.Type) { + continue + } lib := upstreamUserLibrary{ ID: f.ID, Name: f.Name, @@ -212,7 +336,7 @@ func (s *directContentService) BrowseItems(ctx context.Context, session *Session } filters := catalog.BrowseFilters{ - Type: params.Get("type"), + Type: compatScopedTypes(params.Get("type")), Genre: params.Get("genre"), NamePrefix: params.Get("name_prefix"), ContentIDs: contentIDs, @@ -226,6 +350,7 @@ func (s *directContentService) BrowseItems(ctx context.Context, session *Session Limit: fetchLimit, MaxLimit: compatBrowseMaxLimit, Offset: requestedOffset, + RequireBackdrop: parseBool(params.Get("require_backdrop"), false), } var collected []upstreamListItem @@ -334,7 +459,7 @@ func parseContentIDParam(raw string) []string { func (s *directContentService) SearchItems(ctx context.Context, session *Session, query string, itemTypes []string, limit, offset int, libraryID *int) (*upstreamBrowseResponse, error) { filter := applyCompatPresentationLibrary(s.resolveFilter(ctx, session), libraryID) - items, total, err := s.itemRepo.Search(ctx, query, itemTypes, limit, offset, filter) + items, total, err := s.itemRepo.Search(ctx, query, compatScopedSearchTypes(itemTypes), limit, offset, filter) if err != nil { return nil, fmt.Errorf("search items: %w", err) } @@ -365,6 +490,12 @@ func (s *directContentService) GetItemDetail(ctx context.Context, session *Sessi if err != nil { return nil, wrapCatalogError(err) } + // ABS-surface media types are not exposed on the Jellyfin compat surface + // (this guard also blocks PlaybackInfo, which resolves items through + // GetItemDetail). + if isCompatExcludedMediaType(detail.Type) { + return nil, &HTTPError{StatusCode: 404, Message: "item not found"} + } result := itemDetailToUpstream(detail) @@ -378,7 +509,7 @@ func (s *directContentService) GetItemDetail(ctx context.Context, session *Sessi PositionSeconds: progress.PositionSeconds, DurationSeconds: progress.DurationSeconds, Played: progress.Completed, - IsInProgress: progress.PositionSeconds > 0 && !progress.Completed, + IsInProgress: progress.PositionSeconds > 0, } } @@ -549,7 +680,7 @@ func (s *directContentService) ListItemFilters(ctx context.Context, session *Ses filter := s.resolveFilter(ctx, session) filters := catalog.BrowseFilters{ - Type: params.Get("type"), + Type: compatScopedTypes(params.Get("type")), LibraryID: catalog.ParseIntParam(params.Get("library_id")), LibraryIDs: filter.AllowedLibraryIDs, DisabledLibraryIDs: filter.DisabledLibraryIDs, @@ -810,7 +941,7 @@ func seasonUserDataFromProgress(progress userstore.WatchProgress) *catalog.Seaso PositionSeconds: progress.PositionSeconds, DurationSeconds: progress.DurationSeconds, Played: progress.Completed, - IsInProgress: progress.PositionSeconds > 0 && !progress.Completed, + IsInProgress: progress.PositionSeconds > 0, } } diff --git a/internal/jellycompat/content_service.go b/internal/jellycompat/content_service.go index da3b9d63..1537e100 100644 --- a/internal/jellycompat/content_service.go +++ b/internal/jellycompat/content_service.go @@ -26,6 +26,11 @@ type UserDataService interface { AddFavorite(ctx context.Context, session *Session, contentID string) error RemoveFavorite(ctx context.Context, session *Session, contentID string) error ListProgress(ctx context.Context, session *Session, status string, limit, offset int) ([]upstreamProgress, error) + // FilterResumeProgress drops in-progress entries that Continue Watching + // surfaces should hide: entries the user dismissed from the row and + // episodes superseded by a later-completed episode in the same series. + // It mirrors the first-party sections fetcher so both surfaces agree. + FilterResumeProgress(ctx context.Context, session *Session, entries []upstreamProgress) ([]upstreamProgress, error) ListProgressByMediaItems(ctx context.Context, session *Session, mediaItemIDs []string) (map[string]*upstreamProgress, error) GetProgress(ctx context.Context, session *Session, contentID string) (*upstreamProgress, error) MarkPlayed(ctx context.Context, session *Session, contentID string) error diff --git a/internal/jellycompat/download_route_test.go b/internal/jellycompat/download_route_test.go new file mode 100644 index 00000000..50189a7b --- /dev/null +++ b/internal/jellycompat/download_route_test.go @@ -0,0 +1,88 @@ +package jellycompat + +import ( + "context" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-chi/chi/v5" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +// TestHandleDownload_ServesOriginalFile verifies /Items/{id}/Download streams +// the original media file. The route backs CanDownload=true, which Infuse +// requires before it will Direct Play an item. +func TestHandleDownload_ServesOriginalFile(t *testing.T) { + dir := t.TempDir() + filePath := filepath.Join(dir, "movie.mkv") + content := []byte("fake media bytes") + if err := os.WriteFile(filePath, content, 0o644); err != nil { + t.Fatal(err) + } + + codec := NewResourceIDCodec() + contentID := "movie-1" + detail := &upstreamItemDetail{ + ContentID: contentID, + Type: "movie", + Versions: []catalog.FileVersion{{ + FileID: 42, + FilePath: filePath, + Container: "mkv", + Duration: 3600, + AddedAt: time.Now(), + }}, + } + handler := &PlaybackHandler{ + codec: codec, + content: &stubContentService{detail: detail}, + fileResolver: testCompatFileResolver{file: &models.MediaFile{ID: 42, FilePath: filePath}}, + } + + encodedID := codec.EncodeStringID(EncodedIDItem, contentID) + req := httptest.NewRequest("GET", "/Items/"+encodedID+"/Download", nil) + routeCtx := chi.NewRouteContext() + routeCtx.URLParams.Add("id", encodedID) + ctx := context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx) + ctx = context.WithValue(ctx, compatSessionKey, &Session{StreamAppUserID: 1, ProfileID: "profile-1"}) + req = req.WithContext(ctx) + + rec := httptest.NewRecorder() + handler.HandleDownload(rec, req) + + if rec.Code != 200 { + t.Fatalf("expected status 200; got %d, body=%s", rec.Code, rec.Body.String()) + } + if got := rec.Body.String(); got != string(content) { + t.Errorf("expected file content %q; got %q", content, got) + } + if cd := rec.Header().Get("Content-Disposition"); cd == "" { + t.Error("expected Content-Disposition header on download response") + } +} + +// TestItemDetail_AdvertisesCanDownload guards against regressing the +// CanDownload flag: Infuse refuses Direct Play (Static=true streaming) of +// items it believes it cannot download, so playable items must advertise it. +func TestItemDetail_AdvertisesCanDownload(t *testing.T) { + m := newMapper(NewResourceIDCodec(), nil) + detail := upstreamItemDetail{ + ContentID: "movie-1", + Type: "movie", + Versions: []catalog.FileVersion{{ + FileID: 42, + Container: "mkv", + Duration: 3600, + AddedAt: time.Now(), + }}, + } + dto := m.itemFromDetailWithFields(detail, false, nil, nil) + if !dto.CanDownload { + t.Error("playable item detail must advertise CanDownload=true; Infuse requires it for Direct Play") + } +} diff --git a/internal/jellycompat/dto.go b/internal/jellycompat/dto.go index c81307be..c06a725c 100644 --- a/internal/jellycompat/dto.go +++ b/internal/jellycompat/dto.go @@ -7,6 +7,16 @@ type queryResultDTO struct { StartIndex int `json:"StartIndex"` } +// themeMediaResultDTO mirrors Jellyfin's ThemeMediaResult. OwnerId is required: +// jellyfin-sdk-kotlin models it as non-nullable, so omitting it fails client +// deserialization even for an empty result. +type themeMediaResultDTO struct { + Items []baseItemDTO `json:"Items"` + TotalRecordCount int `json:"TotalRecordCount"` + StartIndex int `json:"StartIndex"` + OwnerID string `json:"OwnerId"` +} + type baseItemDTO struct { ServerID string `json:"ServerId,omitempty"` ID string `json:"Id"` @@ -48,6 +58,7 @@ type baseItemDTO struct { ProviderIDs map[string]string `json:"ProviderIds,omitempty"` ProductionLocations []string `json:"ProductionLocations,omitempty"` ImageTags map[string]string `json:"ImageTags"` + PrimaryImageItemID string `json:"PrimaryImageItemId,omitempty"` BackdropImageTags []string `json:"BackdropImageTags,omitempty"` PrimaryImageAspectRatio *float64 `json:"PrimaryImageAspectRatio,omitempty"` ImageBlurHashes map[string]map[string]string `json:"ImageBlurHashes,omitempty"` @@ -73,6 +84,7 @@ type baseItemDTO struct { SpecialFeatureCount int `json:"SpecialFeatureCount,omitempty"` MovieCount int `json:"MovieCount,omitempty"` SeriesCount int `json:"SeriesCount,omitempty"` + SeasonCount int `json:"SeasonCount,omitempty"` EpisodeCount int `json:"EpisodeCount,omitempty"` LockedFields []string `json:"LockedFields,omitempty"` LockData bool `json:"LockData,omitempty"` diff --git a/internal/jellycompat/handlers_collections.go b/internal/jellycompat/handlers_collections.go new file mode 100644 index 00000000..c1afc477 --- /dev/null +++ b/internal/jellycompat/handlers_collections.go @@ -0,0 +1,362 @@ +package jellycompat + +import ( + "context" + "errors" + "net/http" + "sort" + "strings" + "time" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/models" +) + +// collectionSource is the subset of *catalog.LibraryCollectionRepository the +// compat layer relies on to expose library collections as Jellyfin BoxSets. +type collectionSource interface { + ListAll(ctx context.Context, libraryID *int, opts catalog.ListLibraryCollectionsOptions) ([]*models.LibraryCollection, error) + GetByID(ctx context.Context, id string) (*models.LibraryCollection, error) + ListItems(ctx context.Context, collectionID string) ([]*models.LibraryCollectionItem, error) +} + +// visibleLibraryIDSet returns the set of library IDs the session may see on +// the compat surface (access-filtered and ABS-library-excluded by +// ListUserLibraries). +func visibleLibraryIDSet(ctx context.Context, content ContentService, session *Session) (map[int]struct{}, error) { + libraries, err := content.ListUserLibraries(ctx, session) + if err != nil { + return nil, err + } + visible := make(map[int]struct{}, len(libraries)) + for _, lib := range libraries { + visible[lib.ID] = struct{}{} + } + return visible, nil +} + +func (h *ItemsHandler) visibleLibraryIDs(ctx context.Context, session *Session) (map[int]struct{}, error) { + return visibleLibraryIDSet(ctx, h.content, session) +} + +// collectionVisible reports whether any of the collection's libraries is +// visible to the session. Collections scoped only to hidden or ABS-surface +// libraries stay off the compat surface. +func collectionVisible(c *models.LibraryCollection, visible map[int]struct{}) bool { + if len(c.LibraryIDs) == 0 { + _, ok := visible[c.LibraryID] + return ok + } + for _, id := range c.LibraryIDs { + if _, ok := visible[id]; ok { + return true + } + } + return false +} + +// loadVisibleCollection fetches a collection and applies the compat +// visibility rules. Returns (nil, nil) when the collection does not exist or +// the session may not see it; infrastructure errors propagate so transient +// failures don't masquerade as 404s. +func (h *ItemsHandler) loadVisibleCollection(ctx context.Context, session *Session, collectionID string) (*models.LibraryCollection, error) { + if h.collections == nil { + return nil, nil + } + collection, err := h.collections.GetByID(ctx, collectionID) + if err != nil { + if errors.Is(err, catalog.ErrLibraryCollectionNotFound) { + return nil, nil + } + return nil, err + } + if collection == nil || !strings.EqualFold(collection.Visibility, "visible") { + return nil, nil + } + visible, err := h.visibleLibraryIDs(ctx, session) + if err != nil { + return nil, err + } + if !collectionVisible(collection, visible) { + return nil, nil + } + return collection, nil +} + +// boxSetFromCollection maps a library collection to a Jellyfin BoxSet DTO. +// Image tags are signed from the stable artwork key (like library views) so +// they survive restarts and presign rotation; the in-memory image cache is +// seeded as the warm path. +func (h *ItemsHandler) boxSetFromCollection(ctx context.Context, c *models.LibraryCollection) baseItemDTO { + routeID := h.codec.EncodeStringID(EncodedIDCollection, c.ID) + imgTags := map[string]string{} + if posterURL := h.presignCollectionPoster(ctx, c.PosterURL); posterURL != "" { + if h.images != nil { + h.images.RememberSized(routeID, "Primary", posterURL, compatCardImageSize) + } + imgTags["Primary"] = h.mapper.imageTagSigner.Tag( + imageTagSeed(routeID, "Primary", compatCardImageSize, c.PosterURL, "", time.Time{}), + posterURL, + ) + } + dto := baseItemDTO{ + ID: routeID, + Type: "BoxSet", + IsFolder: true, + Name: c.Title, + ServerID: h.mapper.serverID, + Overview: c.Description, + SortName: strings.ToLower(c.Title), + ChildCount: c.ItemCount, + RecursiveItemCount: c.ItemCount, + ImageTags: imgTags, + UserData: &itemUserDataDTO{ + Key: routeID, + ItemID: routeID, + }, + } + if backdropURL := h.presignCollectionPoster(ctx, c.BackdropURL); backdropURL != "" { + if h.images != nil { + h.images.RememberSized(routeID, "Backdrop", backdropURL, compatCardImageSize) + } + dto.BackdropImageTags = []string{h.mapper.imageTagSigner.Tag( + imageTagSeed(routeID, "Backdrop", compatCardImageSize, c.BackdropURL, "", time.Time{}), + backdropURL, + )} + } + return dto +} + +// presignCollectionPoster resolves a collection artwork reference to a +// fetchable URL. Collection posters are stored as S3 keys in the +// general-purpose bucket (same bucket as library posters); absolute and +// app-relative references pass through untouched (matching the main API's +// presignGPURL semantics). +func (h *ItemsHandler) presignCollectionPoster(ctx context.Context, path string) string { + path = strings.TrimSpace(path) + if path == "" { + return "" + } + if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "/") { + return path + } + if h.posterPresigner == nil { + return "" + } + ttl := h.presignTTL + if ttl <= 0 { + ttl = 4 * time.Hour + } + url, err := h.posterPresigner.PresignGetURL(ctx, h.posterPresigner.Bucket(), path, ttl) + if err != nil { + return "" + } + return url +} + +// boxSetsByIDs maps the given collection IDs to BoxSet DTOs, skipping any the +// session may not see. Used by /Items?Ids= re-hydration. +func (h *ItemsHandler) boxSetsByIDs(ctx context.Context, session *Session, collectionIDs []string) ([]baseItemDTO, error) { + if len(collectionIDs) == 0 || h.collections == nil { + return nil, nil + } + items := make([]baseItemDTO, 0, len(collectionIDs)) + for _, id := range collectionIDs { + collection, err := h.loadVisibleCollection(ctx, session, id) + if err != nil { + return nil, err + } + if collection == nil { + continue + } + items = append(items, h.boxSetFromCollection(ctx, collection)) + } + return items, nil +} + +// handleBoxSetsList serves GET /Items with IncludeItemTypes=BoxSet by listing +// visible library collections, optionally scoped to one library via ParentId. +// Filtering, sorting, and paging happen on the lightweight collection rows; +// DTOs (with artwork presigning) are built only for the returned page. +func (h *ItemsHandler) handleBoxSetsList(w http.ResponseWriter, r *http.Request, session *Session, query itemsQuery) { + if h.collections == nil { + writeJSON(w, http.StatusOK, emptyQueryResult(query.startIndex)) + return + } + + visible, err := h.visibleLibraryIDs(r.Context(), session) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + + var libFilter *int + if query.parentLibraryID > 0 { + if _, ok := visible[query.parentLibraryID]; !ok { + writeJSON(w, http.StatusOK, emptyQueryResult(query.startIndex)) + return + } + libFilter = &query.parentLibraryID + } + + collections, err := h.collections.ListAll(r.Context(), libFilter, catalog.ListLibraryCollectionsOptions{}) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + + searchTerm := strings.ToLower(strings.TrimSpace(query.searchTerm)) + namePrefix := strings.ToLower(query.namePrefix) + matched := make([]*models.LibraryCollection, 0, len(collections)) + for _, c := range collections { + if !collectionVisible(c, visible) { + continue + } + title := strings.ToLower(c.Title) + if searchTerm != "" && !strings.Contains(title, searchTerm) { + continue + } + if namePrefix != "" && !strings.HasPrefix(title, namePrefix) { + continue + } + matched = append(matched, c) + } + + if query.sort == "sort_title" { + ascending := query.order != "desc" + sort.SliceStable(matched, func(i, j int) bool { + a, b := strings.ToLower(matched[i].Title), strings.ToLower(matched[j].Title) + if ascending { + return a < b + } + return a > b + }) + } + + page := slicePage(matched, query.startIndex, query.limit) + items := make([]baseItemDTO, 0, len(page)) + for _, c := range page { + items = append(items, h.boxSetFromCollection(r.Context(), c)) + } + writeJSON(w, http.StatusOK, queryResultDTO{ + Items: items, + TotalRecordCount: len(matched), + StartIndex: query.startIndex, + }) +} + +// slicePage returns the [startIndex, startIndex+limit) window of items; +// limit <= 0 means no cap. +func slicePage[T any](items []T, startIndex, limit int) []T { + if startIndex < 0 { + startIndex = 0 + } + if startIndex >= len(items) { + return nil + } + if limit <= 0 { + limit = len(items) + } + end := min(startIndex+limit, len(items)) + return items[startIndex:end] +} + +// handleBoxSetItem serves GET /Items/{id} when the ID decodes as a collection. +func (h *ItemsHandler) handleBoxSetItem(w http.ResponseWriter, r *http.Request, session *Session, collectionID string) { + collection, err := h.loadVisibleCollection(r.Context(), session, collectionID) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + if collection == nil { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + writeJSON(w, http.StatusOK, h.boxSetFromCollection(r.Context(), collection)) +} + +// handleBoxSetChildren serves GET /Items?ParentId={boxsetId} by hydrating the +// collection's members. Without an explicit SortBy the curated collection +// position order is preserved; an explicit SortBy delegates ordering and +// paging to the catalog browse path. +func (h *ItemsHandler) handleBoxSetChildren(w http.ResponseWriter, r *http.Request, session *Session, query itemsQuery) { + collection, err := h.loadVisibleCollection(r.Context(), session, query.parentCollectionID) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + if collection == nil { + writeJSON(w, http.StatusOK, emptyQueryResult(query.startIndex)) + return + } + + members, err := h.collections.ListItems(r.Context(), collection.ID) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + contentIDs := make([]string, 0, len(members)) + for _, member := range members { + contentIDs = append(contentIDs, member.MediaItemID) + } + if len(contentIDs) == 0 { + writeJSON(w, http.StatusOK, emptyQueryResult(query.startIndex)) + return + } + + routeID := h.codec.EncodeStringID(EncodedIDCollection, collection.ID) + + if query.sortExplicit { + // Catalog handles ordering and paging; the member list acts as an + // access-filtered allowlist. + params := buildBrowseParams(query) + params.Set("content_ids", strings.Join(contentIDs, ",")) + result, browseErr := h.content.BrowseItems(r.Context(), session, params) + if browseErr != nil { + writeCompatUpstreamError(w, browseErr) + return + } + h.writeCollectionItemsPage(w, r, session, query, routeID, result.Items, result.Total) + return + } + + // Position order: hydrate the surviving members (collections are capped + // well below the browse limit), rebuild curated order, then page locally + // before building DTOs. + itemsByID, err := h.fetchCompatItemsByContentIDs(r.Context(), session, contentIDs, nil) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + ordered := make([]upstreamListItem, 0, len(contentIDs)) + for _, contentID := range contentIDs { + if item, ok := itemsByID[contentID]; ok { + ordered = append(ordered, item) + } + } + page := slicePage(ordered, query.startIndex, query.limit) + h.writeCollectionItemsPage(w, r, session, query, routeID, page, len(ordered)) +} + +// writeCollectionItemsPage hydrates user state for one page of collection +// members and writes the /Items result with ParentId stamped on each child. +func (h *ItemsHandler) writeCollectionItemsPage(w http.ResponseWriter, r *http.Request, session *Session, query itemsQuery, routeID string, listItems []upstreamListItem, total int) { + h.rememberListImages(listItems) + favorites, progress, err := resolveUserStateForContentIDs(r.Context(), session, h.userData, contentIDsFromListItems(listItems)) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + items := make([]baseItemDTO, 0, len(listItems)) + for _, item := range listItems { + dto := h.mapper.itemFromList(item, favorites[item.ContentID], progress[item.ContentID], query.requestedFields) + dto.ParentID = routeID + items = append(items, dto) + } + applyImageTypeLimit(items, query.imageTypeLimit) + writeJSON(w, http.StatusOK, queryResultDTO{ + Items: items, + TotalRecordCount: total, + StartIndex: query.startIndex, + }) +} diff --git a/internal/jellycompat/handlers_collections_test.go b/internal/jellycompat/handlers_collections_test.go new file mode 100644 index 00000000..742b87d4 --- /dev/null +++ b/internal/jellycompat/handlers_collections_test.go @@ -0,0 +1,376 @@ +package jellycompat + +import ( + "context" + "encoding/json" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-chi/chi/v5" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/config" + "github.com/Silo-Server/silo-server/internal/models" +) + +// fakeCollectionSource is an in-memory collectionSource. +type fakeCollectionSource struct { + collections []*models.LibraryCollection + items map[string][]*models.LibraryCollectionItem + listAllLib *int +} + +func (f *fakeCollectionSource) ListAll(_ context.Context, libraryID *int, _ catalog.ListLibraryCollectionsOptions) ([]*models.LibraryCollection, error) { + f.listAllLib = libraryID + out := make([]*models.LibraryCollection, 0, len(f.collections)) + for _, c := range f.collections { + if libraryID != nil && !collectionInLibrary(c, *libraryID) { + continue + } + if c.Visibility != "visible" { + continue + } + out = append(out, c) + } + return out, nil +} + +func collectionInLibrary(c *models.LibraryCollection, libraryID int) bool { + if len(c.LibraryIDs) == 0 { + return c.LibraryID == libraryID + } + for _, id := range c.LibraryIDs { + if id == libraryID { + return true + } + } + return false +} + +func (f *fakeCollectionSource) GetByID(_ context.Context, id string) (*models.LibraryCollection, error) { + for _, c := range f.collections { + if c.ID == id { + return c, nil + } + } + return nil, nil +} + +func (f *fakeCollectionSource) ListItems(_ context.Context, collectionID string) ([]*models.LibraryCollectionItem, error) { + return f.items[collectionID], nil +} + +// librariesContentService serves a fixed library list; other methods panic. +type librariesContentService struct { + countingContentService + libraries []upstreamUserLibrary +} + +func (s *librariesContentService) ListUserLibraries(context.Context, *Session) ([]upstreamUserLibrary, error) { + out := make([]upstreamUserLibrary, len(s.libraries)) + copy(out, s.libraries) + return out, nil +} + +// fakeBatchItemRepo backs the compatPool()==nil hydration fallback. +type fakeBatchItemRepo struct { + items map[string]*models.MediaItem +} + +func (f *fakeBatchItemRepo) GetByIDs(_ context.Context, contentIDs []string) ([]*models.MediaItem, error) { + return f.byIDs(contentIDs), nil +} + +func (f *fakeBatchItemRepo) GetByIDsWithAccess(_ context.Context, contentIDs []string, _ catalog.AccessFilter) ([]*models.MediaItem, error) { + return f.byIDs(contentIDs), nil +} + +func (f *fakeBatchItemRepo) GetItemsInLibrary(_ context.Context, contentIDs []string, _ int) (map[string]bool, error) { + out := make(map[string]bool, len(contentIDs)) + for _, id := range contentIDs { + out[id] = true + } + return out, nil +} + +func (f *fakeBatchItemRepo) byIDs(contentIDs []string) []*models.MediaItem { + out := make([]*models.MediaItem, 0, len(contentIDs)) + for _, id := range contentIDs { + if item, ok := f.items[id]; ok { + out = append(out, item) + } + } + return out +} + +func newCollectionsTestHandler(collections *fakeCollectionSource, libraries []upstreamUserLibrary, itemRepo itemRepoForBatchLoader) *ItemsHandler { + codec := NewResourceIDCodec() + return &ItemsHandler{ + content: &librariesContentService{libraries: libraries}, + userData: &mockUserDataService{}, + codec: codec, + mapper: newMapper(codec, &config.Config{}), + images: NewImageCache(time.Hour, time.Now), + collections: collections, + itemRepo: itemRepo, + } +} + +func collectionsTestSession() *Session { + return &Session{StreamAppUserID: 1, ProfileID: "profile-1"} +} + +func performItemsRequest(t *testing.T, h *ItemsHandler, target string) queryResultDTO { + t.Helper() + req := httptest.NewRequest("GET", target, nil) + req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, collectionsTestSession())) + rec := httptest.NewRecorder() + h.HandleItems(rec, req) + if rec.Code != 200 { + t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + var result queryResultDTO + if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + return result +} + +func TestHandleItems_BoxSetListingFiltersHiddenLibraries(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible", ItemCount: 3}, + {ID: "102", LibraryID: 2, Title: "Audiobook Picks", Visibility: "visible", ItemCount: 5}, + {ID: "103", LibraryID: 1, Title: "Hidden", Visibility: "hidden", ItemCount: 2}, + }, + } + // Library 2 is not visible (e.g. audiobook library, excluded upstream). + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, nil) + + result := performItemsRequest(t, h, "/Items?IncludeItemTypes=BoxSet&Recursive=true") + if result.TotalRecordCount != 1 || len(result.Items) != 1 { + t.Fatalf("expected exactly the Marvel collection, got %+v", result.Items) + } + item := result.Items[0] + if item.Type != "BoxSet" || item.Name != "Marvel" || !item.IsFolder || item.ChildCount != 3 { + t.Fatalf("unexpected BoxSet DTO: %+v", item) + } +} + +func TestHandleItems_BoxSetListingScopedToParentLibrary(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible"}, + {ID: "104", LibraryID: 3, Title: "Shows Sets", Visibility: "visible"}, + }, + } + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{ + {ID: 1, Name: "Movies", Type: "movies"}, + {ID: 3, Name: "Shows", Type: "series"}, + }, nil) + + parentID := h.codec.EncodeIntID(EncodedIDLibrary, 3) + result := performItemsRequest(t, h, "/Items?IncludeItemTypes=BoxSet&ParentId="+parentID) + if len(result.Items) != 1 || result.Items[0].Name != "Shows Sets" { + t.Fatalf("expected library-scoped collection list, got %+v", result.Items) + } + if collections.listAllLib == nil || *collections.listAllLib != 3 { + t.Fatalf("expected ListAll scoped to library 3, got %v", collections.listAllLib) + } +} + +func TestHandleItems_BoxSetChildrenPreservePositionOrder(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible"}, + }, + items: map[string][]*models.LibraryCollectionItem{ + "101": { + {CollectionID: "101", MediaItemID: "m-2", Position: 0}, + {CollectionID: "101", MediaItemID: "m-1", Position: 1}, + {CollectionID: "101", MediaItemID: "m-3", Position: 2}, + }, + }, + } + itemRepo := &fakeBatchItemRepo{items: map[string]*models.MediaItem{ + "m-1": {ContentID: "m-1", Type: "movie", Title: "Iron Man"}, + "m-2": {ContentID: "m-2", Type: "movie", Title: "Captain America"}, + "m-3": {ContentID: "m-3", Type: "movie", Title: "Thor"}, + }} + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, itemRepo) + + parentID := h.codec.EncodeStringID(EncodedIDCollection, "101") + result := performItemsRequest(t, h, "/Items?ParentId="+parentID) + if len(result.Items) != 3 { + t.Fatalf("expected 3 children, got %d", len(result.Items)) + } + gotNames := []string{result.Items[0].Name, result.Items[1].Name, result.Items[2].Name} + wantNames := []string{"Captain America", "Iron Man", "Thor"} + for i := range wantNames { + if gotNames[i] != wantNames[i] { + t.Fatalf("expected curated order %v, got %v", wantNames, gotNames) + } + } + if result.Items[0].ParentID != parentID { + t.Fatalf("expected ParentId %s, got %s", parentID, result.Items[0].ParentID) + } +} + +func TestHandleItems_BoxSetChildrenPagination(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible"}, + }, + items: map[string][]*models.LibraryCollectionItem{ + "101": { + {CollectionID: "101", MediaItemID: "m-1", Position: 0}, + {CollectionID: "101", MediaItemID: "m-2", Position: 1}, + {CollectionID: "101", MediaItemID: "m-3", Position: 2}, + }, + }, + } + itemRepo := &fakeBatchItemRepo{items: map[string]*models.MediaItem{ + "m-1": {ContentID: "m-1", Type: "movie", Title: "One"}, + "m-2": {ContentID: "m-2", Type: "movie", Title: "Two"}, + "m-3": {ContentID: "m-3", Type: "movie", Title: "Three"}, + }} + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, itemRepo) + + parentID := h.codec.EncodeStringID(EncodedIDCollection, "101") + result := performItemsRequest(t, h, "/Items?ParentId="+parentID+"&StartIndex=1&Limit=1") + if result.TotalRecordCount != 3 { + t.Fatalf("expected total 3, got %d", result.TotalRecordCount) + } + if len(result.Items) != 1 || result.Items[0].Name != "Two" { + t.Fatalf("expected page [Two], got %+v", result.Items) + } +} + +func TestHandleItem_BoxSetDetailAndHiddenCollection(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible", ItemCount: 4, Description: "Heroes"}, + {ID: "103", LibraryID: 1, Title: "Hidden", Visibility: "hidden"}, + }, + } + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, nil) + + router := chi.NewRouter() + router.Get("/Items/{id}", h.HandleItem) + + routeID := h.codec.EncodeStringID(EncodedIDCollection, "101") + req := httptest.NewRequest("GET", "/Items/"+routeID, nil) + req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, collectionsTestSession())) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + if rec.Code != 200 { + t.Fatalf("expected 200 for visible collection, got %d", rec.Code) + } + var dto baseItemDTO + if err := json.Unmarshal(rec.Body.Bytes(), &dto); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if dto.Type != "BoxSet" || dto.Name != "Marvel" || dto.Overview != "Heroes" || dto.ChildCount != 4 { + t.Fatalf("unexpected detail DTO: %+v", dto) + } + + hiddenID := h.codec.EncodeStringID(EncodedIDCollection, "103") + req = httptest.NewRequest("GET", "/Items/"+hiddenID, nil) + req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, collectionsTestSession())) + rec = httptest.NewRecorder() + router.ServeHTTP(rec, req) + if rec.Code != 404 { + t.Fatalf("expected 404 for hidden collection, got %d", rec.Code) + } +} + +func TestHandleItems_UnmappedTypeFilterReturnsEmpty(t *testing.T) { + h := newCollectionsTestHandler(&fakeCollectionSource{}, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, nil) + + result := performItemsRequest(t, h, "/Items?IncludeItemTypes=Playlist") + if result.TotalRecordCount != 0 || len(result.Items) != 0 { + t.Fatalf("expected empty result for unmapped type filter, got %+v", result) + } +} + +func TestHandleItems_BoxSetWithUserStateFilterReturnsEmpty(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible"}, + }, + } + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, nil) + + // Collections carry no favorite/played state; user-state-filtered BoxSet + // queries must stay empty (jellyfin-web Favorites tab relies on this). + for _, target := range []string{ + "/Items?IncludeItemTypes=BoxSet&Filters=IsFavorite", + "/Items?IncludeItemTypes=BoxSet&Filters=IsResumable", + "/Items?IncludeItemTypes=BoxSet&IsPlayed=true", + } { + result := performItemsRequest(t, h, target) + if result.TotalRecordCount != 0 || len(result.Items) != 0 { + t.Fatalf("%s: expected empty result, got %+v", target, result.Items) + } + } +} + +func TestHandleItems_CollectionFolderTypeReturnsViews(t *testing.T) { + h := newCollectionsTestHandler(&fakeCollectionSource{}, []upstreamUserLibrary{ + {ID: 1, Name: "Movies", Type: "movies"}, + {ID: 3, Name: "Shows", Type: "series"}, + }, nil) + + result := performItemsRequest(t, h, "/Items?IncludeItemTypes=CollectionFolder") + if len(result.Items) != 2 { + t.Fatalf("expected 2 library views, got %+v", result.Items) + } + for _, item := range result.Items { + if item.Type != "CollectionFolder" { + t.Fatalf("expected CollectionFolder items, got %+v", item) + } + } +} + +func TestHandleItems_SpecificIdsIncludeBoxSets(t *testing.T) { + collections := &fakeCollectionSource{ + collections: []*models.LibraryCollection{ + {ID: "101", LibraryID: 1, Title: "Marvel", Visibility: "visible", ItemCount: 2}, + }, + } + h := newCollectionsTestHandler(collections, []upstreamUserLibrary{{ID: 1, Name: "Movies", Type: "movies"}}, nil) + + boxSetID := h.codec.EncodeStringID(EncodedIDCollection, "101") + result := performItemsRequest(t, h, "/Items?Ids="+url.QueryEscape(boxSetID)) + if len(result.Items) != 1 || result.Items[0].Type != "BoxSet" || result.Items[0].Name != "Marvel" { + t.Fatalf("expected the BoxSet DTO for Ids= re-hydration, got %+v", result.Items) + } +} + +func TestParseItemsQuery_BoxSetFlags(t *testing.T) { + codec := NewResourceIDCodec() + + req := httptest.NewRequest("GET", "/Items?IncludeItemTypes=BoxSet", nil) + query := parseItemsQuery(req, codec) + if !query.wantsBoxSets || len(query.itemTypes) != 0 || !query.hasItemTypeFilter { + t.Fatalf("expected BoxSet flags, got %+v", query) + } + + collectionID := codec.EncodeStringID(EncodedIDCollection, "555") + req = httptest.NewRequest("GET", "/Items?ParentId="+url.QueryEscape(collectionID), nil) + query = parseItemsQuery(req, codec) + if query.parentCollectionID != "555" { + t.Fatalf("expected parentCollectionID 555, got %q", query.parentCollectionID) + } + if query.sortExplicit { + t.Fatalf("expected sortExplicit=false without SortBy") + } + + req = httptest.NewRequest("GET", "/Items?SortBy=SortName", nil) + query = parseItemsQuery(req, codec) + if !query.sortExplicit { + t.Fatalf("expected sortExplicit=true with SortBy") + } +} diff --git a/internal/jellycompat/handlers_displayprefs.go b/internal/jellycompat/handlers_displayprefs.go index 12a319ef..51e24e5a 100644 --- a/internal/jellycompat/handlers_displayprefs.go +++ b/internal/jellycompat/handlers_displayprefs.go @@ -14,16 +14,18 @@ import ( // displayPreferencesDTO mirrors Jellyfin's DisplayPreferences response. type displayPreferencesDTO struct { - ID string `json:"Id"` - SortBy string `json:"SortBy"` - SortOrder string `json:"SortOrder"` - RememberIndexing bool `json:"RememberIndexing"` - RememberSorting bool `json:"RememberSorting"` - ScrollDirection string `json:"ScrollDirection"` - ShowBackdrop bool `json:"ShowBackdrop"` - ShowSidebar bool `json:"ShowSidebar"` - Client string `json:"Client"` - CustomPrefs map[string]string `json:"CustomPrefs"` + ID string `json:"Id"` + SortBy string `json:"SortBy"` + SortOrder string `json:"SortOrder"` + RememberIndexing bool `json:"RememberIndexing"` + RememberSorting bool `json:"RememberSorting"` + ScrollDirection string `json:"ScrollDirection"` + ShowBackdrop bool `json:"ShowBackdrop"` + ShowSidebar bool `json:"ShowSidebar"` + PrimaryImageHeight int `json:"PrimaryImageHeight"` + PrimaryImageWidth int `json:"PrimaryImageWidth"` + Client string `json:"Client"` + CustomPrefs map[string]string `json:"CustomPrefs"` } // DisplayPreferencesHandler serves Jellyfin display preferences endpoints, diff --git a/internal/jellycompat/handlers_displayprefs_test.go b/internal/jellycompat/handlers_displayprefs_test.go new file mode 100644 index 00000000..e06709e6 --- /dev/null +++ b/internal/jellycompat/handlers_displayprefs_test.go @@ -0,0 +1,27 @@ +package jellycompat + +import ( + "encoding/json" + "testing" +) + +func TestDefaultDisplayPreferencesIncludesRequiredImageDimensions(t *testing.T) { + dto := defaultDisplayPreferences("default", "Wholphin") + + body, err := json.Marshal(dto) + if err != nil { + t.Fatalf("marshal default display preferences: %v", err) + } + + var raw map[string]any + if err := json.Unmarshal(body, &raw); err != nil { + t.Fatalf("unmarshal default display preferences: %v", err) + } + + if _, ok := raw["PrimaryImageHeight"]; !ok { + t.Fatal("PrimaryImageHeight missing from display preferences JSON") + } + if _, ok := raw["PrimaryImageWidth"]; !ok { + t.Fatal("PrimaryImageWidth missing from display preferences JSON") + } +} diff --git a/internal/jellycompat/handlers_genres_test.go b/internal/jellycompat/handlers_genres_test.go new file mode 100644 index 00000000..9637c187 --- /dev/null +++ b/internal/jellycompat/handlers_genres_test.go @@ -0,0 +1,64 @@ +package jellycompat + +import ( + "context" + "encoding/json" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-chi/chi/v5" + + "github.com/Silo-Server/silo-server/internal/config" +) + +// genresContentService serves fixed genre filters; other methods panic. +type genresContentService struct { + countingContentService + genres []string +} + +func (s *genresContentService) ListItemFilters(context.Context, *Session, url.Values) (*upstreamItemFiltersResponse, error) { + return &upstreamItemFiltersResponse{Genres: s.genres}, nil +} + +func TestHandleGenreByName(t *testing.T) { + codec := NewResourceIDCodec() + h := &ItemsHandler{ + content: &genresContentService{genres: []string{"Action", "Science Fiction"}}, + userData: &mockUserDataService{}, + codec: codec, + mapper: newMapper(codec, &config.Config{}), + images: NewImageCache(time.Hour, time.Now), + } + + router := chi.NewRouter() + router.Get("/Genres/{name}", h.HandleGenreByName) + + req := httptest.NewRequest("GET", "/Genres/"+url.PathEscape("science fiction"), nil) + req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, &Session{StreamAppUserID: 1, ProfileID: "p1"})) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + if rec.Code != 200 { + t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + var dto baseItemDTO + if err := json.Unmarshal(rec.Body.Bytes(), &dto); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if dto.Type != "Genre" || dto.Name != "Science Fiction" { + t.Fatalf("expected canonical genre DTO, got %+v", dto) + } + if dto.ID != codec.EncodeStringID(EncodedIDGenre, "Science Fiction") { + t.Fatalf("genre ID does not round-trip: %q", dto.ID) + } + + req = httptest.NewRequest("GET", "/Genres/Nope", nil) + req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, &Session{StreamAppUserID: 1, ProfileID: "p1"})) + rec = httptest.NewRecorder() + router.ServeHTTP(rec, req) + if rec.Code != 404 { + t.Fatalf("expected 404 for unknown genre, got %d", rec.Code) + } +} diff --git a/internal/jellycompat/handlers_images.go b/internal/jellycompat/handlers_images.go index 5dd65885..2c52126b 100644 --- a/internal/jellycompat/handlers_images.go +++ b/internal/jellycompat/handlers_images.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" "time" @@ -16,7 +17,6 @@ import ( type ImagesHandler struct { content ContentService codec *ResourceIDCodec - httpClient *http.Client sessions *SessionStore images *ImageCache personRepo *catalog.PersonRepository @@ -29,6 +29,10 @@ type ImagesHandler struct { posterSigner LibraryPosterPresigner presignTTL time.Duration imageTags *imageTagSigner + httpClient *http.Client + // collections is optional; when set, BoxSet (library collection) artwork + // resolves durably instead of depending on the in-memory image cache. + collections collectionSource } type imageItemRepository interface { @@ -48,15 +52,14 @@ type imageFolderRepository interface { GetByID(ctx context.Context, id int) (*models.MediaFolder, error) } -// NewImagesHandler creates an image proxy handler. -func NewImagesHandler(content ContentService, codec *ResourceIDCodec, httpClient *http.Client, sessions *SessionStore, images *ImageCache, personRepo *catalog.PersonRepository, detailSvc *catalog.DetailService, itemRepo *catalog.ItemRepository, folderRepo *catalog.FolderRepository, seasonRepo *catalog.SeasonRepository, episodeRepo *catalog.EpisodeRepository, accessFilter AccessFilterResolver, posterSigner LibraryPosterPresigner, presignTTL time.Duration, imageTagSecret string) *ImagesHandler { +// NewImagesHandler creates a Jellyfin-compatible image route handler. +func NewImagesHandler(content ContentService, codec *ResourceIDCodec, sessions *SessionStore, images *ImageCache, personRepo *catalog.PersonRepository, detailSvc *catalog.DetailService, itemRepo *catalog.ItemRepository, folderRepo *catalog.FolderRepository, seasonRepo *catalog.SeasonRepository, episodeRepo *catalog.EpisodeRepository, accessFilter AccessFilterResolver, posterSigner LibraryPosterPresigner, presignTTL time.Duration, imageTagSecret string, httpClient *http.Client) *ImagesHandler { if httpClient == nil { httpClient = http.DefaultClient } return &ImagesHandler{ content: content, codec: codec, - httpClient: httpClient, sessions: sessions, images: images, personRepo: personRepo, @@ -69,6 +72,7 @@ func NewImagesHandler(content ContentService, codec *ResourceIDCodec, httpClient posterSigner: posterSigner, presignTTL: presignTTL, imageTags: newImageTagSigner(imageTagSecret), + httpClient: httpClient, } } @@ -80,6 +84,10 @@ func (h *ImagesHandler) HandleItemImage(w http.ResponseWriter, r *http.Request) imageType := chiURLParam(r, "imageType") imageSize := compatRequestImageSize(r, imageType) tag := strings.TrimSpace(r.URL.Query().Get("tag")) + if canonicalRouteID, ok := canonicalCompatImageRouteID(h.codec, routeID); ok { + routeID = canonicalRouteID + r = withCompatImageProxyRouteRequest(r) + } if tag != "" { imageURL, ok, err := h.resolveItemImageURLFromTag(r.Context(), routeID, imageType, imageSize, tag) if err != nil { @@ -88,15 +96,15 @@ func (h *ImagesHandler) HandleItemImage(w http.ResponseWriter, r *http.Request) } if ok { h.images.RememberSizedUntil(routeID, imageType, imageURL.URL, imageSize, imageURL.ExpiresAt) - h.proxyImageURL(w, r, imageURL.URL) + h.serveImageURL(w, r, imageURL.URL) return } if imageURL, ok := h.images.LookupTag(tag); ok { - h.proxyImageURL(w, r, imageURL) + h.serveImageURL(w, r, imageURL) return } } else if imageURL, ok := h.images.LookupSized(routeID, imageType, "", imageSize); ok { - h.proxyImageURL(w, r, imageURL) + h.serveImageURL(w, r, imageURL) return } @@ -116,6 +124,11 @@ func (h *ImagesHandler) HandleItemImage(w http.ResponseWriter, r *http.Request) return } + if collectionID, err := h.codec.DecodeStringID(EncodedIDCollection, routeID); err == nil { + h.handleCollectionImage(w, r, session, routeID, imageType, imageSize, collectionID) + return + } + contentID, err := decodeContentID(h.codec, routeID) if err != nil { writeError(w, http.StatusNotFound, "NotFound", "Item not found") @@ -133,7 +146,7 @@ func (h *ImagesHandler) HandleItemImage(w http.ResponseWriter, r *http.Request) return } h.images.RememberSizedUntil(routeID, imageType, imageURL, imageSize, resolvedImage.ExpiresAt) - h.proxyImageURL(w, r, imageURL) + h.serveImageURL(w, r, imageURL) } // handlePersonImage serves person photo images. @@ -159,7 +172,7 @@ func (h *ImagesHandler) handlePersonImage(w http.ResponseWriter, r *http.Request return } h.images.RememberSizedUntil(routeID, imageType, imageURL, imageSize, resolvedImage.ExpiresAt) - h.proxyImageURL(w, r, imageURL) + h.serveImageURL(w, r, imageURL) } func (h *ImagesHandler) resolveItemImageURL(ctx context.Context, session *Session, contentID, imageType string, r *http.Request) (catalog.ResolvedImageURL, error) { @@ -253,6 +266,9 @@ func (h *ImagesHandler) resolveItemImageURLFromTag(ctx context.Context, routeID, if libraryID, err := h.codec.DecodeIntID(EncodedIDLibrary, routeID); err == nil { return h.resolveLibraryImageURLFromTag(ctx, routeID, int(libraryID), imageType, imageSize, tag) } + if collectionID, err := h.codec.DecodeStringID(EncodedIDCollection, routeID); err == nil { + return h.resolveCollectionImageURLFromTag(ctx, routeID, collectionID, imageType, tag) + } contentID, err := decodeContentID(h.codec, routeID) if err != nil { return catalog.ResolvedImageURL{}, false, nil @@ -260,6 +276,95 @@ func (h *ImagesHandler) resolveItemImageURLFromTag(ctx context.Context, routeID, return h.resolveItemImageURLFromReposWithoutSession(ctx, routeID, contentID, imageType, imageSize, tag) } +// collectionArtworkKey returns the stored artwork reference for the requested +// compat image type ("" when the collection has none of that type). +func collectionArtworkKey(c *models.LibraryCollection, imageType string) string { + switch imageType { + case "Primary": + return c.PosterURL + case "Backdrop": + return c.BackdropURL + } + return "" +} + +// presignCollectionArtwork resolves a collection artwork reference like the +// main API's presignGPURL: absolute and app-relative references pass through, +// bare keys presign against the general-purpose bucket. +func (h *ImagesHandler) presignCollectionArtwork(ctx context.Context, path string) string { + path = strings.TrimSpace(path) + if path == "" { + return "" + } + if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "/") { + return path + } + return h.presignLibraryPosterURL(ctx, path) +} + +// resolveCollectionImageURLFromTag serves tag-authenticated BoxSet artwork. +// The tag must match the stable seed boxSetFromCollection signs. +func (h *ImagesHandler) resolveCollectionImageURLFromTag(ctx context.Context, routeID, collectionID, imageType, tag string) (catalog.ResolvedImageURL, bool, error) { + if h.collections == nil { + return catalog.ResolvedImageURL{}, false, nil + } + collection, err := h.collections.GetByID(ctx, collectionID) + if err != nil || collection == nil { + return catalog.ResolvedImageURL{}, false, nil + } + key := collectionArtworkKey(collection, imageType) + if key == "" || !h.imageTags.Equal( + imageTagSeed(routeID, imageType, compatCardImageSize, key, "", time.Time{}), + "", + tag, + ) { + return catalog.ResolvedImageURL{}, false, nil + } + imageURL := h.presignCollectionArtwork(ctx, key) + if imageURL == "" { + return catalog.ResolvedImageURL{}, false, nil + } + return catalog.ResolvedImageURL{URL: imageURL}, true, nil +} + +// handleCollectionImage serves session-authenticated BoxSet artwork, applying +// the same visibility rules as the BoxSet item endpoints. +func (h *ImagesHandler) handleCollectionImage(w http.ResponseWriter, r *http.Request, session *Session, routeID, imageType, imageSize, collectionID string) { + if h.collections == nil { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + collection, err := h.collections.GetByID(r.Context(), collectionID) + if err != nil { + if errors.Is(err, catalog.ErrLibraryCollectionNotFound) { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + writeCompatUpstreamError(w, err) + return + } + if collection == nil || !strings.EqualFold(collection.Visibility, "visible") { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + visible, err := visibleLibraryIDSet(r.Context(), h.content, session) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + if !collectionVisible(collection, visible) { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + imageURL := h.presignCollectionArtwork(r.Context(), collectionArtworkKey(collection, imageType)) + if imageURL == "" { + writeError(w, http.StatusNotFound, "NotFound", "Image not found") + return + } + h.images.RememberSized(routeID, imageType, imageURL, imageSize) + h.serveImageURL(w, r, imageURL) +} + func (h *ImagesHandler) resolveLibraryImageURLFromTag(ctx context.Context, routeID string, libraryID int, imageType, _ string, tag string) (catalog.ResolvedImageURL, bool, error) { if imageType != "Primary" || h.folderRepo == nil || h.posterSigner == nil { return catalog.ResolvedImageURL{}, false, nil @@ -414,18 +519,45 @@ func firstResolvedImageURL(values ...catalog.ResolvedImageURL) catalog.ResolvedI return catalog.ResolvedImageURL{} } +func (h *ImagesHandler) serveImageURL(w http.ResponseWriter, r *http.Request, imageURL string) { + if shouldProxyCompatImageRequest(r) { + h.proxyImageURL(w, r, imageURL) + return + } + h.redirectImageURL(w, r, imageURL) +} + +func (h *ImagesHandler) redirectImageURL(w http.ResponseWriter, r *http.Request, imageURL string) { + if _, err := parseRemoteImageURL(imageURL); err != nil { + writeError(w, http.StatusBadGateway, "UpstreamError", "Failed to load image") + return + } + + // Do not let clients cache the temporary redirect itself. The object-store + // response can still carry its own cache headers after the client follows it. + setCompatImageRouteNoStore(w.Header()) + http.Redirect(w, r, imageURL, http.StatusFound) +} + func (h *ImagesHandler) proxyImageURL(w http.ResponseWriter, r *http.Request, imageURL string) { - req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, imageURL, nil) + target, err := parseRemoteImageURL(imageURL) if err != nil { writeError(w, http.StatusBadGateway, "UpstreamError", "Failed to load image") return } - for _, header := range []string{"If-None-Match", "If-Modified-Since"} { - if value := r.Header.Get(header); value != "" { - req.Header.Set(header, value) - } + + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target.String(), nil) + if err != nil { + writeError(w, http.StatusBadGateway, "UpstreamError", "Failed to load image") + return } - resp, err := h.httpClient.Do(req) + copyConditionalImageRequestHeaders(req.Header, r.Header) + + client := h.httpClient + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(req) if err != nil { writeError(w, http.StatusBadGateway, "UpstreamError", "Failed to load image") return @@ -433,6 +565,15 @@ func (h *ImagesHandler) proxyImageURL(w http.ResponseWriter, r *http.Request, im proxyImage(w, resp) } +func parseRemoteImageURL(imageURL string) (*url.URL, error) { + target, err := url.Parse(imageURL) + if err != nil || target.Scheme == "" || target.Host == "" || + (target.Scheme != "http" && target.Scheme != "https") { + return nil, errors.New("invalid remote image URL") + } + return target, nil +} + // HandleUserImage returns a deterministic placeholder avatar. func (h *ImagesHandler) HandleUserImage(w http.ResponseWriter, r *http.Request) { session := SessionFromContext(r.Context()) diff --git a/internal/jellycompat/handlers_items.go b/internal/jellycompat/handlers_items.go index cd729145..c37c8566 100644 --- a/internal/jellycompat/handlers_items.go +++ b/internal/jellycompat/handlers_items.go @@ -41,6 +41,11 @@ type ItemsHandler struct { accessFilter AccessFilterResolver subtitleRepo subtitles.Repository recommender recommendations.Recommender + // collections is optional; when set, library collections are exposed as + // Jellyfin BoxSets. posterPresigner/presignTTL resolve their artwork keys. + collections collectionSource + posterPresigner LibraryPosterPresigner + presignTTL time.Duration // FileResolver is optional; when set, /MediaSegments returns real intro/ // credits/recap/preview segments for any file that has them. FileResolver FilePathResolver @@ -130,8 +135,25 @@ func (h *ItemsHandler) HandleItems(w http.ResponseWriter, r *http.Request) { query := parseItemsQuery(r, h.codec) switch { - case len(query.specificIDs) > 0: + case len(query.specificIDs) > 0 || len(query.specificCollectionIDs) > 0: h.handleSpecificItems(w, r, session, query) + case query.parentCollectionID != "": + h.handleBoxSetChildren(w, r, session, query) + case query.hasItemTypeFilter && len(query.itemTypes) == 0: + // Every requested type is one catalog browse cannot serve. BoxSet has + // its own listing (unless a user-state filter applies — collections + // carry no favorite/played state, so those return empty, matching the + // pre-existing favorites guard); CollectionFolder means the library + // views; anything else (Playlist, MusicAlbum, ...) is empty. + hasUserStateFilter := query.isFavorite || query.isResumable || query.isPlayed != nil + switch { + case query.wantsBoxSets && !hasUserStateFilter: + h.handleBoxSetsList(w, r, session, query) + case query.wantsViews && !hasUserStateFilter && query.searchTerm == "": + h.handleViewsResponse(w, r, session) + default: + writeJSON(w, http.StatusOK, emptyQueryResult(query.startIndex)) + } case query.isResumable: h.handleResumeResponse(w, r, session, query) case query.isPlayed != nil && *query.isPlayed: @@ -140,6 +162,8 @@ func (h *ItemsHandler) HandleItems(w http.ResponseWriter, r *http.Request) { h.handleSearchItems(w, r, session, query) case query.isFavorite: h.handleFavoriteItems(w, r, session, query) + case isSeasonChildItemsQuery(query): + h.handleSeasonChildItems(w, r, session, query) case query.parentLibraryID == 0 && len(query.itemTypes) == 0: // No ParentId and no type filter: return top-level library views. // Jellyfin clients (e.g. Findroid "My Media") call GET /Items?userId=... @@ -150,6 +174,27 @@ func (h *ItemsHandler) HandleItems(w http.ResponseWriter, r *http.Request) { } } +// emptyQueryResult is the canonical empty /Items page. +func emptyQueryResult(startIndex int) queryResultDTO { + return queryResultDTO{ + Items: []baseItemDTO{}, + TotalRecordCount: 0, + StartIndex: startIndex, + } +} + +func isSeasonChildItemsQuery(query itemsQuery) bool { + if query.parentItemID == "" { + return false + } + for _, itemType := range query.itemTypes { + if itemType == "season" { + return true + } + } + return false +} + // HandleItem serves GET /Items/{id}. func (h *ItemsHandler) HandleItem(w http.ResponseWriter, r *http.Request) { session := SessionFromContext(r.Context()) @@ -170,6 +215,11 @@ func (h *ItemsHandler) HandleItem(w http.ResponseWriter, r *http.Request) { return } + if collectionID, err := h.codec.DecodeStringID(EncodedIDCollection, rawID); err == nil { + h.handleBoxSetItem(w, r, session, collectionID) + return + } + if mediaSourceID, err := h.codec.DecodeIntID(EncodedIDMediaSource, rawID); err == nil { contentID, ok := h.codec.LookupMediaSourceOwner(mediaSourceID) if !ok { @@ -209,6 +259,7 @@ func (h *ItemsHandler) HandleItem(w http.ResponseWriter, r *http.Request) { browsableSeasons := filterBrowsableSeasons(seasons) dto.ChildCount = len(browsableSeasons) dto.RecursiveItemCount = len(browsableSeasons) + dto.SeasonCount = len(browsableSeasons) } } if strings.EqualFold(detail.Type, "episode") && detail.SeriesID != "" { @@ -542,6 +593,23 @@ func (h *ItemsHandler) HandleItemStub(w http.ResponseWriter, r *http.Request) { }) } +// HandleThemeSongsStub serves an empty ThemeMediaResult for +// /Items/{id}/ThemeSongs. It cannot share HandleItemStub because this +// response shape additionally requires OwnerId (see themeMediaResultDTO). +func (h *ItemsHandler) HandleThemeSongsStub(w http.ResponseWriter, r *http.Request) { + session := SessionFromContext(r.Context()) + if session == nil { + writeError(w, http.StatusUnauthorized, "Unauthorized", "Missing authentication token") + return + } + writeJSON(w, http.StatusOK, themeMediaResultDTO{ + Items: []baseItemDTO{}, + TotalRecordCount: 0, + StartIndex: 0, + OwnerID: chi.URLParam(r, "id"), + }) +} + // mediaSegmentDTO mirrors Jellyfin's MediaSegmentDto shape. Times are // expressed in 100-nanosecond ticks (the convention shared with RunTimeTicks // and chapter position fields). @@ -887,6 +955,46 @@ func (h *ItemsHandler) HandleGenres(w http.ResponseWriter, r *http.Request) { }) } +// HandleGenreByName serves GET /Genres/{name}. Jellyfin addresses genres by +// (URL-escaped) display name; clients use the returned Id for GenreIds= item +// queries. +func (h *ItemsHandler) HandleGenreByName(w http.ResponseWriter, r *http.Request) { + session := SessionFromContext(r.Context()) + if session == nil { + writeError(w, http.StatusUnauthorized, "Unauthorized", "Missing authentication token") + return + } + + name := strings.TrimSpace(chi.URLParam(r, "name")) + if decoded, err := url.PathUnescape(name); err == nil { + name = strings.TrimSpace(decoded) + } + if name == "" { + writeError(w, http.StatusNotFound, "NotFound", "Genre not found") + return + } + + // Confirm the genre exists within the caller's visible scope so the + // response carries the canonical casing. + filters, err := h.content.ListItemFilters(r.Context(), session, urlValuesFromItemsQuery(parseItemsQuery(r, h.codec))) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + for _, genre := range filters.Genres { + if strings.EqualFold(strings.TrimSpace(genre), name) { + writeJSON(w, http.StatusOK, baseItemDTO{ + ID: h.codec.EncodeStringID(EncodedIDGenre, genre), + Type: "Genre", + Name: genre, + ServerID: h.mapper.serverID, + }) + return + } + } + writeError(w, http.StatusNotFound, "NotFound", "Genre not found") +} + // HandleSeasons serves GET /Shows/{id}/Seasons. func (h *ItemsHandler) HandleSeasons(w http.ResponseWriter, r *http.Request) { defer func() { @@ -919,9 +1027,35 @@ func (h *ItemsHandler) HandleSeasons(w http.ResponseWriter, r *http.Request) { writeCompatUpstreamError(w, err) return } + h.writeSeasonItemsResponse(w, r, session, seriesID, seasons, query, false) +} + +func (h *ItemsHandler) handleSeasonChildItems(w http.ResponseWriter, r *http.Request, session *Session, query itemsQuery) { + seasons, err := h.content.ListSeasons(r.Context(), session, query.parentItemID, nil) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + h.writeSeasonItemsResponse(w, r, session, query.parentItemID, seasons, query, true) +} + +func (h *ItemsHandler) writeSeasonItemsResponse(w http.ResponseWriter, r *http.Request, session *Session, seriesID string, seasons []upstreamSeason, query itemsQuery, page bool) { seasons = filterBrowsableSeasons(seasons) h.rememberSeasonImages(seasons, seriesID) + total := len(seasons) + if page { + start := query.startIndex + if start > total { + start = total + } + end := total + if query.limit > 0 && start+query.limit < end { + end = start + query.limit + } + seasons = seasons[start:end] + } + favorites, err := resolveFavoritesForContentIDs(r.Context(), session, h.userData, seasonContentIDs(seasons)) if err != nil { writeCompatUpstreamError(w, err) @@ -944,10 +1078,16 @@ func (h *ItemsHandler) HandleSeasons(w http.ResponseWriter, r *http.Request) { } items = append(items, h.mapper.seasonFromUpstream(season, seriesID, favorites[season.ContentID])) } + applyImageTypeLimit(items, query.imageTypeLimit) + + startIndex := 0 + if page { + startIndex = query.startIndex + } writeJSON(w, http.StatusOK, queryResultDTO{ Items: items, - TotalRecordCount: len(items), - StartIndex: 0, + TotalRecordCount: total, + StartIndex: startIndex, }) } @@ -1074,6 +1214,7 @@ func (h *ItemsHandler) HandleEpisodes(w http.ResponseWriter, r *http.Request) { if target, ok := episodeTargets[episode.ContentID]; ok { upstreamEpisode.StillURL = firstNonEmpty(target.Item.StillURL, target.Item.PosterURL, upstreamEpisode.StillURL) upstreamEpisode.SeriesTitle = firstNonEmpty(target.Item.SeriesTitle, upstreamEpisode.SeriesTitle) + upstreamEpisode.HasMediaFiles = target.Item.HasMediaFiles } dto := h.mapper.episodeFromUpstream(upstreamEpisode, favorites[episode.ContentID], progress[episode.ContentID]) dto.SeasonName = firstNonEmpty(seasonTitleByID[episode.SeasonID], seasonTitleByNumber[episode.SeasonNumber]) @@ -1467,6 +1608,7 @@ func (h *ItemsHandler) handleFavoriteItems(w http.ResponseWriter, r *http.Reques AllowedLibraryIDs: access.AllowedLibraryIDs, DisabledLibraryIDs: access.DisabledLibraryIDs, MaxContentRating: clampMaxContentRating(access.MaxContentRating, query.maxOfficialRating), + ExcludedMediaTypes: access.ExcludedMediaTypes, SortField: query.sort, SortOrder: query.order, Limit: query.limit, @@ -1670,6 +1812,16 @@ func (h *ItemsHandler) handleSpecificItems(w http.ResponseWriter, r *http.Reques h.appendDownloadedSubtitlesToDetailDTO(r.Context(), detail.ContentID, detail.Versions, &dto) items = append(items, dto) } + + // Ids= may also reference collections (BoxSet route IDs handed out by the + // collections listing); append their DTOs so clients can re-hydrate them. + boxSets, err := h.boxSetsByIDs(r.Context(), session, query.specificCollectionIDs) + if err != nil { + writeCompatUpstreamError(w, err) + return + } + items = append(items, boxSets...) + writeJSON(w, http.StatusOK, queryResultDTO{ Items: items, TotalRecordCount: len(items), @@ -1771,7 +1923,12 @@ type progressHydratedItem struct { const maxDetailUpgrades = 100 func (h *ItemsHandler) loadProgressPage(ctx context.Context, session *Session, status string, query itemsQuery, typeSet map[string]bool, libraryID *int) ([]baseItemDTO, int, error) { - if len(typeSet) == 0 && libraryID == nil && !query.enableTotalRecordCount { + // Resume views hide dismissed and superseded entries, so the visible list + // is sparser than the raw store list. The raw-offset fast path below is + // only safe for the first page; deeper StartIndex values must go through + // the scan-from-zero branch, which paginates over visible entries. + resumeFiltered := status == "in_progress" + if len(typeSet) == 0 && libraryID == nil && !query.enableTotalRecordCount && (!resumeFiltered || query.startIndex == 0) { batchSize := min(max(query.limit*2, 48), 200) if batchSize <= 0 { batchSize = 48 @@ -1787,6 +1944,13 @@ func (h *ItemsHandler) loadProgressPage(ctx context.Context, session *Session, s if len(progressEntries) == 0 { break } + rawCount := len(progressEntries) + if resumeFiltered { + progressEntries, err = h.userData.FilterResumeProgress(ctx, session, progressEntries) + if err != nil { + return nil, 0, err + } + } items, err := h.hydrateProgressItems(ctx, session, progressEntries, query.requestedFields, libraryID) if err != nil { @@ -1798,10 +1962,10 @@ func (h *ItemsHandler) loadProgressPage(ctx context.Context, session *Session, s } result = append(result, item) } - if len(result) >= query.limit || len(progressEntries) < batchSize { + if len(result) >= query.limit || rawCount < batchSize { break } - offset += len(progressEntries) + offset += rawCount } return h.finishProgressPage(ctx, session, result, query, libraryID), 0, nil } @@ -1823,6 +1987,13 @@ func (h *ItemsHandler) loadProgressPage(ctx context.Context, session *Session, s if len(progressEntries) == 0 { break } + rawCount := len(progressEntries) + if resumeFiltered { + progressEntries, err = h.userData.FilterResumeProgress(ctx, session, progressEntries) + if err != nil { + return nil, 0, err + } + } hydrated, err := h.hydrateProgressItems(ctx, session, progressEntries, query.requestedFields, libraryID) if err != nil { @@ -1838,8 +2009,8 @@ func (h *ItemsHandler) loadProgressPage(ctx context.Context, session *Session, s matchedCount++ } - offset += len(progressEntries) - if len(progressEntries) < batchSize { + offset += rawCount + if rawCount < batchSize { break } if !query.enableTotalRecordCount && len(items) >= query.limit { @@ -1957,9 +2128,9 @@ func (h *ItemsHandler) hydrateProgressItems(ctx context.Context, session *Sessio func (h *ItemsHandler) resolveAccessFilter(ctx context.Context, session *Session) catalog.AccessFilter { if h.accessFilter != nil { - return h.accessFilter(ctx, session.StreamAppUserID, session.ProfileID) + return withCompatAccessExclusions(h.accessFilter(ctx, session.StreamAppUserID, session.ProfileID)) } - return catalog.AccessFilter{} + return withCompatAccessExclusions(catalog.AccessFilter{}) } func (h *ItemsHandler) presignCompatListItem(ctx context.Context, item *upstreamListItem) { diff --git a/internal/jellycompat/handlers_items_test.go b/internal/jellycompat/handlers_items_test.go index 91006012..7826ab85 100644 --- a/internal/jellycompat/handlers_items_test.go +++ b/internal/jellycompat/handlers_items_test.go @@ -2,6 +2,7 @@ package jellycompat import ( "context" + "encoding/json" "net/http/httptest" "net/url" "strings" @@ -19,7 +20,10 @@ import ( type countingContentService struct { episodeDetail *upstreamItemDetail seriesDetail *upstreamItemDetail + seasons []upstreamSeason getItemDetailCalls int + listSeasonsCalls int + listSeasonsSeries string } func (s *countingContentService) GetItemDetail(_ context.Context, _ *Session, contentID string, _ *int) (*upstreamItemDetail, error) { @@ -50,8 +54,15 @@ func (s *countingContentService) SearchItems(context.Context, *Session, string, panic("unused") } -func (s *countingContentService) ListSeasons(context.Context, *Session, string, *int) ([]upstreamSeason, error) { - panic("unused") +func (s *countingContentService) ListSeasons(_ context.Context, _ *Session, seriesID string, _ *int) ([]upstreamSeason, error) { + s.listSeasonsCalls++ + s.listSeasonsSeries = seriesID + if s.seasons == nil { + panic("unused") + } + out := make([]upstreamSeason, len(s.seasons)) + copy(out, s.seasons) + return out, nil } func (s *countingContentService) GetSeason(context.Context, *Session, string, int, *int) (*upstreamSeason, error) { @@ -70,6 +81,65 @@ func (s *countingContentService) ListItemFilters(context.Context, *Session, url. panic("unused") } +func TestHandleItems_SeriesParentSeasonFilterReturnsPagedSeasons(t *testing.T) { + codec := NewResourceIDCodec() + seriesContentID := "series-1" + encodedSeriesID := codec.EncodeStringID(EncodedIDItem, seriesContentID) + contentSvc := &countingContentService{ + seasons: []upstreamSeason{ + {ContentID: "season-1", SeasonNumber: 1, Title: "Season 1", EpisodeCount: 10}, + {ContentID: "season-2", SeasonNumber: 2, Title: "Season 2", EpisodeCount: 8}, + }, + } + + h := &ItemsHandler{ + content: contentSvc, + userData: &mockUserDataService{}, + codec: codec, + mapper: newMapper(codec, &config.Config{}), + images: NewImageCache(time.Hour, time.Now), + } + + req := httptest.NewRequest("GET", "/Users/test/Items?ParentId="+encodedSeriesID+ + "&IncludeItemTypes=Season&Recursive=false&SortBy=IndexNumber&SortOrder=Ascending"+ + "&Fields=PrimaryImageAspectRatio,CanDelete&StartIndex=1&Limit=1", nil) + req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, &Session{ + StreamAppUserID: 1, + ProfileID: "profile-1", + })) + + rec := httptest.NewRecorder() + h.HandleItems(rec, req) + + if rec.Code != 200 { + t.Fatalf("expected status 200; got %d, body=%s", rec.Code, rec.Body.String()) + } + if contentSvc.listSeasonsCalls != 1 || contentSvc.listSeasonsSeries != seriesContentID { + t.Fatalf("ListSeasons calls = %d for %q, want 1 for %q", + contentSvc.listSeasonsCalls, contentSvc.listSeasonsSeries, seriesContentID) + } + + var result queryResultDTO + if err := json.NewDecoder(rec.Body).Decode(&result); err != nil { + t.Fatalf("decode response: %v", err) + } + if result.TotalRecordCount != 2 || result.StartIndex != 1 { + t.Fatalf("TotalRecordCount/StartIndex = %d/%d, want 2/1", + result.TotalRecordCount, result.StartIndex) + } + if len(result.Items) != 1 { + t.Fatalf("len(Items) = %d, want 1", len(result.Items)) + } + item := result.Items[0] + if item.Type != "Season" || item.Name != "Season 2" { + t.Fatalf("item = {%q %q}, want Season/Season 2", item.Type, item.Name) + } + if item.ParentID != encodedSeriesID || item.SeriesID != encodedSeriesID { + t.Fatalf("ParentID/SeriesID = %q/%q, want %q/%q", + item.ParentID, item.SeriesID, encodedSeriesID, encodedSeriesID) + } +} + // TestHandleItem_Episode_FetchesSeriesDetailForStableParentImageTags verifies // that episode detail responses fetch parent series image metadata even when // image URLs are already cached. Cached URLs are not enough to build stable diff --git a/internal/jellycompat/handlers_playback.go b/internal/jellycompat/handlers_playback.go index 6c64bf85..aa90ebe2 100644 --- a/internal/jellycompat/handlers_playback.go +++ b/internal/jellycompat/handlers_playback.go @@ -439,7 +439,7 @@ func (h *PlaybackHandler) HandlePlaybackInfo(w http.ResponseWriter, r *http.Requ allow4KTranscode := h.allow4KVideoTranscode(r.Context()) for _, version := range detail.Versions { source := h.buildPlaybackSource(routeItemID, playSessionID, version, profile, req, allow4KTranscode) - if req.MediaSourceID != "" && source.ID != req.MediaSourceID { + if req.MediaSourceID != "" && !mediaSourceIDsEqual(source.ID, req.MediaSourceID) { continue } sources = append(sources, source) diff --git a/internal/jellycompat/idcodec.go b/internal/jellycompat/idcodec.go index ccf3d167..cf6942d2 100644 --- a/internal/jellycompat/idcodec.go +++ b/internal/jellycompat/idcodec.go @@ -21,6 +21,8 @@ const ( EncodedIDGenre EncodedIDType = 6 EncodedIDStudio EncodedIDType = 7 EncodedIDPerson EncodedIDType = 8 + EncodedIDImageProxy EncodedIDType = 9 + EncodedIDCollection EncodedIDType = 10 ) var ( @@ -32,6 +34,7 @@ var ( EncodedIDGenre: uuid.MustParse("c0cbb8ea-8331-52c0-b160-15e7cf899fb0"), EncodedIDStudio: uuid.MustParse("23712982-b769-592d-9360-b4d3f39654db"), EncodedIDPerson: uuid.MustParse("a4e7c1d6-3b8f-5a2e-9c01-7d6f4e8b2a13"), + EncodedIDCollection: uuid.MustParse("7f3c2a91-5b64-5c1d-8e07-9a2f4d6b1c35"), } ) @@ -141,6 +144,21 @@ func (c *ResourceIDCodec) LookupMediaSourceOwner(fileID int64) (string, bool) { return contentID, ok } +// mediaSourceIDsEqual reports whether two media-source IDs refer to the same +// source, tolerating UUID format differences. Silo exposes the canonical +// dashed compat UUID (e.g. "03000000-0000-0000-0000-00000019e8c2"), but some +// Jellyfin clients (e.g. Wholphin) echo it back in the compact 32-char hex +// form ("0300000000000000000000000019e8c2"). Both parse to the same UUID, so +// matching must compare the parsed values rather than the raw strings. +func mediaSourceIDsEqual(a, b string) bool { + if a == b { + return true + } + ua, errA := uuid.Parse(a) + ub, errB := uuid.Parse(b) + return errA == nil && errB == nil && ua == ub +} + // DecodeID unpacks a compat UUID into its original numeric value. func DecodeID(raw string) (DecodedID, error) { parsed, err := uuid.Parse(raw) diff --git a/internal/jellycompat/idcodec_mediasource_test.go b/internal/jellycompat/idcodec_mediasource_test.go new file mode 100644 index 00000000..c24802e1 --- /dev/null +++ b/internal/jellycompat/idcodec_mediasource_test.go @@ -0,0 +1,63 @@ +package jellycompat + +import "testing" + +func TestMediaSourceIDsEqual(t *testing.T) { + cases := []struct { + name string + a string + b string + want bool + }{ + { + name: "identical dashed", + a: "03000000-0000-0000-0000-00000019e8c2", + b: "03000000-0000-0000-0000-00000019e8c2", + want: true, + }, + { + name: "dashed vs compact (Wholphin)", + a: "03000000-0000-0000-0000-00000019e8c2", + b: "0300000000000000000000000019e8c2", + want: true, + }, + { + name: "compact vs dashed", + a: "0300000000000000000000000019e8c2", + b: "03000000-0000-0000-0000-00000019e8c2", + want: true, + }, + { + name: "case insensitive hex", + a: "03000000-0000-0000-0000-00000019E8C2", + b: "0300000000000000000000000019e8c2", + want: true, + }, + { + name: "different uuids", + a: "03000000-0000-0000-0000-00000019e8c2", + b: "0300000000000000000000000019e8c3", + want: false, + }, + { + name: "non-uuid falls back to exact match", + a: "abc", + b: "abc", + want: true, + }, + { + name: "non-uuid mismatch", + a: "abc", + b: "def", + want: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := mediaSourceIDsEqual(tc.a, tc.b); got != tc.want { + t.Fatalf("mediaSourceIDsEqual(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) + } + }) + } +} diff --git a/internal/jellycompat/image_cache.go b/internal/jellycompat/image_cache.go index ab0939f4..177e8b95 100644 --- a/internal/jellycompat/image_cache.go +++ b/internal/jellycompat/image_cache.go @@ -93,7 +93,7 @@ func (c *ImageCache) LookupSized(routeID, imageType, tag, size string) (string, return "", false } - if tag = strings.TrimSpace(tag); tag != "" { + if tag = canonicalCompatImageTag(tag); tag != "" { return c.LookupTag(tag) } @@ -108,7 +108,7 @@ func (c *ImageCache) LookupTag(tag string) (string, bool) { if c == nil { return "", false } - return c.lookupTag(strings.TrimSpace(tag)) + return c.lookupTag(canonicalCompatImageTag(tag)) } // lookupTag resolves a tag without size partitioning. Tags are sha1 of the diff --git a/internal/jellycompat/image_proxy_tags.go b/internal/jellycompat/image_proxy_tags.go new file mode 100644 index 00000000..79711304 --- /dev/null +++ b/internal/jellycompat/image_proxy_tags.go @@ -0,0 +1,233 @@ +package jellycompat + +import ( + "bytes" + "encoding/json" + "net/http" + "strings" +) + +func compatImageProxyTagVariantMiddleware(codec *ResourceIDCodec) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || !isCompatImageProxyClientRequest(r) || isWebSocketUpgrade(r) { + next.ServeHTTP(w, r) + return + } + + rw := &compatImageProxyTagResponseWriter{ResponseWriter: w, codec: codec} + next.ServeHTTP(rw, r) + rw.finish() + }) + } +} + +type compatImageProxyTagResponseWriter struct { + http.ResponseWriter + status int + body bytes.Buffer + passthrough bool + codec *ResourceIDCodec +} + +func (w *compatImageProxyTagResponseWriter) WriteHeader(status int) { + if w.passthrough { + w.ResponseWriter.WriteHeader(status) + return + } + w.status = status +} + +func (w *compatImageProxyTagResponseWriter) Write(p []byte) (int, error) { + if w.passthrough { + return w.ResponseWriter.Write(p) + } + if w.status == 0 { + w.status = http.StatusOK + } + if !isJSONResponse(w.Header().Get("Content-Type")) { + w.passthrough = true + w.ResponseWriter.WriteHeader(w.status) + return w.ResponseWriter.Write(p) + } + return w.body.Write(p) +} + +func (w *compatImageProxyTagResponseWriter) finish() { + if w.passthrough { + return + } + if w.status == 0 && w.body.Len() == 0 { + return + } + status := w.status + if status == 0 { + status = http.StatusOK + } + body := w.body.Bytes() + if isJSONResponse(w.Header().Get("Content-Type")) && len(body) > 0 { + body = rewriteCompatImageProxyTags(w.codec, body) + } + w.ResponseWriter.WriteHeader(status) + if len(body) > 0 { + _, _ = w.ResponseWriter.Write(body) + } +} + +func isJSONResponse(contentType string) bool { + contentType = strings.ToLower(strings.TrimSpace(contentType)) + return contentType == "application/json" || strings.HasPrefix(contentType, "application/json;") +} + +func isWebSocketUpgrade(r *http.Request) bool { + return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") +} + +func rewriteCompatImageProxyTags(codec *ResourceIDCodec, body []byte) []byte { + var value any + if err := json.Unmarshal(body, &value); err != nil { + return body + } + if !appendCompatImageProxyTags(codec, value) { + return body + } + rewritten, err := json.Marshal(value) + if err != nil { + return body + } + return append(rewritten, '\n') +} + +func appendCompatImageProxyTags(codec *ResourceIDCodec, value any) bool { + switch typed := value.(type) { + case map[string]any: + changed := false + hasPrimaryImageTag := compatMapHasPrimaryImageTag(typed) + for key, child := range typed { + switch key { + case "ImageTags": + changed = appendCompatImageProxyTagsInMap(child) || changed + case "ImageBlurHashes": + changed = appendCompatImageProxyTagsInBlurHashes(child) || changed + case "BackdropImageTags", "ParentBackdropImageTags": + changed = appendCompatImageProxyTagsInSlice(child) || changed + case "PrimaryImageTag", "SeriesPrimaryImageTag", "ParentThumbImageTag", "BackdropImageTag", "ImageTag": + if tag, ok := child.(string); ok && tag != "" { + typed[key] = compatImageProxyTag(tag) + changed = true + } + default: + changed = appendCompatImageProxyTags(codec, child) || changed + } + } + if hasPrimaryImageTag { + changed = appendCompatPrimaryImageItemID(codec, typed) || changed + } + return changed + case []any: + changed := false + for _, child := range typed { + changed = appendCompatImageProxyTags(codec, child) || changed + } + return changed + default: + return false + } +} + +func compatMapHasPrimaryImageTag(value map[string]any) bool { + if tags, ok := value["ImageTags"].(map[string]any); ok { + if tag, ok := tags["Primary"].(string); ok && strings.TrimSpace(tag) != "" { + return true + } + } + for _, key := range []string{"PrimaryImageTag", "ImageTag"} { + if tag, ok := value[key].(string); ok && strings.TrimSpace(tag) != "" { + return true + } + } + return false +} + +func appendCompatPrimaryImageItemID(codec *ResourceIDCodec, value map[string]any) bool { + if codec == nil { + return false + } + routeID := compatStringMapValue(value, "PrimaryImageItemId") + if routeID == "" { + routeID = compatStringMapValue(value, "Id") + } + if routeID == "" { + routeID = compatStringMapValue(value, "ItemId") + } + if routeID == "" { + return false + } + proxyRouteID := compatImageProxyRouteID(codec, routeID) + if proxyRouteID == "" || proxyRouteID == routeID { + return false + } + if compatStringMapValue(value, "PrimaryImageItemId") == proxyRouteID { + return false + } + value["PrimaryImageItemId"] = proxyRouteID + return true +} + +func compatStringMapValue(value map[string]any, key string) string { + if raw, ok := value[key].(string); ok { + return strings.TrimSpace(raw) + } + return "" +} + +func appendCompatImageProxyTagsInMap(value any) bool { + tags, ok := value.(map[string]any) + if !ok { + return false + } + changed := false + for key, raw := range tags { + if tag, ok := raw.(string); ok && tag != "" { + tags[key] = compatImageProxyTag(tag) + changed = true + } + } + return changed +} + +func appendCompatImageProxyTagsInSlice(value any) bool { + tags, ok := value.([]any) + if !ok { + return false + } + changed := false + for idx, raw := range tags { + if tag, ok := raw.(string); ok && tag != "" { + tags[idx] = compatImageProxyTag(tag) + changed = true + } + } + return changed +} + +func appendCompatImageProxyTagsInBlurHashes(value any) bool { + byImageType, ok := value.(map[string]any) + if !ok { + return false + } + changed := false + for imageType, rawTags := range byImageType { + tags, ok := rawTags.(map[string]any) + if !ok { + continue + } + next := make(map[string]any, len(tags)) + for tag, hash := range tags { + next[compatImageProxyTag(tag)] = hash + changed = true + } + byImageType[imageType] = next + } + return changed +} diff --git a/internal/jellycompat/image_proxy_tags_test.go b/internal/jellycompat/image_proxy_tags_test.go new file mode 100644 index 00000000..aaa8a07e --- /dev/null +++ b/internal/jellycompat/image_proxy_tags_test.go @@ -0,0 +1,111 @@ +package jellycompat + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestCompatImageProxyTagVariantMiddlewareSuffixesInfuseImageTags(t *testing.T) { + codec := NewResourceIDCodec() + handler := compatImageProxyTagVariantMiddleware(codec)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, queryResultDTO{ + Items: []baseItemDTO{{ + ID: "item-1", + Name: "Movie", + ImageTags: map[string]string{"Primary": "primary-tag"}, + BackdropImageTags: []string{"backdrop-tag"}, + SeriesPrimaryImageTag: "series-tag", + ParentBackdropImageTags: []string{"parent-backdrop-tag"}, + ParentThumbImageTag: "parent-thumb-tag", + ImageBlurHashes: map[string]map[string]string{ + "Primary": {"primary-tag": "thumbhash"}, + }, + }}, + TotalRecordCount: 1, + }) + })) + + req := httptest.NewRequest(http.MethodGet, "/Items", nil) + req.Header.Set("User-Agent", "Infuse-Direct/8.4.6") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) + } + var got queryResultDTO + if err := json.NewDecoder(rec.Body).Decode(&got); err != nil { + t.Fatalf("decode response: %v", err) + } + item := got.Items[0] + if item.ImageTags["Primary"] != "primary-tag-p" { + t.Fatalf("primary tag = %q, want primary-tag-p", item.ImageTags["Primary"]) + } + if item.PrimaryImageItemID != compatImageProxyRouteID(codec, "item-1") { + t.Fatalf("PrimaryImageItemId = %q, want proxy route id", item.PrimaryImageItemID) + } + if canonical, ok := canonicalCompatImageRouteID(codec, item.PrimaryImageItemID); !ok || canonical != "item-1" { + t.Fatalf("canonical proxy route = %q, %v; want item-1, true", canonical, ok) + } + if item.BackdropImageTags[0] != "backdrop-tag-p" { + t.Fatalf("backdrop tag = %q, want backdrop-tag-p", item.BackdropImageTags[0]) + } + if item.SeriesPrimaryImageTag != "series-tag-p" { + t.Fatalf("series tag = %q, want series-tag-p", item.SeriesPrimaryImageTag) + } + if item.ParentBackdropImageTags[0] != "parent-backdrop-tag-p" { + t.Fatalf("parent backdrop tag = %q, want parent-backdrop-tag-p", item.ParentBackdropImageTags[0]) + } + if item.ParentThumbImageTag != "parent-thumb-tag-p" { + t.Fatalf("parent thumb tag = %q, want parent-thumb-tag-p", item.ParentThumbImageTag) + } + if _, ok := item.ImageBlurHashes["Primary"]["primary-tag-p"]; !ok { + t.Fatalf("blurhash keys = %#v, want suffixed primary tag", item.ImageBlurHashes["Primary"]) + } +} + +func TestCompatImageProxyTagVariantMiddlewareLeavesOtherClientsUnchanged(t *testing.T) { + codec := NewResourceIDCodec() + handler := compatImageProxyTagVariantMiddleware(codec)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, queryResultDTO{ + Items: []baseItemDTO{{ + ID: "item-1", + Name: "Movie", + ImageTags: map[string]string{"Primary": "primary-tag"}, + }}, + TotalRecordCount: 1, + }) + })) + + req := httptest.NewRequest(http.MethodGet, "/Items", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) + } + var got queryResultDTO + if err := json.NewDecoder(rec.Body).Decode(&got); err != nil { + t.Fatalf("decode response: %v", err) + } + if got.Items[0].ImageTags["Primary"] != "primary-tag" { + t.Fatalf("primary tag = %q, want primary-tag", got.Items[0].ImageTags["Primary"]) + } + if got.Items[0].PrimaryImageItemID != "" { + t.Fatalf("PrimaryImageItemId = %q, want empty", got.Items[0].PrimaryImageItemID) + } +} + +func TestCompatImageProxyRouteIDCanonicalizesNumericRouteWithoutRegistration(t *testing.T) { + routeID := EncodeNumericID(EncodedIDItem, 12345).String() + proxyRouteID := compatImageProxyRouteID(NewResourceIDCodec(), routeID) + + canonical, ok := canonicalCompatImageRouteID(NewResourceIDCodec(), proxyRouteID) + if !ok || canonical != routeID { + t.Fatalf("canonical proxy route = %q, %v; want %q, true", canonical, ok, routeID) + } +} diff --git a/internal/jellycompat/image_tag_signer.go b/internal/jellycompat/image_tag_signer.go index a2669043..00bc770a 100644 --- a/internal/jellycompat/image_tag_signer.go +++ b/internal/jellycompat/image_tag_signer.go @@ -40,7 +40,7 @@ func (s *imageTagSigner) Equal(seed, fallbackURL, actual string) bool { if s == nil { return false } - actual = strings.TrimSpace(actual) + actual = canonicalCompatImageTag(actual) expected := s.Tag(seed, fallbackURL) if expected == "" || actual == "" || len(expected) != len(actual) { return false diff --git a/internal/jellycompat/images.go b/internal/jellycompat/images.go index 083735d5..61ec5d89 100644 --- a/internal/jellycompat/images.go +++ b/internal/jellycompat/images.go @@ -2,48 +2,226 @@ package jellycompat import ( "bytes" + "context" "crypto/sha1" + "encoding/binary" "image" "image/color" "image/png" "io" "net/http" + "strings" + + "github.com/google/uuid" ) -const defaultImageProxyCacheControl = "public, max-age=300, must-revalidate" +var compatImageProxyUserAgentSubstrings = []string{ + "infuse", +} + +const compatImageRouteCacheControl = "private, no-store, no-cache, max-age=0, s-maxage=0, must-revalidate" +const compatImageProxyTagSuffix = "-p" +const compatImageProxyRoutePrefix = "__jellycompat_image_proxy__:" + +type compatImageProxyRouteContextKey struct{} + +var compatImageProxyHeaders = []string{ + "Accept-Ranges", + "Content-Length", + "Content-Type", + "ETag", + "Last-Modified", +} + +func shouldProxyCompatImageRequest(r *http.Request) bool { + return isCompatImageProxyClientRequest(r) || + isCompatImageProxyRouteRequest(r) || + isCompatImageProxyTag(r.URL.Query().Get("tag")) +} + +func isCompatImageProxyClientRequest(r *http.Request) bool { + if r == nil { + return false + } + clientText := strings.ToLower(strings.Join([]string{ + r.UserAgent(), + r.Header.Get("X-Emby-Authorization"), + }, " ")) + for _, substring := range compatImageProxyUserAgentSubstrings { + if strings.Contains(clientText, substring) { + return true + } + } + return false +} + +func compatImageProxyTag(tag string) string { + tag = strings.TrimSpace(tag) + if tag == "" || isCompatImageProxyTag(tag) { + return tag + } + return tag + compatImageProxyTagSuffix +} + +func isCompatImageProxyTag(tag string) bool { + return strings.HasSuffix(strings.TrimSpace(tag), compatImageProxyTagSuffix) +} + +func canonicalCompatImageTag(tag string) string { + tag = strings.TrimSpace(tag) + return strings.TrimSuffix(tag, compatImageProxyTagSuffix) +} + +func compatImageProxyRouteID(codec *ResourceIDCodec, routeID string) string { + routeID = strings.TrimSpace(routeID) + if codec == nil || routeID == "" { + return routeID + } + if canonical, ok := canonicalCompatImageRouteID(codec, routeID); ok { + return compatImageProxyRouteID(codec, canonical) + } + if decoded, ok := decodePackedCompatRouteID(routeID); ok { + return encodeNumericCompatImageProxyRouteID(decoded) + } + return codec.EncodeStringID(EncodedIDItem, compatImageProxyRoutePrefix+routeID) +} + +func canonicalCompatImageRouteID(codec *ResourceIDCodec, routeID string) (string, bool) { + if canonical, ok := decodeNumericCompatImageProxyRouteID(routeID); ok { + return canonical, true + } + if codec == nil { + return routeID, false + } + value, err := codec.DecodeStringID(EncodedIDItem, strings.TrimSpace(routeID)) + if err != nil || !strings.HasPrefix(value, compatImageProxyRoutePrefix) { + return routeID, false + } + canonical := strings.TrimSpace(strings.TrimPrefix(value, compatImageProxyRoutePrefix)) + if canonical == "" { + return routeID, false + } + return canonical, true +} + +func encodeNumericCompatImageProxyRouteID(decoded DecodedID) string { + var raw [16]byte + raw[0] = byte(EncodedIDImageProxy) + raw[1] = byte(decoded.Type) + binary.BigEndian.PutUint64(raw[8:], decoded.Value) + return uuid.UUID(raw).String() +} + +func decodeNumericCompatImageProxyRouteID(routeID string) (string, bool) { + parsed, err := uuid.Parse(strings.TrimSpace(routeID)) + if err != nil || EncodedIDType(parsed[0]) != EncodedIDImageProxy { + return "", false + } + kind := EncodedIDType(parsed[1]) + if !isPackedCompatRouteIDType(kind) || !zeroUUIDBytes(parsed[2:8]) { + return "", false + } + return EncodeNumericID(kind, binary.BigEndian.Uint64(parsed[8:])).String(), true +} + +func decodePackedCompatRouteID(routeID string) (DecodedID, bool) { + parsed, err := uuid.Parse(strings.TrimSpace(routeID)) + if err != nil { + return DecodedID{}, false + } + kind := EncodedIDType(parsed[0]) + if !isPackedCompatRouteIDType(kind) || !zeroUUIDBytes(parsed[1:8]) { + return DecodedID{}, false + } + return DecodedID{Type: kind, Value: binary.BigEndian.Uint64(parsed[8:])}, true +} + +func isPackedCompatRouteIDType(kind EncodedIDType) bool { + switch kind { + case EncodedIDLibrary, + EncodedIDItem, + EncodedIDMediaSource, + EncodedIDSeason, + EncodedIDPlaySession, + EncodedIDGenre, + EncodedIDStudio, + EncodedIDPerson: + return true + default: + return false + } +} + +func zeroUUIDBytes(values []byte) bool { + for _, value := range values { + if value != 0 { + return false + } + } + return true +} + +func withCompatImageProxyRouteRequest(r *http.Request) *http.Request { + if r == nil { + return r + } + return r.WithContext(context.WithValue(r.Context(), compatImageProxyRouteContextKey{}, true)) +} + +func isCompatImageProxyRouteRequest(r *http.Request) bool { + if r == nil { + return false + } + forceProxy, _ := r.Context().Value(compatImageProxyRouteContextKey{}).(bool) + return forceProxy +} + +func copyConditionalImageRequestHeaders(dst, src http.Header) { + for _, key := range []string{"If-Match", "If-None-Match", "If-Modified-Since", "If-Unmodified-Since"} { + if value := src.Values(key); len(value) > 0 { + dst[key] = append([]string(nil), value...) + } + } +} func proxyImage(w http.ResponseWriter, resp *http.Response) { defer resp.Body.Close() - copyImageProxyHeaders(w.Header(), resp.Header) + if resp.StatusCode == http.StatusNotModified { + copyImageProxyHeaders(w.Header(), resp.Header) + setCompatImageRouteNoStore(w.Header()) w.WriteHeader(http.StatusNotModified) return } - if resp.StatusCode < 200 || resp.StatusCode >= 300 { + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + setCompatImageRouteNoStore(w.Header()) writeError(w, http.StatusBadGateway, "UpstreamError", "Failed to load image") return } - w.WriteHeader(http.StatusOK) + + copyImageProxyHeaders(w.Header(), resp.Header) + setCompatImageRouteNoStore(w.Header()) + w.WriteHeader(resp.StatusCode) _, _ = io.Copy(w, resp.Body) } func copyImageProxyHeaders(dst, src http.Header) { - for _, name := range []string{"Content-Type", "Cache-Control", "ETag", "Last-Modified", "Expires"} { - values := src.Values(name) - if len(values) == 0 { - values = src[name] - } - if len(values) == 0 { - continue - } - dst.Del(name) - for _, value := range values { - dst.Add(name, value) + for _, key := range compatImageProxyHeaders { + if value := src.Values(key); len(value) > 0 { + dst[key] = append([]string(nil), value...) } } - if dst.Get("Cache-Control") == "" { - dst.Set("Cache-Control", defaultImageProxyCacheControl) - } +} + +func setCompatImageRouteNoStore(header http.Header) { + header.Set("Cache-Control", compatImageRouteCacheControl) + header.Set("CDN-Cache-Control", "private, no-store, no-cache, max-age=0") + header.Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") + header.Set("Pragma", "no-cache") + header.Set("Surrogate-Control", "no-store") + header.Set("X-Accel-Expires", "0") + header.Add("Vary", "User-Agent") } func placeholderAvatarPNG(seed string) ([]byte, error) { diff --git a/internal/jellycompat/images_test.go b/internal/jellycompat/images_test.go index 43e38e29..0f0acd90 100644 --- a/internal/jellycompat/images_test.go +++ b/internal/jellycompat/images_test.go @@ -2,10 +2,8 @@ package jellycompat import ( "context" - "io" "net/http" "net/http/httptest" - "strings" "testing" "time" @@ -16,88 +14,66 @@ import ( "github.com/Silo-Server/silo-server/internal/models" ) -func TestProxyImageDefaultsToRevalidatingCachePolicy(t *testing.T) { - rec := httptest.NewRecorder() - resp := &http.Response{ - StatusCode: http.StatusOK, - Header: http.Header{ - "Content-Type": []string{"image/jpeg"}, - "ETag": []string{`"abc"`}, - "Last-Modified": []string{"Tue, 12 May 2026 12:00:00 GMT"}, - "Expires": []string{"Tue, 12 May 2026 12:05:00 GMT"}, - }, - Body: io.NopCloser(strings.NewReader("image-bytes")), - } - - proxyImage(rec, resp) - - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want 200", rec.Code) - } - if got := rec.Header().Get("Cache-Control"); got != defaultImageProxyCacheControl { - t.Fatalf("Cache-Control = %q, want %q", got, defaultImageProxyCacheControl) - } - if got := rec.Header().Get("ETag"); got != `"abc"` { - t.Fatalf("ETag = %q", got) - } - if strings.Contains(rec.Header().Get("Cache-Control"), "immutable") { - t.Fatalf("Cache-Control unexpectedly immutable: %q", rec.Header().Get("Cache-Control")) - } -} - -func TestProxyImageRelaysNotModified(t *testing.T) { - rec := httptest.NewRecorder() - resp := &http.Response{ - StatusCode: http.StatusNotModified, - Header: http.Header{ - "ETag": []string{`"abc"`}, - "Cache-Control": []string{"public, max-age=60"}, - }, - Body: io.NopCloser(strings.NewReader("")), - } - - proxyImage(rec, resp) - - if rec.Code != http.StatusNotModified { - t.Fatalf("status = %d, want 304", rec.Code) - } - if got := rec.Header().Get("ETag"); got != `"abc"` { - t.Fatalf("ETag = %q", got) - } - if rec.Body.Len() != 0 { - t.Fatalf("304 body length = %d, want 0", rec.Body.Len()) - } -} - -func TestProxyImageURLForwardsConditionalHeaders(t *testing.T) { +func TestHandleItemImageAcceptsSignedTagWithoutSessionOrCache(t *testing.T) { + upstreamCalled := false upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("If-None-Match"); got != `"abc"` { - t.Fatalf("If-None-Match = %q", got) - } - if got := r.Header.Get("If-Modified-Since"); got != "Tue, 12 May 2026 12:00:00 GMT" { - t.Fatalf("If-Modified-Since = %q", got) - } - w.Header().Set("ETag", `"abc"`) - w.WriteHeader(http.StatusNotModified) + upstreamCalled = true + w.WriteHeader(http.StatusTeapot) })) defer upstream.Close() - h := &ImagesHandler{httpClient: upstream.Client()} - req := httptest.NewRequest(http.MethodGet, "/Items/1/Images/Primary", nil) - req.Header.Set("If-None-Match", `"abc"`) - req.Header.Set("If-Modified-Since", "Tue, 12 May 2026 12:00:00 GMT") + codec := NewResourceIDCodec() + contentID := "movie-1" + routeID := codec.EncodeStringID(EncodedIDItem, contentID) + updatedAt := time.Date(2026, 5, 26, 12, 0, 0, 0, time.UTC) + item := &models.MediaItem{ + ContentID: contentID, + PosterPath: upstream.URL, + PosterThumbhash: "poster-thumbhash", + UpdatedAt: updatedAt, + } + cfg := &config.Config{Auth: config.AuthConfig{JWTSecret: "image-secret"}} + tag := newMapper(codec, cfg).itemFromList(upstreamListItem{ + ContentID: contentID, + Type: "movie", + Title: "Movie", + PosterURL: item.PosterPath, + PosterPath: item.PosterPath, + PosterThumbhash: item.PosterThumbhash, + UpdatedAt: item.UpdatedAt, + }, false, nil, nil).ImageTags["Primary"] + h := &ImagesHandler{ + codec: codec, + images: NewImageCache(time.Hour, func() time.Time { return updatedAt }), + itemRepo: fakeImageItemRepo{item: item}, + imageTags: newImageTagSigner(cfg.Auth.JWTSecret), + } + + req := httptest.NewRequest(http.MethodGet, "/Items/"+routeID+"/Images/Primary?fillHeight=267&fillWidth=474&quality=96&tag="+tag, nil) + req = withImageRouteParams(req, routeID, "Primary") rec := httptest.NewRecorder() - h.proxyImageURL(rec, req, upstream.URL) + h.HandleItemImage(rec, req) - if rec.Code != http.StatusNotModified { - t.Fatalf("status = %d, want 304", rec.Code) + assertImageRedirect(t, rec, upstream.URL) + if upstreamCalled { + t.Fatal("compat image route proxied the upstream image instead of redirecting") + } + if cached, ok := h.images.LookupSized(routeID, "Primary", "", compatRequestImageSize(req, "Primary")); !ok || cached == "" { + t.Fatal("signed-tag image URL was not cached after resolution") } } -func TestHandleItemImageAcceptsSignedTagWithoutSessionOrCache(t *testing.T) { +func TestHandleItemImageProxiesInfuseSignedTagWithoutSessionOrCache(t *testing.T) { + upstreamCalled := false + var gotIfNoneMatch string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamCalled = true + gotIfNoneMatch = r.Header.Get("If-None-Match") + w.Header().Set("Cache-Control", "public, max-age=14400") w.Header().Set("Content-Type", "image/jpeg") + w.Header().Set("ETag", `"poster-v1"`) + w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("image-bytes")) })) defer upstream.Close() @@ -124,13 +100,15 @@ func TestHandleItemImageAcceptsSignedTagWithoutSessionOrCache(t *testing.T) { }, false, nil, nil).ImageTags["Primary"] h := &ImagesHandler{ codec: codec, - httpClient: upstream.Client(), images: NewImageCache(time.Hour, func() time.Time { return updatedAt }), itemRepo: fakeImageItemRepo{item: item}, imageTags: newImageTagSigner(cfg.Auth.JWTSecret), + httpClient: upstream.Client(), } - req := httptest.NewRequest(http.MethodGet, "/Items/"+routeID+"/Images/Primary?fillHeight=267&fillWidth=474&quality=96&tag="+tag, nil) + req := httptest.NewRequest(http.MethodGet, "/Items/"+routeID+"/Images/Primary?fillHeight=267&fillWidth=474&quality=96&tag="+compatImageProxyTag(tag), nil) + req.Header.Set("If-None-Match", `"poster-v1"`) + req.Header.Set("User-Agent", "Infuse-Direct/8.4.6") req = withImageRouteParams(req, routeID, "Primary") rec := httptest.NewRecorder() @@ -140,10 +118,90 @@ func TestHandleItemImageAcceptsSignedTagWithoutSessionOrCache(t *testing.T) { t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) } if got := rec.Body.String(); got != "image-bytes" { - t.Fatalf("body = %q, want image bytes", got) + t.Fatalf("body = %q, want image-bytes", got) + } + if got := rec.Header().Get("Location"); got != "" { + t.Fatalf("Location = %q, want empty", got) + } + if got := rec.Header().Get("Content-Type"); got != "image/jpeg" { + t.Fatalf("Content-Type = %q, want image/jpeg", got) + } + if got := rec.Header().Get("Cache-Control"); got != compatImageRouteCacheControl { + t.Fatalf("Cache-Control = %q, want %q", got, compatImageRouteCacheControl) + } + if got := rec.Header().Get("CDN-Cache-Control"); got != "private, no-store, no-cache, max-age=0" { + t.Fatalf("CDN-Cache-Control = %q, want private, no-store, no-cache, max-age=0", got) + } + if got := rec.Header().Get("X-Accel-Expires"); got != "0" { + t.Fatalf("X-Accel-Expires = %q, want 0", got) + } + if got := gotIfNoneMatch; got != `"poster-v1"` { + t.Fatalf("forwarded If-None-Match = %q, want poster-v1", got) + } + if !upstreamCalled { + t.Fatal("Infuse compat image route did not proxy the upstream image") + } +} + +func TestHandleItemImageProxyRouteIDUsesCanonicalItemAndProxy(t *testing.T) { + upstreamCalled := false + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamCalled = true + w.Header().Set("Content-Type", "image/webp") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("proxy-route-image")) + })) + defer upstream.Close() + + codec := NewResourceIDCodec() + contentID := "movie-1" + routeID := codec.EncodeStringID(EncodedIDItem, contentID) + proxyRouteID := compatImageProxyRouteID(codec, routeID) + updatedAt := time.Date(2026, 5, 26, 12, 0, 0, 0, time.UTC) + item := &models.MediaItem{ + ContentID: contentID, + PosterPath: upstream.URL, + PosterThumbhash: "poster-thumbhash", + UpdatedAt: updatedAt, + } + cfg := &config.Config{Auth: config.AuthConfig{JWTSecret: "image-secret"}} + tag := newMapper(codec, cfg).itemFromList(upstreamListItem{ + ContentID: contentID, + Type: "movie", + Title: "Movie", + PosterURL: item.PosterPath, + PosterPath: item.PosterPath, + PosterThumbhash: item.PosterThumbhash, + UpdatedAt: item.UpdatedAt, + }, false, nil, nil).ImageTags["Primary"] + h := &ImagesHandler{ + codec: codec, + images: NewImageCache(time.Hour, func() time.Time { return updatedAt }), + itemRepo: fakeImageItemRepo{item: item}, + imageTags: newImageTagSigner(cfg.Auth.JWTSecret), + httpClient: upstream.Client(), + } + + req := httptest.NewRequest(http.MethodGet, "/Items/"+proxyRouteID+"/Images/Primary?fillHeight=267&fillWidth=474&quality=96&tag="+compatImageProxyTag(tag), nil) + req = withImageRouteParams(req, proxyRouteID, "Primary") + rec := httptest.NewRecorder() + + h.HandleItemImage(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) + } + if got := rec.Body.String(); got != "proxy-route-image" { + t.Fatalf("body = %q, want proxy-route-image", got) + } + if got := rec.Header().Get("Location"); got != "" { + t.Fatalf("Location = %q, want empty", got) } if cached, ok := h.images.LookupSized(routeID, "Primary", "", compatRequestImageSize(req, "Primary")); !ok || cached == "" { - t.Fatal("signed-tag image URL was not cached after resolution") + t.Fatal("proxy route image URL was not cached under the canonical route ID") + } + if !upstreamCalled { + t.Fatal("proxy route did not fetch the upstream image") } } @@ -168,10 +226,9 @@ func TestHandleItemImageRejectsUnsignedTagWhenSecretBlank(t *testing.T) { UpdatedAt: item.UpdatedAt, }, false, nil, nil).ImageTags["Primary"] h := &ImagesHandler{ - codec: codec, - httpClient: http.DefaultClient, - itemRepo: fakeImageItemRepo{item: item}, - imageTags: newImageTagSigner(""), + codec: codec, + itemRepo: fakeImageItemRepo{item: item}, + imageTags: newImageTagSigner(""), } req := httptest.NewRequest(http.MethodGet, "/Items/"+routeID+"/Images/Primary?tag="+tag, nil) @@ -186,9 +243,10 @@ func TestHandleItemImageRejectsUnsignedTagWhenSecretBlank(t *testing.T) { } func TestHandleItemImageAcceptsSignedCanonicalBackdropTagWithoutSessionOrCache(t *testing.T) { + upstreamCalled := false upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "image/jpeg") - _, _ = w.Write([]byte("backdrop-bytes")) + upstreamCalled = true + w.WriteHeader(http.StatusTeapot) })) defer upstream.Close() @@ -201,9 +259,8 @@ func TestHandleItemImageAcceptsSignedCanonicalBackdropTagWithoutSessionOrCache(t upstream.URL, ) h := &ImagesHandler{ - codec: codec, - httpClient: upstream.Client(), - images: NewImageCache(time.Hour, time.Now), + codec: codec, + images: NewImageCache(time.Hour, time.Now), itemRepo: fakeImageItemRepo{item: &models.MediaItem{ ContentID: contentID, BackdropPath: upstream.URL, @@ -217,18 +274,17 @@ func TestHandleItemImageAcceptsSignedCanonicalBackdropTagWithoutSessionOrCache(t h.HandleItemImage(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) - } - if got := rec.Body.String(); got != "backdrop-bytes" { - t.Fatalf("body = %q, want backdrop bytes", got) + assertImageRedirect(t, rec, upstream.URL) + if upstreamCalled { + t.Fatal("compat image route proxied the upstream image instead of redirecting") } } func TestHandleItemImageAcceptsLibraryPosterTagWithoutSessionOrCache(t *testing.T) { + upstreamCalled := false upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "image/jpeg") - _, _ = w.Write([]byte("library-poster")) + upstreamCalled = true + w.WriteHeader(http.StatusTeapot) })) defer upstream.Close() @@ -243,7 +299,6 @@ func TestHandleItemImageAcceptsLibraryPosterTagWithoutSessionOrCache(t *testing. ) h := &ImagesHandler{ codec: codec, - httpClient: upstream.Client(), images: NewImageCache(time.Hour, time.Now), folderRepo: fakeImageFolderRepo{folder: &models.MediaFolder{ID: libraryID, PosterPath: posterPath}}, posterSigner: fakeLibraryPosterPresigner{url: upstream.URL}, @@ -256,18 +311,17 @@ func TestHandleItemImageAcceptsLibraryPosterTagWithoutSessionOrCache(t *testing. h.HandleItemImage(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) - } - if got := rec.Body.String(); got != "library-poster" { - t.Fatalf("body = %q, want library poster", got) + assertImageRedirect(t, rec, upstream.URL) + if upstreamCalled { + t.Fatal("compat image route proxied the upstream image instead of redirecting") } } func TestHandleItemImageAcceptsLegacyCachedURLTagWithoutRouteFallback(t *testing.T) { + upstreamCalled := false upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "image/jpeg") - _, _ = w.Write([]byte("cached-image")) + upstreamCalled = true + w.WriteHeader(http.StatusTeapot) })) defer upstream.Close() @@ -276,10 +330,9 @@ func TestHandleItemImageAcceptsLegacyCachedURLTagWithoutRouteFallback(t *testing cache := NewImageCache(time.Hour, time.Now) cache.RememberSized(routeID, "Primary", upstream.URL, compatCardImageSize) h := &ImagesHandler{ - codec: codec, - httpClient: upstream.Client(), - images: cache, - imageTags: newImageTagSigner("image-secret"), + codec: codec, + images: cache, + imageTags: newImageTagSigner("image-secret"), } req := httptest.NewRequest(http.MethodGet, "/Items/"+routeID+"/Images/Primary?tag="+tagValue(upstream.URL), nil) @@ -288,11 +341,9 @@ func TestHandleItemImageAcceptsLegacyCachedURLTagWithoutRouteFallback(t *testing h.HandleItemImage(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, body = %s; want 200", rec.Code, rec.Body.String()) - } - if got := rec.Body.String(); got != "cached-image" { - t.Fatalf("body = %q, want cached image", got) + assertImageRedirect(t, rec, upstream.URL) + if upstreamCalled { + t.Fatal("compat image route proxied the upstream image instead of redirecting") } } @@ -328,11 +379,10 @@ func TestHandleItemImageRevalidatesTagBeforeRouteCacheHit(t *testing.T) { UpdatedAt: item.UpdatedAt, }, false, nil, nil).ImageTags["Primary"] h := &ImagesHandler{ - codec: codec, - httpClient: upstream.Client(), - images: cache, - itemRepo: fakeImageItemRepo{item: item}, - imageTags: newImageTagSigner("new-secret"), + codec: codec, + images: cache, + itemRepo: fakeImageItemRepo{item: item}, + imageTags: newImageTagSigner("new-secret"), } req := httptest.NewRequest(http.MethodGet, "/Items/"+routeID+"/Images/Primary?tag="+tag, nil) @@ -349,6 +399,34 @@ func TestHandleItemImageRevalidatesTagBeforeRouteCacheHit(t *testing.T) { } } +func TestRedirectImageURLRejectsNonHTTPURL(t *testing.T) { + h := &ImagesHandler{} + req := httptest.NewRequest(http.MethodGet, "/Items/1/Images/Primary", nil) + rec := httptest.NewRecorder() + + h.redirectImageURL(rec, req, "catalog/poster.jpg") + + if rec.Code != http.StatusBadGateway { + t.Fatalf("status = %d, body = %s; want 502", rec.Code, rec.Body.String()) + } + if got := rec.Header().Get("Location"); got != "" { + t.Fatalf("Location = %q, want empty", got) + } +} + +func assertImageRedirect(t *testing.T, rec *httptest.ResponseRecorder, wantLocation string) { + t.Helper() + if rec.Code != http.StatusFound { + t.Fatalf("status = %d, body = %s; want 302", rec.Code, rec.Body.String()) + } + if got := rec.Header().Get("Location"); got != wantLocation { + t.Fatalf("Location = %q, want %q", got, wantLocation) + } + if got := rec.Header().Get("Cache-Control"); got != compatImageRouteCacheControl { + t.Fatalf("Cache-Control = %q, want %q", got, compatImageRouteCacheControl) + } +} + type fakeImageItemRepo struct { item *models.MediaItem } diff --git a/internal/jellycompat/mapping.go b/internal/jellycompat/mapping.go index cc2591d9..b3ab98c3 100644 --- a/internal/jellycompat/mapping.go +++ b/internal/jellycompat/mapping.go @@ -65,6 +65,21 @@ func (m *mapper) viewFromLibrary(library upstreamUserLibrary) baseItemDTO { } } +// applyPlayableLocation stamps location fields for a movie/episode based on +// whether any media file backs it. Fileless (provider-metadata-only) items are +// Jellyfin "Virtual" and carry no VideoType, so clients exclude them from +// playback queues. Items whose file went missing are currently also reported +// as Virtual rather than Jellyfin's "Offline". +func applyPlayableLocation(dto *baseItemDTO, hasFile bool) { + if hasFile { + dto.LocationType = "FileSystem" + dto.VideoType = "VideoFile" + } else { + dto.LocationType = "Virtual" + dto.VideoType = "" + } +} + func (m *mapper) itemFromList(item upstreamListItem, isFavorite bool, progress *upstreamProgress, fields map[string]bool) baseItemDTO { _, allFields := fields["*"] // detail views pass allDetailFields sentinel @@ -88,8 +103,7 @@ func (m *mapper) itemFromList(item upstreamListItem, isFavorite bool, progress * dto.RunTimeTicks = minutesToTicks(item.Runtime) } if item.Type == "movie" || item.Type == "episode" { - dto.LocationType = "FileSystem" - dto.VideoType = "VideoFile" + applyPlayableLocation(&dto, item.HasMediaFiles == nil || *item.HasMediaFiles) } if item.SeriesID != "" { dto.SeriesID = m.codec.EncodeStringID(EncodedIDItem, item.SeriesID) @@ -106,8 +120,10 @@ func (m *mapper) itemFromList(item upstreamListItem, isFavorite bool, progress * dto.RecursiveItemCount = *item.EpisodeCount } if item.SeasonCount != nil { - dto.ChildCount = *item.SeasonCount - dto.RecursiveItemCount = *item.SeasonCount + seasonCount := *item.SeasonCount + dto.ChildCount = seasonCount + dto.RecursiveItemCount = seasonCount + dto.SeasonCount = seasonCount } primaryPath, primaryThumbhash := listItemPrimaryImageSeedParts(item) if tags := imageTagsWithSeed(m.imageTagSigner, @@ -366,12 +382,14 @@ func (m *mapper) itemFromDetailWithFields(item upstreamItemDetail, isFavorite bo dto.RunTimeTicks = secondsToTicks(float64(firstVersion.Duration)) } dto.DateCreated = formatCompatTime(firstVersion.AddedAt) + // CanDownload is load-bearing for Infuse: it refuses Direct Play + // (Static=true streaming) of items it believes it cannot download. + // The flag is backed by the /Items/{id}/Download route (streams.go). dto.CanDownload = true dto.HasSubtitles = versionsHaveSubtitles(item.Versions) dto.SupportsSync = false dto.Container = strings.ToLower(firstVersion.Container) - dto.LocationType = "FileSystem" - dto.VideoType = "VideoFile" + applyPlayableLocation(&dto, true) dto.Path = compatMediaPath(firstVersion) if len(firstVersion.VideoTracks) > 0 { dto.Width = firstVersion.VideoTracks[0].Width @@ -406,6 +424,11 @@ func (m *mapper) itemFromDetailWithFields(item upstreamItemDetail, isFavorite bo if wantField("chapters") { dto.Chapters = compatChapters(firstVersion.Chapters, firstVersion.AddedAt) } + } else if isPlayableItemType(item.Type) { + // Provider-metadata-only (unaired/missing) item: version data is + // authoritative here, so override the FileSystem default set by + // itemFromList with Jellyfin's Virtual contract. + applyPlayableLocation(&dto, false) } slog.Info("jellycompat item detail mapped", "content_id", item.ContentID, @@ -450,13 +473,12 @@ func (m *mapper) episodeFromUpstream(ep upstreamEpisode, isFavorite bool, progre Name: ep.Title, ServerID: m.serverID, Overview: ep.Overview, - LocationType: "FileSystem", - VideoType: "VideoFile", RunTimeTicks: minutesToTicks(ep.Runtime), ImageTags: map[string]string{}, SeriesName: ep.SeriesTitle, UserData: userDataDTO(m.codec.EncodeStringID(EncodedIDItem, ep.ContentID), ep.UserData, isFavorite, progress), } + applyPlayableLocation(&dto, ep.HasMediaFiles == nil || *ep.HasMediaFiles) dto.IndexNumber = &ep.EpisodeNumber dto.ParentIndexNumber = &ep.SeasonNumber if ep.SeriesID != "" { @@ -514,10 +536,9 @@ func userDataDTO(itemID string, data *catalog.SeasonUserData, isFavorite bool, p dto := &itemUserDataDTO{IsFavorite: isFavorite, ItemID: itemID, Key: itemID} if data != nil { - dto.PlaybackPositionTicks = resumePositionTicks(data.PositionSeconds, data.DurationSeconds, data.Played) - if data.DurationSeconds > 0 { - dto.PlayedPercentage = (data.PositionSeconds / data.DurationSeconds) * 100 - } + pos := clampResumeSeconds(data.PositionSeconds, data.DurationSeconds) + dto.PlaybackPositionTicks = secondsToTicks(pos) + dto.PlayedPercentage = playedPercentage(pos, data.DurationSeconds, data.Played) dto.Played = data.Played dto.UnplayedItemCount = data.UnplayedCount if data.Played { @@ -526,10 +547,9 @@ func userDataDTO(itemID string, data *catalog.SeasonUserData, isFavorite bool, p } if progress != nil { - dto.PlaybackPositionTicks = resumePositionTicks(progress.PositionSeconds, progress.DurationSeconds, progress.Completed) - if progress.DurationSeconds > 0 { - dto.PlayedPercentage = (progress.PositionSeconds / progress.DurationSeconds) * 100 - } + pos := clampResumeSeconds(progress.PositionSeconds, progress.DurationSeconds) + dto.PlaybackPositionTicks = secondsToTicks(pos) + dto.PlayedPercentage = playedPercentage(pos, progress.DurationSeconds, progress.Completed) dto.Played = progress.Completed if progress.Completed { dto.PlayCount = 1 @@ -540,6 +560,21 @@ func userDataDTO(itemID string, data *catalog.SeasonUserData, isFavorite bool, p return dto } +// playedPercentage derives PlayedPercentage from the same clamped position +// used for PlaybackPositionTicks so the two fields can never disagree. A +// played item at rest (position 0, no resume point) reports 100 so clients +// rendering progress from the DTO show it fully watched; a rewatch in flight +// reports its live fraction. +func playedPercentage(clampedPos, duration float64, played bool) float64 { + if played && clampedPos == 0 { + return 100 + } + if duration <= 0 { + return 0 + } + return (clampedPos / duration) * 100 +} + func jellyfinItemType(native string) string { switch strings.ToLower(native) { case "movie": @@ -674,22 +709,21 @@ func secondsToTicks(seconds float64) int64 { return int64(seconds * 10_000_000) } -// resumePositionTicks returns the PlaybackPositionTicks value for a UserData -// DTO. When the item is fully watched the position is reported as 0 so clients -// start fresh on the next play; otherwise the position is clamped to the item -// duration so a stale/overflowed stored position cannot drive the player past -// end-of-file (which stalls the HLS transcoder on resume). -func resumePositionTicks(position, duration float64, played bool) int64 { - if played { - return 0 - } +// clampResumeSeconds normalizes a stored resume position for client-facing +// user data. Completed rows store position 0, so fully watched items report 0 +// (clients start fresh) while a rewatch in flight reports its live resume +// point alongside Played=true — matching real Jellyfin. The position is +// clamped to the item duration so a stale/overflowed stored position cannot +// drive the player past end-of-file (which stalls the HLS transcoder on +// resume). +func clampResumeSeconds(position, duration float64) float64 { if duration > 0 && position > duration { position = duration } if position < 0 { position = 0 } - return secondsToTicks(position) + return position } func imageTagsWithSeed(signer *imageTagSigner, seed, imageURL string) map[string]string { diff --git a/internal/jellycompat/mapping_sort_test.go b/internal/jellycompat/mapping_sort_test.go index f04c7551..08c73758 100644 --- a/internal/jellycompat/mapping_sort_test.go +++ b/internal/jellycompat/mapping_sort_test.go @@ -35,3 +35,39 @@ func TestItemListSortNamePrefersSortTitle(t *testing.T) { t.Fatalf("SortName = %q, want %q", dto.SortName, "Matrix, The") } } + +func TestSeriesListIncludesSeasonCount(t *testing.T) { + m := newMapper(NewResourceIDCodec(), &config.Config{}) + seasonCount := 4 + dto := m.itemFromList(upstreamListItem{ + ContentID: "series-1", + Type: "series", + Title: "Snowpiercer", + SeasonCount: &seasonCount, + }, false, nil, nil) + + if dto.SeasonCount != 4 { + t.Fatalf("SeasonCount = %d, want 4", dto.SeasonCount) + } + if dto.ChildCount != 4 || dto.RecursiveItemCount != 4 { + t.Fatalf("ChildCount/RecursiveItemCount = %d/%d, want 4/4", dto.ChildCount, dto.RecursiveItemCount) + } +} + +func TestSeriesDetailIncludesSeasonCount(t *testing.T) { + m := newMapper(NewResourceIDCodec(), &config.Config{}) + seasonCount := 4 + dto := m.itemFromDetail(upstreamItemDetail{ + ContentID: "series-1", + Type: "series", + Title: "Snowpiercer", + SeasonCount: &seasonCount, + }, false, nil) + + if dto.SeasonCount != 4 { + t.Fatalf("SeasonCount = %d, want 4", dto.SeasonCount) + } + if dto.ChildCount != 4 || dto.RecursiveItemCount != 4 { + t.Fatalf("ChildCount/RecursiveItemCount = %d/%d, want 4/4", dto.ChildCount, dto.RecursiveItemCount) + } +} diff --git a/internal/jellycompat/mapping_userdata_test.go b/internal/jellycompat/mapping_userdata_test.go index fc60887f..944192f7 100644 --- a/internal/jellycompat/mapping_userdata_test.go +++ b/internal/jellycompat/mapping_userdata_test.go @@ -6,9 +6,10 @@ import ( "github.com/Silo-Server/silo-server/internal/catalog" ) -func TestUserDataDTOPlayedZerosResumePosition(t *testing.T) { +func TestUserDataDTOPlayedReportsZeroPosition(t *testing.T) { + // Watched rows store position 0, so played items naturally report 0 ticks. data := &catalog.SeasonUserData{ - PositionSeconds: 1290.33, + PositionSeconds: 0, DurationSeconds: 1290.0, Played: true, } @@ -19,6 +20,27 @@ func TestUserDataDTOPlayedZerosResumePosition(t *testing.T) { if !dto.Played { t.Fatalf("Played = false, want true") } + if dto.PlayedPercentage != 100 { + t.Fatalf("PlayedPercentage = %v, want 100 for played item at rest", dto.PlayedPercentage) + } +} + +func TestUserDataDTOPlayedRewatchReportsResumePosition(t *testing.T) { + // A rewatch in flight keeps Played=true with a live resume point; clients + // must see both the checkmark and the position (matches Jellyfin). + data := &catalog.SeasonUserData{ + PositionSeconds: 600.0, + DurationSeconds: 1290.0, + Played: true, + } + dto := userDataDTO("item-1", data, false, nil) + want := secondsToTicks(600.0) + if dto.PlaybackPositionTicks != want { + t.Fatalf("PlaybackPositionTicks = %d, want %d for rewatch in flight", dto.PlaybackPositionTicks, want) + } + if !dto.Played { + t.Fatalf("Played = false, want true") + } } func TestUserDataDTOClampsPositionPastDuration(t *testing.T) { @@ -32,6 +54,9 @@ func TestUserDataDTOClampsPositionPastDuration(t *testing.T) { if dto.PlaybackPositionTicks != want { t.Fatalf("PlaybackPositionTicks = %d, want %d (clamped to duration)", dto.PlaybackPositionTicks, want) } + if dto.PlayedPercentage > 100 { + t.Fatalf("PlayedPercentage = %v, want <= 100 (derived from the clamped position)", dto.PlayedPercentage) + } } func TestUserDataDTOPreservesValidPosition(t *testing.T) { @@ -48,9 +73,10 @@ func TestUserDataDTOPreservesValidPosition(t *testing.T) { } func TestUserDataDTOProgressCompletedZeros(t *testing.T) { + // Completed rows store position 0, so watched items report 0 ticks. progress := &upstreamProgress{ MediaItemID: "x", - PositionSeconds: 1290.33, + PositionSeconds: 0, DurationSeconds: 1290.0, Completed: true, } @@ -61,6 +87,33 @@ func TestUserDataDTOProgressCompletedZeros(t *testing.T) { if !dto.Played { t.Fatalf("Played = false, want true") } + if dto.PlayedPercentage != 100 { + t.Fatalf("PlayedPercentage = %v, want 100 for completed item at rest", dto.PlayedPercentage) + } +} + +func TestUserDataDTOProgressRewatchKeepsPlayedAndPosition(t *testing.T) { + progress := &upstreamProgress{ + MediaItemID: "x", + PositionSeconds: 600.0, + DurationSeconds: 1290.0, + Completed: true, + } + dto := userDataDTO("item-4", nil, false, progress) + want := secondsToTicks(600.0) + if dto.PlaybackPositionTicks != want { + t.Fatalf("PlaybackPositionTicks = %d, want %d for rewatch in flight", dto.PlaybackPositionTicks, want) + } + if !dto.Played { + t.Fatalf("Played = false, want true") + } + if dto.PlayCount != 1 { + t.Fatalf("PlayCount = %d, want 1 (watched state survives rewatch)", dto.PlayCount) + } + wantPct := (600.0 / 1290.0) * 100 + if dto.PlayedPercentage != wantPct { + t.Fatalf("PlayedPercentage = %v, want %v (live rewatch fraction)", dto.PlayedPercentage, wantPct) + } } func TestUserDataDTOProgressClampsPosition(t *testing.T) { diff --git a/internal/jellycompat/path_normalization.go b/internal/jellycompat/path_normalization.go index 4de6d7a5..2c620d7b 100644 --- a/internal/jellycompat/path_normalization.go +++ b/internal/jellycompat/path_normalization.go @@ -27,8 +27,10 @@ var compatPathSegments = map[string]string{ "suggestions": "Suggestions", "similar": "Similar", "thememedia": "ThemeMedia", + "themesongs": "ThemeSongs", "specialfeatures": "SpecialFeatures", "intros": "Intros", + "download": "Download", "images": "Images", "primary": "Primary", "backdrop": "Backdrop", diff --git a/internal/jellycompat/playback_sessions.go b/internal/jellycompat/playback_sessions.go index 4b075a9c..2c8c156e 100644 --- a/internal/jellycompat/playback_sessions.go +++ b/internal/jellycompat/playback_sessions.go @@ -140,7 +140,7 @@ func (s *PlaybackSessionStore) FindByRoute(compatToken, routeID string) (*Playba return &cp, nil, true } for _, source := range session.MediaSources { - if source.ID == routeID { + if mediaSourceIDsEqual(source.ID, routeID) { cp := session sourceCopy := source return &cp, &sourceCopy, true diff --git a/internal/jellycompat/query.go b/internal/jellycompat/query.go index 0e89c8be..a81b138b 100644 --- a/internal/jellycompat/query.go +++ b/internal/jellycompat/query.go @@ -19,12 +19,18 @@ type itemsQuery struct { namePrefix string maxOfficialRating string parentLibraryID int + parentItemID string + parentCollectionID string specificIDs []string + specificCollectionIDs []string itemTypes []string genreName string isFavorite bool isResumable bool hasItemTypeFilter bool // true when IncludeItemTypes was present in the request + wantsBoxSets bool // true when IncludeItemTypes contains BoxSet + wantsViews bool // true when IncludeItemTypes contains CollectionFolder + sortExplicit bool // true when SortBy was present in the request needsDetailFields bool // true when requested Fields include detail-level data (e.g. MediaSources) itemType string sort string @@ -32,6 +38,7 @@ type itemsQuery struct { personID int64 isPlayed *bool // nil = not specified imageTypeLimit *int // nil = not specified + requireBackdrop bool // true when ImageTypes includes Backdrop (filter, not just a hint) mediaTypes []string mediaTypesSet map[string]bool mediaTypesExplicit bool @@ -55,14 +62,21 @@ func parseItemsQuery(r *http.Request, codec *ResourceIDCodec) itemsQuery { if parentID := strings.TrimSpace(q.Get("ParentId")); parentID != "" { if libraryID, err := codec.DecodeIntID(EncodedIDLibrary, parentID); err == nil { result.parentLibraryID = int(libraryID) + } else if collectionID, collErr := codec.DecodeStringID(EncodedIDCollection, parentID); collErr == nil && collectionID != "" { + result.parentCollectionID = collectionID + } else if contentID, itemErr := decodeItemID(codec, parentID); itemErr == nil && contentID != "" { + result.parentItemID = contentID } } if ids := strings.TrimSpace(q.Get("Ids")); ids != "" { parts := strings.SplitSeq(ids, ",") for part := range parts { - if decoded, err := decodeItemID(codec, strings.TrimSpace(part)); err == nil && decoded != "" { + raw := strings.TrimSpace(part) + if decoded, err := decodeItemID(codec, raw); err == nil && decoded != "" { result.specificIDs = append(result.specificIDs, decoded) + } else if collectionID, collErr := codec.DecodeStringID(EncodedIDCollection, raw); collErr == nil && collectionID != "" { + result.specificCollectionIDs = append(result.specificCollectionIDs, collectionID) } } } @@ -92,6 +106,9 @@ func parseItemsQuery(r *http.Request, codec *ResourceIDCodec) itemsQuery { rawItemTypes := q.Values("IncludeItemTypes") result.hasItemTypeFilter = len(rawItemTypes) > 0 && strings.TrimSpace(strings.Join(rawItemTypes, "")) != "" result.itemTypes = mapIncludeItemTypes(rawItemTypes) + result.wantsBoxSets = includeItemTypesContain(rawItemTypes, "boxset") + result.wantsViews = includeItemTypesContain(rawItemTypes, "collectionfolder") + result.sortExplicit = strings.TrimSpace(q.Get("SortBy")) != "" if len(result.itemTypes) > 0 { result.itemType = result.itemTypes[0] } @@ -111,6 +128,17 @@ func parseItemsQuery(r *http.Request, codec *ResourceIDCodec) itemsQuery { } } + // ImageTypes acts as a filter: clients (e.g. Wholphin genre cards) request + // ImageTypes=Backdrop and assume every returned item has a backdrop. Only + // Backdrop is enforced — the catalog browse path can filter on backdrop_path. + for _, raw := range q.Values("ImageTypes") { + for part := range strings.SplitSeq(raw, ",") { + if strings.EqualFold(strings.TrimSpace(part), "Backdrop") { + result.requireBackdrop = true + } + } + } + mediaTypesRaw := q.Values("MediaTypes") result.mediaTypes = parseMediaTypes(mediaTypesRaw) result.mediaTypesExplicit = len(mediaTypesRaw) > 0 && strings.TrimSpace(strings.Join(mediaTypesRaw, "")) != "" @@ -122,9 +150,16 @@ func parseItemsQuery(r *http.Request, codec *ResourceIDCodec) itemsQuery { } // Fields — parse requested fields and track if explicitly present. - fieldsRaw := q.Get("Fields") + // Clients send Fields two ways: comma-separated in a single param (VidHub: + // Fields=A,B,C) OR as repeated params (Wholphin / jellyfin-sdk-kotlin: + // Fields=A&Fields=B&Fields=C). q.Get returns only the FIRST repeated value, + // which silently dropped every field after the first — so a Wholphin + // playlist request (Fields=PrimaryImageAspectRatio&...&Fields=MediaSources) + // never triggered the detail path and came back without MediaSources, + // breaking next-episode playback. Join all values, then split on commas. + fieldsRaw := strings.Join(q.Values("Fields"), ",") result.requestedFields = parseRequestedFields(fieldsRaw) - result.fieldsExplicit = fieldsRaw != "" + result.fieldsExplicit = strings.TrimSpace(fieldsRaw) != "" result.needsDetailFields = requestedFieldsNeedDetail(result.requestedFields) // Diagnostic: when the request stays on the list path, emit a Debug log @@ -211,6 +246,9 @@ func buildBrowseParams(query itemsQuery) url.Values { params.Set("is_played", "false") } } + if query.requireBackdrop { + params.Set("require_backdrop", "true") + } return params } @@ -298,6 +336,20 @@ func mapIncludeItemTypes(rawValues []string) []string { return result } +// includeItemTypesContain reports whether a raw IncludeItemTypes value list +// contains the given (lowercase) type, before mapIncludeItemTypes drops +// entries it cannot map to catalog types (e.g. BoxSet). +func includeItemTypesContain(rawValues []string, target string) bool { + for _, raw := range rawValues { + for part := range strings.SplitSeq(raw, ",") { + if strings.ToLower(strings.TrimSpace(part)) == target { + return true + } + } + } + return false +} + func parseMediaTypes(rawValues []string) []string { if len(rawValues) == 0 { return nil diff --git a/internal/jellycompat/query_imagetypes_test.go b/internal/jellycompat/query_imagetypes_test.go new file mode 100644 index 00000000..18255227 --- /dev/null +++ b/internal/jellycompat/query_imagetypes_test.go @@ -0,0 +1,37 @@ +package jellycompat + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestParseItemsQuery_ImageTypesBackdropFilter(t *testing.T) { + codec := NewResourceIDCodec() + cases := []struct { + name string + rawQuery string + wantRequire bool + }{ + {"backdrop requested", "ImageTypes=Backdrop", true}, + {"lowercase param and value", "imagetypes=backdrop", true}, + {"backdrop among several", "ImageTypes=Primary,Backdrop,Logo", true}, + {"bracket array variant", "ImageTypes[]=Backdrop", true}, + {"primary only does not filter", "ImageTypes=Primary", false}, + {"absent does not filter", "Limit=1", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/Items?"+tc.rawQuery, nil) + query := parseItemsQuery(req, codec) + if query.requireBackdrop != tc.wantRequire { + t.Fatalf("requireBackdrop = %v, want %v", query.requireBackdrop, tc.wantRequire) + } + params := buildBrowseParams(query) + gotParam := params.Get("require_backdrop") == "true" + if gotParam != tc.wantRequire { + t.Fatalf("require_backdrop param = %q, want emitted=%v", params.Get("require_backdrop"), tc.wantRequire) + } + }) + } +} diff --git a/internal/jellycompat/query_repeated_fields_test.go b/internal/jellycompat/query_repeated_fields_test.go new file mode 100644 index 00000000..ce807aff --- /dev/null +++ b/internal/jellycompat/query_repeated_fields_test.go @@ -0,0 +1,52 @@ +package jellycompat + +import ( + "net/http/httptest" + "testing" +) + +// TestRepeatedFieldsParamTriggersDetail covers clients (e.g. the +// jellyfin-sdk-kotlin / Wholphin) that send Fields as repeated query params +// (Fields=A&Fields=B&...) rather than comma-separated in a single param. +// q.Get("Fields") returns only the first repeated value, which silently +// dropped every field after the first — so a request whose first Fields entry +// is a list-servable field but which also asks for a detail-only field (e.g. +// MediaSources) later never triggered the detail path and came back without +// MediaSources. parseItemsQuery must join all repeated values before splitting. +func TestRepeatedFieldsParamTriggersDetail(t *testing.T) { + url := "/Shows/abc/Episodes?Fields=PrimaryImageAspectRatio&Fields=SeasonUserData" + + "&Fields=ChildCount&Fields=Overview&Fields=Trickplay&Fields=SortName" + + "&Fields=Chapters&Fields=MediaSources&Fields=MediaSourceCount" + + "&Fields=ParentId&Fields=CanDelete&startItemId=x&limit=100" + req := httptest.NewRequest("GET", url, nil) + query := parseItemsQuery(req, NewResourceIDCodec()) + + if !query.needsDetailFields { + t.Fatal("repeated Fields params with a detail-only field not-first must set needsDetailFields=true") + } + if !query.requestedFields["mediasources"] { + t.Error("mediasources must be parsed from repeated Fields params") + } + if !query.requestedFields["chapters"] { + t.Error("chapters must be parsed from repeated Fields params") + } + if !query.fieldsExplicit { + t.Error("fieldsExplicit must be true when repeated Fields params are present") + } +} + +// TestCommaSeparatedFieldsStillParsed guards the original single-param, +// comma-separated form (e.g. VidHub: Fields=A,B,C) which must keep working. +func TestCommaSeparatedFieldsStillParsed(t *testing.T) { + req := httptest.NewRequest("GET", "/Items?Fields=Overview,MediaSources,People", nil) + query := parseItemsQuery(req, NewResourceIDCodec()) + + if !query.needsDetailFields { + t.Fatal("comma-separated Fields with MediaSources must set needsDetailFields=true") + } + for _, f := range []string{"overview", "mediasources", "people"} { + if !query.requestedFields[f] { + t.Errorf("field %q must be parsed from comma-separated Fields", f) + } + } +} diff --git a/internal/jellycompat/resume_filter_test.go b/internal/jellycompat/resume_filter_test.go new file mode 100644 index 00000000..c022bf3b --- /dev/null +++ b/internal/jellycompat/resume_filter_test.go @@ -0,0 +1,222 @@ +package jellycompat + +import ( + "context" + "testing" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/config" + "github.com/Silo-Server/silo-server/internal/models" + "github.com/Silo-Server/silo-server/internal/userstore" +) + +// resumeFilteringUserData serves a fixed in-progress list and hides a chosen +// set of media item IDs from FilterResumeProgress, recording which statuses +// were listed and how often the filter ran. +type resumeFilteringUserData struct { + mockUserDataService + entries []upstreamProgress + hidden map[string]bool + listedStatuses []string + filterCalls int + filteredBatchSize []int +} + +func (s *resumeFilteringUserData) ListProgress(_ context.Context, _ *Session, status string, limit, offset int) ([]upstreamProgress, error) { + s.listedStatuses = append(s.listedStatuses, status) + if offset >= len(s.entries) { + return nil, nil + } + end := min(offset+limit, len(s.entries)) + return s.entries[offset:end], nil +} + +func (s *resumeFilteringUserData) FilterResumeProgress(_ context.Context, _ *Session, entries []upstreamProgress) ([]upstreamProgress, error) { + s.filterCalls++ + s.filteredBatchSize = append(s.filteredBatchSize, len(entries)) + kept := make([]upstreamProgress, 0, len(entries)) + for _, entry := range entries { + if s.hidden[entry.MediaItemID] { + continue + } + kept = append(kept, entry) + } + return kept, nil +} + +func resumeTestHandler(userData UserDataService, itemsByID map[string]*models.MediaItem) *ItemsHandler { + codec := NewResourceIDCodec() + return &ItemsHandler{ + content: &stubContentService{detail: &upstreamItemDetail{}}, + userData: userData, + itemRepo: &countingItemRepo{itemsByID: itemsByID}, + codec: codec, + mapper: newMapper(codec, &config.Config{}), + } +} + +func dtoNames(dtos []baseItemDTO) []string { + names := make([]string, 0, len(dtos)) + for _, dto := range dtos { + names = append(names, dto.Name) + } + return names +} + +// TestLoadProgressPage_ResumeHidesFilteredEntries pins that the Resume path +// runs every raw batch through FilterResumeProgress so dismissed and +// superseded entries never reach Jellyfin clients, mirroring the first-party +// Continue Watching row. +func TestLoadProgressPage_ResumeHidesFilteredEntries(t *testing.T) { + items := map[string]*models.MediaItem{ + "movie-1": {ContentID: "movie-1", Type: "movie", Title: "Movie One"}, + "movie-2": {ContentID: "movie-2", Type: "movie", Title: "Movie Two"}, + "movie-3": {ContentID: "movie-3", Type: "movie", Title: "Movie Three"}, + } + userData := &resumeFilteringUserData{ + entries: []upstreamProgress{ + {MediaItemID: "movie-1", PositionSeconds: 10, DurationSeconds: 100}, + {MediaItemID: "movie-2", PositionSeconds: 20, DurationSeconds: 100}, + {MediaItemID: "movie-3", PositionSeconds: 30, DurationSeconds: 100}, + }, + hidden: map[string]bool{"movie-2": true}, + } + h := resumeTestHandler(userData, items) + + session := &Session{StreamAppUserID: 1, ProfileID: "profile-1"} + dtos, _, err := h.loadProgressPage(context.Background(), session, "in_progress", itemsQuery{limit: 10}, nil, nil) + if err != nil { + t.Fatalf("loadProgressPage: %v", err) + } + + got := dtoNames(dtos) + want := []string{"Movie One", "Movie Three"} + if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { + t.Fatalf("resume names = %v, want %v", got, want) + } + if userData.filterCalls == 0 { + t.Fatal("expected FilterResumeProgress to be called for in_progress status") + } +} + +// TestLoadProgressPage_ResumeFilterAppliesWithTypeFilter covers the +// scan-from-zero branch (IncludeItemTypes present): filtering must apply +// there too, and visible pagination must skip filtered entries. +func TestLoadProgressPage_ResumeFilterAppliesWithTypeFilter(t *testing.T) { + items := map[string]*models.MediaItem{ + "movie-1": {ContentID: "movie-1", Type: "movie", Title: "Movie One"}, + "movie-2": {ContentID: "movie-2", Type: "movie", Title: "Movie Two"}, + "movie-3": {ContentID: "movie-3", Type: "movie", Title: "Movie Three"}, + } + userData := &resumeFilteringUserData{ + entries: []upstreamProgress{ + {MediaItemID: "movie-1", PositionSeconds: 10, DurationSeconds: 100}, + {MediaItemID: "movie-2", PositionSeconds: 20, DurationSeconds: 100}, + {MediaItemID: "movie-3", PositionSeconds: 30, DurationSeconds: 100}, + }, + hidden: map[string]bool{"movie-1": true}, + } + h := resumeTestHandler(userData, items) + + session := &Session{StreamAppUserID: 1, ProfileID: "profile-1"} + query := itemsQuery{limit: 10, enableTotalRecordCount: true} + typeSet := map[string]bool{"movie": true} + dtos, total, err := h.loadProgressPage(context.Background(), session, "in_progress", query, typeSet, nil) + if err != nil { + t.Fatalf("loadProgressPage: %v", err) + } + + got := dtoNames(dtos) + want := []string{"Movie Two", "Movie Three"} + if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { + t.Fatalf("resume names = %v, want %v", got, want) + } + if total != 2 { + t.Fatalf("TotalRecordCount = %d, want 2 (hidden entries excluded)", total) + } +} + +// TestLoadProgressPage_CompletedSkipsResumeFilter pins that the watched-items +// view ("completed" status) is served unfiltered: dismissals and superseded +// hiding are Continue Watching semantics only. +func TestLoadProgressPage_CompletedSkipsResumeFilter(t *testing.T) { + items := map[string]*models.MediaItem{ + "movie-1": {ContentID: "movie-1", Type: "movie", Title: "Movie One"}, + } + userData := &resumeFilteringUserData{ + entries: []upstreamProgress{ + {MediaItemID: "movie-1", PositionSeconds: 100, DurationSeconds: 100, Completed: true}, + }, + hidden: map[string]bool{"movie-1": true}, + } + h := resumeTestHandler(userData, items) + + session := &Session{StreamAppUserID: 1, ProfileID: "profile-1"} + dtos, _, err := h.loadProgressPage(context.Background(), session, "completed", itemsQuery{limit: 10}, nil, nil) + if err != nil { + t.Fatalf("loadProgressPage: %v", err) + } + + if userData.filterCalls != 0 { + t.Fatalf("FilterResumeProgress calls = %d, want 0 for completed status", userData.filterCalls) + } + if len(dtos) != 1 || dtos[0].Name != "Movie One" { + t.Fatalf("completed names = %v, want [Movie One]", dtoNames(dtos)) + } +} + +// fakeDismissalStore embeds userstore.UserStore so only the methods +// FilterResumeProgress touches need real implementations. +type fakeDismissalStore struct { + userstore.UserStore + dismissals []userstore.HomeItemDismissal +} + +func (s *fakeDismissalStore) ListHomeDismissals(_ context.Context, _ string, _ string) ([]userstore.HomeItemDismissal, error) { + return s.dismissals, nil +} + +func (s *fakeDismissalStore) ListProgress(_ context.Context, _ string, _ string, _ int, _ int) ([]userstore.WatchProgress, error) { + return nil, nil +} + +type fakeDismissalStoreProvider struct { + store userstore.UserStore +} + +func (p *fakeDismissalStoreProvider) ForUser(context.Context, int) (userstore.UserStore, error) { + return p.store, nil +} + +func (p *fakeDismissalStoreProvider) Close() error { return nil } + +// TestDirectUserDataServiceFilterResumeProgressDropsDismissedEntries covers +// the glue in directUserDataService: dismissals from the store hide matching +// entries (same timestamp rule as the first-party row), everything else +// passes through. The superseded check is disabled via a nil pool — its +// behavior is pinned by the catalog package tests. +func TestDirectUserDataServiceFilterResumeProgressDropsDismissedEntries(t *testing.T) { + dismissedAt := "2025-01-01T00:00:00Z" + svc := &directUserDataService{ + storeProvider: &fakeDismissalStoreProvider{store: &fakeDismissalStore{ + dismissals: []userstore.HomeItemDismissal{ + {MediaItemID: "movie-1", ProgressUpdatedAt: &dismissedAt}, + }, + }}, + resumeFilter: catalog.NewContinueWatchingProgressFilter(nil), + } + + entries := []upstreamProgress{ + {MediaItemID: "movie-1", PositionSeconds: 10, DurationSeconds: 100, UpdatedAt: dismissedAt}, + {MediaItemID: "movie-2", PositionSeconds: 20, DurationSeconds: 100, UpdatedAt: dismissedAt}, + } + + session := &Session{StreamAppUserID: 1, ProfileID: "profile-1"} + got, err := svc.FilterResumeProgress(context.Background(), session, entries) + if err != nil { + t.Fatalf("FilterResumeProgress: %v", err) + } + if len(got) != 1 || got[0].MediaItemID != "movie-2" { + t.Fatalf("filtered = %+v, want only movie-2", got) + } +} diff --git a/internal/jellycompat/router.go b/internal/jellycompat/router.go index a2239969..8eab5064 100644 --- a/internal/jellycompat/router.go +++ b/internal/jellycompat/router.go @@ -10,6 +10,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" + "github.com/jackc/pgx/v5/pgxpool" "gopkg.in/natefinch/lumberjack.v2" "github.com/Silo-Server/silo-server/internal/catalog" @@ -56,6 +57,7 @@ func NewRouter(deps Dependencies) chi.Router { } slog.Info("jellycompat debug logging enabled", logAttrs...) } + r.Use(compatImageProxyTagVariantMiddleware(deps.IDCodec)) r.Use(requestLoggerMiddleware) r.Use(middleware.Recoverer) @@ -74,6 +76,11 @@ func NewRouter(deps Dependencies) chi.Router { } itemsHandler := NewItemsHandler(deps.ContentService, deps.UserDataService, deps.IDCodec, deps.Config, deps.ImageCache, nextUpRepo, deps.BrowseRepo, deps.PersonRepo, deps.DetailSvc, deps.ItemRepo, deps.EpisodeRepo, deps.AccessFilterFn, subtitleRepo) itemsHandler.recommender = deps.Recommender + if deps.DB != nil { + itemsHandler.collections = catalog.NewLibraryCollectionRepository(deps.DB) + } + itemsHandler.posterPresigner = deps.PosterPresigner + itemsHandler.presignTTL = deps.PresignTTL autoscanHandler := NewAutoscanHandler(deps.FolderRepo, deps.ScanQueue, deps.IDCodec, itemsHandler) adminAPIKeyAuth := NewAdminAPIKeyAuthenticator(deps.APIKeyValidator, deps.APIKeyUserLoader, deps.UserStoreProvider, deps.Now) autoscanVirtualFoldersRegistered := false @@ -99,7 +106,8 @@ func NewRouter(deps Dependencies) chi.Router { playbackHandler.S3Client = deps.S3Client playbackHandler.S3Bucket = deps.S3Bucket } - imagesHandler := NewImagesHandler(deps.ContentService, deps.IDCodec, deps.HTTPClient, deps.SessionStore, deps.ImageCache, deps.PersonRepo, deps.DetailSvc, deps.ItemRepo, deps.FolderRepo, deps.SeasonRepo, deps.EpisodeRepo, deps.AccessFilterFn, deps.PosterPresigner, deps.PresignTTL, deps.JWTSecret) + imagesHandler := NewImagesHandler(deps.ContentService, deps.IDCodec, deps.SessionStore, deps.ImageCache, deps.PersonRepo, deps.DetailSvc, deps.ItemRepo, deps.FolderRepo, deps.SeasonRepo, deps.EpisodeRepo, deps.AccessFilterFn, deps.PosterPresigner, deps.PresignTTL, deps.JWTSecret, deps.HTTPClient) + imagesHandler.collections = itemsHandler.collections displayPrefsHandler := NewDisplayPreferencesHandler(deps.UserStoreProvider) recsHandler := NewRecommendationsHandler(deps.Recommender, deps.ItemRepo, deps.ContentService, deps.UserDataService, deps.IDCodec, deps.Config, deps.AccessFilterFn) @@ -146,15 +154,18 @@ func NewRouter(deps Dependencies) chi.Router { r.Get("/Movies/{id}/Similar", itemsHandler.HandleSimilar) r.Get("/Shows/{id}/Similar", itemsHandler.HandleSimilar) r.Get("/Items/{id}/ThemeMedia", itemsHandler.HandleItemStub) + r.Get("/Items/{id}/ThemeSongs", itemsHandler.HandleThemeSongsStub) r.Get("/Items/{id}/SpecialFeatures", itemsHandler.HandleItemStub) r.Get("/Items/{id}/Intros", itemsHandler.HandleItemStub) r.Get("/Users/{userId}/Items/{id}/ThemeMedia", itemsHandler.HandleItemStub) + r.Get("/Users/{userId}/Items/{id}/ThemeSongs", itemsHandler.HandleThemeSongsStub) r.Get("/Users/{userId}/Items/{id}/SpecialFeatures", itemsHandler.HandleItemStub) r.Get("/Users/{userId}/Items/{id}/Intros", itemsHandler.HandleItemStub) r.Get("/Items/{id}", itemsHandler.HandleItem) r.Get("/Users/{userId}/Items/Resume", itemsHandler.HandleResume) r.Get("/Users/{userId}/Items/{id}", itemsHandler.HandleItem) r.Get("/Genres", itemsHandler.HandleGenres) + r.Get("/Genres/{name}", itemsHandler.HandleGenreByName) r.Get("/Shows/{id}/Seasons", itemsHandler.HandleSeasons) r.Get("/Shows/{id}/Episodes", itemsHandler.HandleEpisodes) r.Get("/Shows/NextUp", itemsHandler.HandleNextUp) @@ -207,6 +218,8 @@ func NewRouter(deps Dependencies) chi.Router { // (e.g. libmpv) that don't forward auth headers or query parameters. r.Group(func(r chi.Router) { r.Use(PlaybackSessionAuth(deps.SessionStore, deps.PlaybackStore, adminAPIKeyAuth)) + r.Method(http.MethodHead, "/Items/{id}/Download", http.HandlerFunc(playbackHandler.HandleDownload)) + r.Get("/Items/{id}/Download", playbackHandler.HandleDownload) r.Method(http.MethodHead, "/Videos/{id}/stream", http.HandlerFunc(playbackHandler.HandleVideoStream)) r.Get("/Videos/{id}/stream", playbackHandler.HandleVideoStream) r.Method(http.MethodHead, "/Videos/{id}/stream.{container}", http.HandlerFunc(playbackHandler.HandleVideoStream)) @@ -230,6 +243,10 @@ func withDefaults(deps Dependencies) Dependencies { if deps.Now == nil { deps.Now = timeNow } + // Stamp the compat surface's media-type exclusions onto every resolved + // access filter so all consumers (content service, items/images handlers, + // recommendations) inherit them without per-call-site guards. + deps.AccessFilterFn = compatAccessFilterResolver(deps.AccessFilterFn) if deps.JWTSecret == "" && deps.Config != nil { deps.JWTSecret = deps.Config.Auth.JWTSecret } @@ -261,15 +278,15 @@ func withDefaults(deps Dependencies) Dependencies { if deps.Config != nil && deps.Config.JellyfinCompat.PlaybackSessionTTL > 0 { playbackTTL = deps.Config.JellyfinCompat.PlaybackSessionTTL } - if deps.HTTPClient == nil { - deps.HTTPClient = &http.Client{Timeout: 30 * time.Second} - } if deps.DeviceProfiles == nil { deps.DeviceProfiles = NewDeviceProfileStore(playbackTTL, deps.Now) } if deps.PlaybackStore == nil { deps.PlaybackStore = NewPlaybackSessionStore(playbackTTL, deps.Now) } + if deps.HTTPClient == nil { + deps.HTTPClient = &http.Client{Timeout: 30 * time.Second} + } // Build ContentService from repos if not provided if deps.ContentService == nil && deps.BrowseRepo != nil && deps.ItemRepo != nil && deps.DetailSvc != nil { @@ -296,12 +313,17 @@ func withDefaults(deps Dependencies) Dependencies { if deps.DB != nil { staler = recommendations.NewRepo(deps.DB) } + var pool *pgxpool.Pool + if deps.BrowseRepo != nil { + pool = deps.BrowseRepo.Pool() + } deps.UserDataService = newDirectUserDataService( deps.UserStoreProvider, deps.ItemRepo, deps.EpisodeRepo, deps.ProviderIDRepo, deps.DetailSvc, + catalog.NewContinueWatchingProgressFilter(pool), staler, deps.RecWorker, ) diff --git a/internal/jellycompat/server.go b/internal/jellycompat/server.go index f5733aea..ef6cf956 100644 --- a/internal/jellycompat/server.go +++ b/internal/jellycompat/server.go @@ -27,7 +27,6 @@ type Dependencies struct { DB *pgxpool.Pool SecretCipher *secret.Cipher // at-rest credential cipher (required when DB is set) ClientIPResolver *clientip.Resolver - HTTPClient *http.Client Now func() time.Time TokenGenerator func() string SessionStore *SessionStore @@ -38,6 +37,7 @@ type Dependencies struct { LoginResolver loginResolver Authenticator *Authenticator WebFS fs.FS + HTTPClient *http.Client // Direct service dependencies (replaces Client) ContentService ContentService @@ -145,6 +145,5 @@ func NewDependencies(cfg *config.Config) Dependencies { Config: cfg, Now: time.Now, TokenGenerator: uuid.NewString, - HTTPClient: &http.Client{Timeout: 30 * time.Second}, } } diff --git a/internal/jellycompat/streams.go b/internal/jellycompat/streams.go index 8b8ecfd0..9df8347e 100644 --- a/internal/jellycompat/streams.go +++ b/internal/jellycompat/streams.go @@ -113,6 +113,55 @@ func (h *PlaybackHandler) HandleVideoStream(w http.ResponseWriter, r *http.Reque } } +// HandleDownload serves the original media file for /Items/{id}/Download. +// This route backs the CanDownload flag set in mapping.go. CanDownload is +// load-bearing for Infuse: it refuses Direct Play (Static=true streaming) +// for items it believes it cannot download, so the flag must stay true and +// this route must exist. +func (h *PlaybackHandler) HandleDownload(w http.ResponseWriter, r *http.Request) { + session := SessionFromContext(r.Context()) + if session == nil { + writeError(w, http.StatusUnauthorized, "Unauthorized", "Missing authentication token") + return + } + + contentID, err := decodeContentID(h.codec, chiURLParam(r, "id")) + if err != nil { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + detail, err := h.content.GetItemDetail(r.Context(), session, contentID, nil) + if err != nil || detail == nil || len(detail.Versions) == 0 { + writeError(w, http.StatusNotFound, "NotFound", "Item not found") + return + } + + version := detail.Versions[0] + if mediaSourceID := firstNonEmpty(r.URL.Query().Get("mediaSourceId"), r.URL.Query().Get("MediaSourceId")); mediaSourceID != "" { + if fileID, decodeErr := h.codec.DecodeIntID(EncodedIDMediaSource, mediaSourceID); decodeErr == nil { + for _, v := range detail.Versions { + if int64(v.FileID) == fileID { + version = v + break + } + } + } + } + + if h.fileResolver == nil { + writeError(w, http.StatusInternalServerError, "ServerError", "File resolver not available") + return + } + file, err := h.fileResolver.GetByID(r.Context(), version.FileID) + if err != nil { + writeError(w, http.StatusNotFound, "NotFound", "Media file not found") + return + } + + w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(filepath.Base(file.FilePath))) + _ = playback.ServeDirectPlay(w, r, file.FilePath) +} + // HandleMasterManifest serves the compat-owned HLS manifest route. // It returns a full-duration VOD manifest so clients can seek to any position. // Segments that haven't been transcoded yet are served on-demand by the segment handler. @@ -123,7 +172,7 @@ func (h *PlaybackHandler) HandleMasterManifest(w http.ResponseWriter, r *http.Re return } - playSessionID := firstNonEmpty(r.URL.Query().Get("PlaySessionId"), r.URL.Query().Get("PlaySessionID")) + playSessionID := newCaseInsensitiveQuery(r.URL.Query()).Get("PlaySessionId") if playSessionID == "" { writeError(w, http.StatusBadRequest, "BadRequest", "PlaySessionId is required") return @@ -906,7 +955,7 @@ func audioSelectionChanged(session *PlaybackSession, mediaSourceID string, incom return true } for _, source := range session.MediaSources { - if mediaSourceID != "" && source.ID != mediaSourceID { + if mediaSourceID != "" && !mediaSourceIDsEqual(source.ID, mediaSourceID) { continue } if source.SelectedAudioStreamIndex == nil { @@ -925,7 +974,7 @@ func (h *PlaybackHandler) setSelectedAudioStream(playSessionID, mediaSourceID st if mediaSourceID != "" { sourceIndex = -1 for index := range current.MediaSources { - if current.MediaSources[index].ID == mediaSourceID { + if mediaSourceIDsEqual(current.MediaSources[index].ID, mediaSourceID) { sourceIndex = index break } @@ -1061,7 +1110,7 @@ func (h *PlaybackHandler) createStaticPlaySession(ctx context.Context, session * } func (h *PlaybackHandler) resolvePlaybackRoute(r *http.Request, compatSession *Session, routeID, mediaSourceID string) (*PlaybackSession, *PlaybackMediaSource, error) { - if playSessionID := firstNonEmpty(r.URL.Query().Get("PlaySessionId"), r.URL.Query().Get("PlaySessionID")); playSessionID != "" { + if playSessionID := newCaseInsensitiveQuery(r.URL.Query()).Get("PlaySessionId"); playSessionID != "" { playSession, ok := h.playbackStore.Get(playSessionID) if !ok || playSession.CompatToken != compatSession.Token { return nil, nil, ErrSessionNotFound @@ -1099,7 +1148,7 @@ func findMediaSource(session *PlaybackSession, mediaSourceID string) *PlaybackMe return nil } for _, source := range session.MediaSources { - if source.ID == mediaSourceID { + if mediaSourceIDsEqual(source.ID, mediaSourceID) { copy := source return © } diff --git a/internal/jellycompat/theme_songs_stub_test.go b/internal/jellycompat/theme_songs_stub_test.go new file mode 100644 index 00000000..eb3fa970 --- /dev/null +++ b/internal/jellycompat/theme_songs_stub_test.go @@ -0,0 +1,46 @@ +package jellycompat + +import ( + "context" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" +) + +// TestHandleThemeSongsStub_IncludesOwnerID verifies the empty ThemeMediaResult +// carries OwnerId: jellyfin-sdk-kotlin models it as non-nullable, so a plain +// query-result envelope would fail client deserialization. +func TestHandleThemeSongsStub_IncludesOwnerID(t *testing.T) { + h := &ItemsHandler{} + + itemID := "f27caa37e5142225cceded48f6553502" + req := httptest.NewRequest("GET", "/Items/"+itemID+"/ThemeSongs", nil) + routeCtx := chi.NewRouteContext() + routeCtx.URLParams.Add("id", itemID) + ctx := context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx) + ctx = context.WithValue(ctx, compatSessionKey, &Session{StreamAppUserID: 1, ProfileID: "profile-1"}) + req = req.WithContext(ctx) + + rec := httptest.NewRecorder() + h.HandleThemeSongsStub(rec, req) + + if rec.Code != 200 { + t.Fatalf("expected status 200; got %d, body=%s", rec.Code, rec.Body.String()) + } + var body struct { + Items []json.RawMessage `json:"Items"` + TotalRecordCount int `json:"TotalRecordCount"` + OwnerID *string `json:"OwnerId"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("response is not valid JSON: %v", err) + } + if body.Items == nil || len(body.Items) != 0 || body.TotalRecordCount != 0 { + t.Errorf("expected empty Items array; got body=%s", rec.Body.String()) + } + if body.OwnerID == nil || *body.OwnerID != itemID { + t.Errorf("expected OwnerId %q; got body=%s", itemID, rec.Body.String()) + } +} diff --git a/internal/jellycompat/upstream_types.go b/internal/jellycompat/upstream_types.go index 5b4d5a11..bdff2679 100644 --- a/internal/jellycompat/upstream_types.go +++ b/internal/jellycompat/upstream_types.go @@ -55,6 +55,10 @@ type upstreamListItem struct { Tagline string `json:"tagline,omitempty"` RatingTMDB *float64 `json:"rating_tmdb,omitempty"` UserData *catalog.SeasonUserData `json:"user_data,omitempty"` + // HasMediaFiles reports whether at least one live media file backs this + // item. nil means unknown (the producing query did not check); the mapper + // then keeps the historical LocationType=FileSystem default. + HasMediaFiles *bool `json:"-"` } type upstreamBrowseResponse struct { @@ -144,6 +148,10 @@ type upstreamEpisode struct { SeriesID string `json:"series_id,omitempty"` SeriesTitle string `json:"series_title,omitempty"` SeasonID string `json:"season_id,omitempty"` + // HasMediaFiles reports whether at least one live media file backs this + // episode. nil means unknown; the mapper then keeps the historical + // LocationType=FileSystem default. + HasMediaFiles *bool `json:"-"` } type upstreamProgress struct { diff --git a/internal/jellycompat/userdata_direct.go b/internal/jellycompat/userdata_direct.go index 5fde5abb..42315717 100644 --- a/internal/jellycompat/userdata_direct.go +++ b/internal/jellycompat/userdata_direct.go @@ -3,6 +3,7 @@ package jellycompat import ( "context" "fmt" + "log/slog" "time" "github.com/Silo-Server/silo-server/internal/catalog" @@ -16,6 +17,7 @@ type directUserDataService struct { itemRepo *catalog.ItemRepository detailSvc *catalog.DetailService watchState *watchstate.Service + resumeFilter *catalog.ContinueWatchingProgressFilter profileStaler profileStaler profileRefreshRequester profileRefreshRequester } @@ -26,6 +28,7 @@ func newDirectUserDataService( episodeRepo *catalog.EpisodeRepository, providerIDRepo *catalog.ProviderIDRepository, detailSvc *catalog.DetailService, + resumeFilter *catalog.ContinueWatchingProgressFilter, staler profileStaler, requester profileRefreshRequester, ) *directUserDataService { @@ -36,6 +39,7 @@ func newDirectUserDataService( watchState: watchstate.NewService(storeProvider).WithStableIdentityResolver( watchstate.NewStableIdentityResolver(itemRepo, episodeRepo, providerIDRepo), ), + resumeFilter: resumeFilter, profileStaler: staler, profileRefreshRequester: requester, } @@ -47,7 +51,12 @@ func (s *directUserDataService) ListFavorites(ctx context.Context, session *Sess return nil, fmt.Errorf("open user store: %w", err) } - favorites, err := store.ListFavorites(ctx, session.ProfileID, limit, offset) + // ABS-surface favorites (audiobooks/podcasts) are filtered out below, so + // the limit/offset window must apply to the *filtered* list — a raw + // store-level window would shift or shrink the visible page. Over-fetch + // the raw rows, filter, then window. + scanLimit := min(max((limit+offset)*2, 200), 10000) + favorites, err := store.ListFavorites(ctx, session.ProfileID, scanLimit, 0) if err != nil { return nil, fmt.Errorf("list favorites: %w", err) } @@ -69,19 +78,32 @@ func (s *directUserDataService) ListFavorites(ctx context.Context, session *Sess // Build a map for ordering by the original favorites list order itemMap := make(map[string]*upstreamListItem, len(items)) for _, mi := range items { + // Favorites are shared with the ABS surface; its media types are + // never exposed here (they would 404 on detail/PlaybackInfo). + if isCompatExcludedMediaType(mi.Type) { + continue + } li := mediaItemToListItem(mi) - li.PosterURL = compatPresignImage(s.detailSvc, ctx, li.PosterURL, "poster", compatCardImageSize) - li.BackdropURL = compatPresignImage(s.detailSvc, ctx, li.BackdropURL, "backdrop", compatCardImageSize) - li.LogoURL = compatPresignImage(s.detailSvc, ctx, li.LogoURL, "logo", compatCardImageSize) itemMap[mi.ContentID] = &li } - result := make([]upstreamListItem, 0, len(contentIDs)) + ordered := make([]upstreamListItem, 0, len(contentIDs)) for _, id := range contentIDs { if li, ok := itemMap[id]; ok { - result = append(result, *li) + ordered = append(ordered, *li) } } + + // Presign artwork only for the page being returned. + result := slicePage(ordered, offset, limit) + for i := range result { + result[i].PosterURL = compatPresignImage(s.detailSvc, ctx, result[i].PosterURL, "poster", compatCardImageSize) + result[i].BackdropURL = compatPresignImage(s.detailSvc, ctx, result[i].BackdropURL, "backdrop", compatCardImageSize) + result[i].LogoURL = compatPresignImage(s.detailSvc, ctx, result[i].LogoURL, "logo", compatCardImageSize) + } + if result == nil { + result = []upstreamListItem{} + } return result, nil } @@ -156,6 +178,44 @@ func (s *directUserDataService) ListProgress(ctx context.Context, session *Sessi return result, nil } +// FilterResumeProgress applies the same hiding rules as the first-party +// Continue Watching fetcher: dismissed entries and episodes superseded by a +// later-completed episode in the same series. +func (s *directUserDataService) FilterResumeProgress(ctx context.Context, session *Session, entries []upstreamProgress) ([]upstreamProgress, error) { + if len(entries) == 0 { + return entries, nil + } + store, err := s.storeProvider.ForUser(ctx, session.StreamAppUserID) + if err != nil { + return nil, fmt.Errorf("open user store: %w", err) + } + + progress := make([]userstore.WatchProgress, 0, len(entries)) + for _, entry := range entries { + progress = append(progress, fromUpstreamProgress(entry)) + } + + // Dismissal lookup failures degrade to showing the entries, matching the + // first-party fetcher. + if dismissals, err := store.ListHomeDismissals(ctx, session.ProfileID, userstore.HomeSurfaceContinueWatching); err != nil { + slog.Error("listing continue watching dismissals", "profile_id", session.ProfileID, "error", err) + } else { + progress = catalog.NewHomeDismissalIndex(dismissals).FilterProgress(progress) + } + + superseded, err := s.resumeFilter.SupersededEpisodeProgressIDs(ctx, store, session.ProfileID, progress) + if err != nil { + return nil, fmt.Errorf("filter superseded progress: %w", err) + } + progress = catalog.FilterSupersededProgress(progress, superseded) + + result := make([]upstreamProgress, 0, len(progress)) + for _, entry := range progress { + result = append(result, toUpstreamProgress(entry)) + } + return result, nil +} + func (s *directUserDataService) ListProgressByMediaItems(ctx context.Context, session *Session, mediaItemIDs []string) (map[string]*upstreamProgress, error) { store, err := s.storeProvider.ForUser(ctx, session.StreamAppUserID) if err != nil { @@ -252,3 +312,13 @@ func toUpstreamProgress(entry userstore.WatchProgress) upstreamProgress { UpdatedAt: entry.UpdatedAt, } } + +func fromUpstreamProgress(entry upstreamProgress) userstore.WatchProgress { + return userstore.WatchProgress{ + MediaItemID: entry.MediaItemID, + PositionSeconds: entry.PositionSeconds, + DurationSeconds: entry.DurationSeconds, + Completed: entry.Completed, + UpdatedAt: entry.UpdatedAt, + } +} diff --git a/internal/jellycompat/virtual_episode_test.go b/internal/jellycompat/virtual_episode_test.go new file mode 100644 index 00000000..043f6a6c --- /dev/null +++ b/internal/jellycompat/virtual_episode_test.go @@ -0,0 +1,104 @@ +package jellycompat + +import ( + "testing" + + "github.com/Silo-Server/silo-server/internal/catalog" + "github.com/Silo-Server/silo-server/internal/config" +) + +// TestVirtualEpisodeLocationType verifies the fix: a playable episode with no +// underlying file versions (provider-metadata-only / unaired) reports +// LocationType=Virtual with no MediaSources, while a real episode with a +// version reports LocationType=FileSystem and gets MediaSources. +func TestVirtualEpisodeLocationType(t *testing.T) { + m := newMapper(NewResourceIDCodec(), &config.Config{}) + fields := map[string]bool{"mediasources": true} + + // Fileless episode -> Virtual, no media sources. + fileless := upstreamItemDetail{ + ContentID: "1001", + Type: "episode", + Title: "Unaired Episode", + Versions: nil, + } + dto := m.itemFromDetailWithFields(fileless, false, nil, fields) + if dto.LocationType != "Virtual" { + t.Errorf("fileless episode LocationType = %q, want Virtual", dto.LocationType) + } + if dto.VideoType != "" { + t.Errorf("fileless episode VideoType = %q, want empty", dto.VideoType) + } + if len(dto.MediaSources) != 0 { + t.Errorf("fileless episode MediaSources = %d, want 0", len(dto.MediaSources)) + } + + // Episode with a real file version -> FileSystem + MediaSources present. + withFile := upstreamItemDetail{ + ContentID: "1002", + Type: "episode", + Title: "Real Episode", + Versions: []catalog.FileVersion{{ + FileID: 42, + Container: "mkv", + Duration: 1200, + }}, + } + dto2 := m.itemFromDetailWithFields(withFile, false, nil, fields) + if dto2.LocationType != "FileSystem" { + t.Errorf("real episode LocationType = %q, want FileSystem", dto2.LocationType) + } + if len(dto2.MediaSources) != 1 { + t.Errorf("real episode MediaSources = %d, want 1", len(dto2.MediaSources)) + } +} + +// TestVirtualEpisodeListPaths verifies the list-side mappers honor the +// HasMediaFiles signal: false -> Virtual, true -> FileSystem, and nil +// (unknown, producing query did not check) preserves the historical +// FileSystem default. +func TestVirtualEpisodeListPaths(t *testing.T) { + m := newMapper(NewResourceIDCodec(), &config.Config{}) + boolp := func(v bool) *bool { return &v } + + cases := []struct { + name string + hasMediaFiles *bool + wantLocation string + wantVideoType string + }{ + {"fileless", boolp(false), "Virtual", ""}, + {"with file", boolp(true), "FileSystem", "VideoFile"}, + {"unknown", nil, "FileSystem", "VideoFile"}, + } + + for _, tc := range cases { + t.Run("itemFromList/"+tc.name, func(t *testing.T) { + dto := m.itemFromList(upstreamListItem{ + ContentID: "2001", + Type: "episode", + Title: "Episode", + HasMediaFiles: tc.hasMediaFiles, + }, false, nil, nil) + if dto.LocationType != tc.wantLocation { + t.Errorf("LocationType = %q, want %q", dto.LocationType, tc.wantLocation) + } + if dto.VideoType != tc.wantVideoType { + t.Errorf("VideoType = %q, want %q", dto.VideoType, tc.wantVideoType) + } + }) + t.Run("episodeFromUpstream/"+tc.name, func(t *testing.T) { + dto := m.episodeFromUpstream(upstreamEpisode{ + ContentID: "3001", + Title: "Episode", + HasMediaFiles: tc.hasMediaFiles, + }, false, nil) + if dto.LocationType != tc.wantLocation { + t.Errorf("LocationType = %q, want %q", dto.LocationType, tc.wantLocation) + } + if dto.VideoType != tc.wantVideoType { + t.Errorf("VideoType = %q, want %q", dto.VideoType, tc.wantVideoType) + } + }) + } +} diff --git a/internal/jellycompat/web.go b/internal/jellycompat/web.go index 694de6cb..4900a924 100644 --- a/internal/jellycompat/web.go +++ b/internal/jellycompat/web.go @@ -44,6 +44,10 @@ func newCompatWebHandler(webFS fs.FS, version string) http.Handler { if version != "" { w.Header().Set("X-Silo-Jellyfin-Web-Version", version) } + // Block content sniffing on everything we serve here. A CSP is + // intentionally NOT set: the vendored jellyfin-web bundle relies on + // inline scripts and would break under any meaningful policy. + w.Header().Set("X-Content-Type-Options", "nosniff") if fileExists(webFS, relPath) { if relPath == "index.html" { diff --git a/internal/metadata/chain.go b/internal/metadata/chain.go index a3c9c608..f287f1b4 100644 --- a/internal/metadata/chain.go +++ b/internal/metadata/chain.go @@ -303,9 +303,21 @@ func resolveEnabledProvidersByPriority( priority int } - items := make([]ranked, len(caps)) - for i, c := range caps { - items[i] = ranked{cap: c, priority: LookupDefaultPriority(ctx, pool, c.PluginInstallationID, contentLevel)} + // Only providers that support contentLevel participate in the chain-less + // fallback. A provider declaring a non-empty default_priority map that omits + // this level is excluded outright (not merely ranked last), so a + // single-purpose provider is never invoked for content it does not handle. + items := make([]ranked, 0, len(caps)) + for _, c := range caps { + metadataJSON := lookupCapabilityMetadata(ctx, pool, c.PluginInstallationID, c.CapabilityID) + if !providerSupportsLevel(metadataJSON, contentLevel) { + slog.Debug("skipping metadata provider: does not declare support for content level", + "installation_id", c.PluginInstallationID, + "capability_id", c.CapabilityID, + "content_level", contentLevel) + continue + } + items = append(items, ranked{cap: c, priority: extractDefaultPriority(metadataJSON, contentLevel)}) } sort.SliceStable(items, func(i, j int) bool { @@ -326,32 +338,68 @@ func resolveEnabledProvidersByPriority( return buildProviders(ctx, sorted, resolver, pool), nil } -// LookupDefaultPriority queries plugin_capabilities for a provider's declared -// default_priority at the given content level. Returns 0 if not found. -func LookupDefaultPriority(ctx context.Context, pool *pgxpool.Pool, pluginInstallationID int, contentLevel string) int { +// providerSupportsLevel reports whether a metadata provider should participate +// in the chain-less global fallback for contentLevel. A provider that declares +// a non-empty default_priority map is treated as enumerating the content levels +// it supports: it is eligible only for levels present in that map with a +// positive priority. A provider that declares no default_priority makes no +// claim and stays eligible for every level (legacy behavior), ranked last. +// +// This is what keeps a single-purpose provider (e.g. an audiobook metadata +// provider declaring only {"audiobook": N}) from being pulled into video +// content levels when a library has no enabled chain entry for that level. +func providerSupportsLevel(metadataJSON []byte, contentLevel string) bool { + levels, declared := declaredPriorityLevels(metadataJSON) + if !declared { + return true + } + return levels[contentLevel] > 0 +} + +// lookupCapabilityMetadata returns the raw plugin_capabilities.metadata JSON for +// a provider's exact metadata_provider.v1 capability, or nil if absent. +func lookupCapabilityMetadata(ctx context.Context, pool *pgxpool.Pool, pluginInstallationID int, capabilityID string) []byte { var metadataJSON []byte err := pool.QueryRow(ctx, `SELECT metadata FROM plugin_capabilities - WHERE plugin_installation_id = $1 AND capability_type = 'metadata_provider.v1' - LIMIT 1`, + WHERE plugin_installation_id = $1 + AND capability_id = $2 + AND capability_type = 'metadata_provider.v1'`, pluginInstallationID, + capabilityID, ).Scan(&metadataJSON) if err != nil { - return 0 + return nil } + return metadataJSON +} - return extractDefaultPriority(metadataJSON, contentLevel) +// LookupDefaultPriority queries plugin_capabilities for a provider's declared +// default_priority at the given content level. Returns 0 if not found. +func LookupDefaultPriority(ctx context.Context, pool *pgxpool.Pool, pluginInstallationID int, capabilityID, contentLevel string) int { + return extractDefaultPriority(lookupCapabilityMetadata(ctx, pool, pluginInstallationID, capabilityID), contentLevel) } // extractDefaultPriority parses the default_priority for a content level from // capability metadata JSON. func extractDefaultPriority(metadataJSON []byte, contentLevel string) int { + levels, _ := declaredPriorityLevels(metadataJSON) + if v, ok := levels[contentLevel]; ok && v > 0 { + return int(v) + } + return 0 +} + +// declaredPriorityLevels parses a capability's default_priority map. The map may +// sit at the top level or inside a "metadata" envelope (plugin capability +// metadata wraps plugin-declared fields in a "metadata" sub-object). The second +// return value is true only when a non-empty map was found, i.e. the provider +// explicitly enumerates the content levels it supports. +func declaredPriorityLevels(metadataJSON []byte) (map[string]float64, bool) { var meta map[string]json.RawMessage if err := json.Unmarshal(metadataJSON, &meta); err != nil { - return 0 + return nil, false } - // default_priority may be at the top level or nested inside a "metadata" sub-object - // (plugin capability metadata wraps plugin-declared fields in a "metadata" envelope). dpRaw, ok := meta["default_priority"] if !ok { if innerRaw, innerOK := meta["metadata"]; innerOK { @@ -360,18 +408,15 @@ func extractDefaultPriority(metadataJSON []byte, contentLevel string) int { dpRaw, ok = inner["default_priority"] } } - if !ok { - return 0 - } + } + if !ok { + return nil, false } var dpMap map[string]float64 if err := json.Unmarshal(dpRaw, &dpMap); err != nil { - return 0 + return nil, false } - if v, ok := dpMap[contentLevel]; ok && v > 0 { - return int(v) - } - return 0 + return dpMap, len(dpMap) > 0 } // ListEnabledMetadataCapabilities returns all metadata_provider.v1 capabilities diff --git a/internal/metadata/chain_test.go b/internal/metadata/chain_test.go new file mode 100644 index 00000000..2f439723 --- /dev/null +++ b/internal/metadata/chain_test.go @@ -0,0 +1,59 @@ +package metadata + +import "testing" + +// Capability metadata as stored in plugin_capabilities.metadata: plugin-declared +// fields are wrapped in a "metadata" envelope. +const ( + audiobookCapMetadata = `{"metadata":{"default_priority":{"audiobook":2}},"display_name":"Audiobook Metadata"}` + tmdbCapMetadata = `{"metadata":{"default_priority":{"movie":2,"season":3,"series":3,"episode":3}},"display_name":"TMDB"}` + tvdbCapMetadata = `{"metadata":{"default_priority":{"season":2,"series":2,"episode":2}},"display_name":"TVDB"}` +) + +func TestProviderSupportsLevel(t *testing.T) { + cases := []struct { + name string + metadataJSON string + contentLevel string + want bool + }{ + // An audiobook-only provider must NOT be pulled into video content + // levels by the chain-less global fallback. This is the regression + // that let silo.audiobook-metadata hammer audiobook APIs with + // anime/movie/series titles. + {"audiobook provider excluded from episode", audiobookCapMetadata, "episode", false}, + {"audiobook provider excluded from series", audiobookCapMetadata, "series", false}, + {"audiobook provider excluded from season", audiobookCapMetadata, "season", false}, + {"audiobook provider excluded from movie", audiobookCapMetadata, "movie", false}, + {"audiobook provider included for audiobook", audiobookCapMetadata, "audiobook", true}, + + // Providers that declare a level stay eligible for it. + {"tmdb included for movie", tmdbCapMetadata, "movie", true}, + {"tmdb included for season", tmdbCapMetadata, "season", true}, + {"tmdb included for episode", tmdbCapMetadata, "episode", true}, + + // tvdb declares no movie level -> excluded from the movie fallback. + {"tvdb excluded from movie", tvdbCapMetadata, "movie", false}, + {"tvdb included for series", tvdbCapMetadata, "series", true}, + + // A provider that declares no default_priority makes no claim and stays + // eligible everywhere (legacy behavior), ranked last by priority 0. + {"no metadata is eligible", ``, "episode", true}, + {"empty object is eligible", `{}`, "episode", true}, + {"empty priority map is eligible", `{"metadata":{"default_priority":{}}}`, "episode", true}, + {"malformed json is eligible (fail-open)", `{not json`, "episode", true}, + + // A declared level with non-positive priority is not supported. + {"zero priority level not supported", `{"metadata":{"default_priority":{"episode":0}}}`, "episode", false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := providerSupportsLevel([]byte(tc.metadataJSON), tc.contentLevel) + if got != tc.want { + t.Fatalf("providerSupportsLevel(%q, %q) = %v, want %v", + tc.metadataJSON, tc.contentLevel, got, tc.want) + } + }) + } +} diff --git a/internal/metadata/movie_match_queue_repo.go b/internal/metadata/movie_match_queue_repo.go index 36240e86..2003f4eb 100644 --- a/internal/metadata/movie_match_queue_repo.go +++ b/internal/metadata/movie_match_queue_repo.go @@ -18,8 +18,32 @@ const ( movieQueueRetryDelay = 15 * time.Second seriesRootQueueQuietWindow = 10 * time.Second seriesRootQueueRetryDelay = 30 * time.Second + + // matchQueueRetryMaxDelay caps the exponential retry backoff shared by both + // match queues. Terminal outcomes like "no metadata found from any + // provider" stay in the queue but decay to at most one attempt per day + // instead of hot-looping at the base delay forever. A row self-heals by + // being deleted on the first successful attempt. + matchQueueRetryMaxDelay = 24 * time.Hour + // matchQueueBackoffMaxExponent clamps the 2^attempt_count factor so the + // float math stays finite for rows that accumulated very large attempt + // counts before backoff existed. + matchQueueBackoffMaxExponent = 16 ) +// matchQueueBackoffExpr returns the SQL expression both match queues use to +// schedule the next retry after a failure: base_delay * 2^attempt_count, +// capped at matchQueueRetryMaxDelay. attempt_count is incremented when a row +// is claimed, so the first failure backs off to twice the base delay. +// basePlaceholder and maxPlaceholder name the interval bind parameters (e.g. +// "$3", "$4") in the caller's statement. +func matchQueueBackoffExpr(basePlaceholder, maxPlaceholder string) string { + return fmt.Sprintf( + "NOW() + LEAST(%s::interval * power(2::float8, LEAST(attempt_count, %d)), %s::interval)", + basePlaceholder, matchQueueBackoffMaxExponent, maxPlaceholder, + ) +} + type MovieMatchQueueRepository struct { pool *pgxpool.Pool fileRepo *scannerrepo.FileRepository @@ -477,10 +501,10 @@ func (r *MovieMatchQueueRepository) UpdateError(ctx context.Context, mediaFileID if _, err := r.pool.Exec(ctx, ` UPDATE movie_match_queue SET last_error = $2, - available_at = NOW() + $3::interval, + available_at = `+matchQueueBackoffExpr("$3", "$4")+`, updated_at = NOW() WHERE media_file_id = $1 - `, mediaFileID, errText, intervalLiteral(movieQueueRetryDelay)); err != nil { + `, mediaFileID, errText, intervalLiteral(movieQueueRetryDelay), intervalLiteral(matchQueueRetryMaxDelay)); err != nil { return fmt.Errorf("updating movie queue error: %w", err) } return nil diff --git a/internal/metadata/provider_id_integrity.go b/internal/metadata/provider_id_integrity.go index 2850968d..1d45acb6 100644 --- a/internal/metadata/provider_id_integrity.go +++ b/internal/metadata/provider_id_integrity.go @@ -722,6 +722,19 @@ var mediaItemMergeSteps = []mediaItemMergeStep{ last_codec_video = CASE WHEN EXCLUDED.updated_at >= user_watch_progress.updated_at THEN EXCLUDED.last_codec_video ELSE user_watch_progress.last_codec_video END, last_edition_key = CASE WHEN EXCLUDED.updated_at >= user_watch_progress.updated_at THEN EXCLUDED.last_edition_key ELSE user_watch_progress.last_edition_key END`}, {"delete source progress", `DELETE FROM user_watch_progress WHERE media_item_id = $1`}, + {"merge ebook reader progress", ` + INSERT INTO ebook_reader_progress ( + user_id, profile_id, content_id, file_id, location, progress, updated_at + ) + SELECT user_id, profile_id, $2, file_id, location, progress, updated_at + FROM ebook_reader_progress + WHERE content_id = $1 + ON CONFLICT (user_id, profile_id, content_id) DO UPDATE + SET file_id = CASE WHEN EXCLUDED.updated_at >= ebook_reader_progress.updated_at THEN EXCLUDED.file_id ELSE ebook_reader_progress.file_id END, + location = CASE WHEN EXCLUDED.updated_at >= ebook_reader_progress.updated_at THEN EXCLUDED.location ELSE ebook_reader_progress.location END, + progress = CASE WHEN EXCLUDED.updated_at >= ebook_reader_progress.updated_at THEN EXCLUDED.progress ELSE ebook_reader_progress.progress END, + updated_at = GREATEST(ebook_reader_progress.updated_at, EXCLUDED.updated_at)`}, + {"delete source ebook reader progress", `DELETE FROM ebook_reader_progress WHERE content_id = $1`}, {"move history", `UPDATE user_watch_history SET media_item_id = $2 WHERE media_item_id = $1`}, {"merge hidden history", ` INSERT INTO user_history_hidden_items (user_id, profile_id, media_item_id, hidden_before, updated_at) diff --git a/internal/metadata/provider_id_integrity_test.go b/internal/metadata/provider_id_integrity_test.go index ef2ad10f..12441e8a 100644 --- a/internal/metadata/provider_id_integrity_test.go +++ b/internal/metadata/provider_id_integrity_test.go @@ -110,6 +110,25 @@ func TestMergeStepPlaceholdersAreBounded(t *testing.T) { } } +func TestMergeStepsPreserveEbookReaderProgress(t *testing.T) { + var hasMerge bool + var hasDelete bool + for _, step := range mediaItemMergeSteps { + if strings.Contains(step.sql, "ebook_reader_progress") && strings.Contains(step.sql, "INSERT INTO") { + hasMerge = true + } + if strings.Contains(step.sql, "ebook_reader_progress") && strings.Contains(step.sql, "DELETE FROM") { + hasDelete = true + } + } + if !hasMerge { + t.Fatal("media item merge steps should merge ebook_reader_progress rows") + } + if !hasDelete { + t.Fatal("media item merge steps should delete source ebook_reader_progress rows") + } +} + func TestBothCandidatesHaveContentDetectsRealUserData(t *testing.T) { withFiles := canonicalCandidate{ContentID: "a", ActiveFiles: 2} withLibraries := canonicalCandidate{ContentID: "b", LibraryCount: 1} diff --git a/internal/metadata/series_root_match_queue_repo.go b/internal/metadata/series_root_match_queue_repo.go index e832d405..e1c5c542 100644 --- a/internal/metadata/series_root_match_queue_repo.go +++ b/internal/metadata/series_root_match_queue_repo.go @@ -550,10 +550,10 @@ func (r *SeriesRootMatchQueueRepository) UpdateError(ctx context.Context, folder _, err := r.pool.Exec(ctx, ` UPDATE series_root_match_queue SET last_error = $3, - available_at = NOW() + $4::interval, + available_at = `+matchQueueBackoffExpr("$4", "$5")+`, updated_at = NOW() WHERE media_folder_id = $1 AND observed_root_path = $2 - `, folderID, filepath.Clean(observedRootPath), errText, intervalLiteral(seriesRootQueueRetryDelay)) + `, folderID, filepath.Clean(observedRootPath), errText, intervalLiteral(seriesRootQueueRetryDelay), intervalLiteral(matchQueueRetryMaxDelay)) if err != nil { return fmt.Errorf("updating series root queue error: %w", err) } diff --git a/internal/metadata/service.go b/internal/metadata/service.go index 3cc23d26..754db16c 100644 --- a/internal/metadata/service.go +++ b/internal/metadata/service.go @@ -3856,10 +3856,6 @@ func (s *MetadataService) createOrFindSkeleton(ctx context.Context, file *models if res.Type == "" { res.Type = "movie" } - folderType := res.Type - if folderType == "" { - folderType = "movie" - } // Explicit structured IDs from the file or folder are treated as trusted. // When present, they override scanner ambiguity and become the authoritative @@ -3874,9 +3870,9 @@ func (s *MetadataService) createOrFindSkeleton(ctx context.Context, file *models // Parse observed location names for external IDs before falling back to the // legacy canonical root path. This preserves existing heuristic behavior for // roots without explicit structured tags. - folderIDs := naming.ParseFolderIDs(filepath.Base(observedRootPath), folderType) + folderIDs := naming.ParseFolderIDs(filepath.Base(observedRootPath)) if folderIDs == nil && contentRootPath != "" && contentRootPath != observedRootPath { - folderIDs = naming.ParseFolderIDs(filepath.Base(contentRootPath), folderType) + folderIDs = naming.ParseFolderIDs(filepath.Base(contentRootPath)) } effectiveExternalIDs := folderIDs diff --git a/internal/models/ebook_progress.go b/internal/models/ebook_progress.go new file mode 100644 index 00000000..0e249595 --- /dev/null +++ b/internal/models/ebook_progress.go @@ -0,0 +1,8 @@ +package models + +// EbookFinishedProgressThreshold is the reading-progress ratio (0..1) at or +// above which an ebook counts as finished/read. Progress strictly between 0 +// and this threshold counts as in progress. Shared by the reader API, +// catalog query builder, sections, and recommendation signals so the +// definition of "finished" cannot drift between subsystems. +const EbookFinishedProgressThreshold = 0.9 diff --git a/internal/naming/folderid.go b/internal/naming/folderid.go index d977648a..5a4a96a3 100644 --- a/internal/naming/folderid.go +++ b/internal/naming/folderid.go @@ -3,7 +3,6 @@ package naming import ( "regexp" "strings" - "unicode" ) // folderIDPattern matches patterns like [tmdbid-27205], {tmdb-27205}, @@ -11,7 +10,6 @@ import ( // The regex captures the provider prefix and the ID value. var folderIDPattern = regexp.MustCompile(`[{\[](tmdb|tmdbid|imdb|imdbid|tvdb|tvdbid)-([\w]+)[}\]]`) var trailingImdbIDPattern = regexp.MustCompile(`(?i)(?:^|\s)(tt\d{7,8})$`) -var trailingNumericIDPattern = regexp.MustCompile(`(?:^|\s)(\d+)$`) // bracketedBareImdbPattern matches a bare IMDb id wrapped in brackets without a // provider prefix, e.g. [tt10011226] or {tt0095016} (Plex/Kodi-style tags). A @@ -48,54 +46,30 @@ func ParseStructuredFolderIDs(name string) *FolderIDHints { return hints } -// ParseFolderIDs extracts external provider IDs from a folder name. -// It supports bracket styles [] and {} with provider prefixes tmdb/tmdbid, -// imdb/imdbid, tvdb/tvdbid, plus bare trailing IDs. Bare numeric IDs are -// interpreted using folderType: series -> TVDB, everything else -> TMDB. -func ParseFolderIDs(folderName string, folderType string) *FolderIDHints { - if hints := ParseStructuredFolderIDs(folderName); hints != nil { - return hints - } +// ParseFolderIDs extracts external provider IDs from a folder name. Mirroring +// Jellyfin's path-attribute model, only explicit evidence is honored: bracket +// tags with provider prefixes ({tmdb-27205}, [tvdbid-81189], [imdbid-tt1375666]), +// bracketed bare IMDb ids ([tt1375666]), and a trailing bare IMDb id — the +// "tt" prefix makes IMDb ids unambiguous without brackets. Bare trailing +// numbers are never treated as IDs: titles legitimately end in numbers +// ("District 9", "Beverly Hills 90210"), no mainstream tool emits bare-number +// tags, and a misparsed ID becomes a trusted match hint downstream where it +// silently produces a wrong match or blocks matching entirely. +func ParseFolderIDs(folderName string) *FolderIDHints { + hints := ParseStructuredFolderIDs(folderName) + // A trailing bare IMDb id complements structured tags for other providers + // ("Show [tvdbid-81189] tt1375666"); an explicit structured imdb tag still + // wins when both are present. trimmed := strings.TrimSpace(folderName) if m := trailingImdbIDPattern.FindStringSubmatch(trimmed); m != nil { - return &FolderIDHints{ImdbID: strings.ToLower(m[1])} - } - - m := trailingNumericIDPattern.FindStringSubmatch(trimmed) - if m == nil { - return nil - } - - id := m[1] - if looksLikeYear(id) { - return nil - } - - // A bare trailing number is only an ID when appended to a real title. If the - // name has no letters (e.g. "86", "22 7"), it's a numeric title, not an ID. - // Trade-off: a folder named ONLY a bare provider id (e.g. "81189") with no - // title text now returns nil rather than that id — acceptable because - // Sonarr/Radarr never produce bare-number folders. - if !containsLetter(trimmed) { - return nil - } - - if strings.EqualFold(strings.TrimSpace(folderType), "series") { - return &FolderIDHints{TvdbID: id} - } - return &FolderIDHints{TmdbID: id} -} - -func containsLetter(s string) bool { - for _, r := range s { - if unicode.IsLetter(r) { - return true + if hints == nil { + return &FolderIDHints{ImdbID: strings.ToLower(m[1])} + } + if hints.ImdbID == "" { + hints.ImdbID = strings.ToLower(m[1]) } } - return false -} -func looksLikeYear(value string) bool { - return len(value) == 4 && value >= "1800" && value <= "2100" + return hints } diff --git a/internal/naming/folderid_numeric_test.go b/internal/naming/folderid_numeric_test.go index 5e0e61be..3567256b 100644 --- a/internal/naming/folderid_numeric_test.go +++ b/internal/naming/folderid_numeric_test.go @@ -2,36 +2,63 @@ package naming import "testing" -func TestParseFolderIDs_NumericTitleIsNotAnID(t *testing.T) { - // Numeric-only anime titles must NOT be parsed as a bare trailing tvdb/tmdb id. - if got := ParseFolderIDs("86", "series"); got != nil { - t.Errorf(`ParseFolderIDs("86","series") = %+v, want nil`, got) - } - if got := ParseFolderIDs("22 7", "series"); got != nil { - t.Errorf(`ParseFolderIDs("22 7","series") = %+v, want nil`, got) - } - - // A real bare trailing id with title text must still be parsed. - got := ParseFolderIDs("Some Show 81189", "series") - if got == nil || got.TvdbID != "81189" { - t.Errorf(`ParseFolderIDs("Some Show 81189","series") = %+v, want TvdbID="81189"`, got) - } - - // Structured tags must still win regardless of letters. - got = ParseFolderIDs("{tmdb-27205}", "movies") - if got == nil || got.TmdbID != "27205" { - t.Errorf(`ParseFolderIDs("{tmdb-27205}","movies") = %+v, want TmdbID="27205"`, got) - } - - // Non-Latin (CJK) title with a trailing real id must still parse — - // unicode.IsLetter covers kana/kanji, so this is treated as title + id. - got = ParseFolderIDs("進撃の巨人 73743", "series") - if got == nil || got.TvdbID != "73743" { - t.Errorf(`ParseFolderIDs("進撃の巨人 73743","series") = %+v, want TvdbID="73743"`, got) - } - - // Numeric-only title for a movies library is also not an id. - if got := ParseFolderIDs("86", "movies"); got != nil { - t.Errorf(`ParseFolderIDs("86","movies") = %+v, want nil`, got) +// Bare numbers in folder names are never provider IDs. This mirrors +// Jellyfin's path-attribute model: only explicit bracket tags (and +// unambiguous tt-prefixed IMDb ids) carry identity. Titles legitimately end +// in numbers, and a misparsed bare ID becomes a trusted match hint that +// silently mismatches or blocks matching. +func TestParseFolderIDs_BareNumbersAreNeverIDs(t *testing.T) { + for _, c := range []string{ + // Numeric-only titles (anime and otherwise). + "86", + "22 7", + // Titles ending in a short number. + "District 9", + "Apollo 13", + "Ocean's 11", + "THX 1138", + "Stranger Things 4", + // Season-style directory names. + "Season 01", + // Titles ending in a long number that looks like a plausible ID. + "Beverly Hills 90210", + // Bare trailing numbers that previously parsed as IDs. + "Some Show 81189", + "進撃の巨人 73743", + } { + if got := ParseFolderIDs(c); got != nil { + t.Errorf(`ParseFolderIDs(%q) = %+v, want nil`, c, got) + } + } +} + +func TestParseFolderIDs_StructuredTagsStillParse(t *testing.T) { + got := ParseFolderIDs("{tmdb-27205}") + if got == nil || got.TmdbID != "27205" { + t.Errorf(`ParseFolderIDs("{tmdb-27205}") = %+v, want TmdbID="27205"`, got) + } + + got = ParseFolderIDs("Some Show (2010) [tvdbid-81189]") + if got == nil || got.TvdbID != "81189" { + t.Errorf(`ParseFolderIDs("Some Show (2010) [tvdbid-81189]") = %+v, want TvdbID="81189"`, got) + } + + // Trailing bare IMDb ids stay supported — the tt prefix is unambiguous. + got = ParseFolderIDs("Some Movie (2010) tt1375666") + if got == nil || got.ImdbID != "tt1375666" { + t.Errorf(`ParseFolderIDs("Some Movie (2010) tt1375666") = %+v, want ImdbID="tt1375666"`, got) + } + + // A structured tag for one provider must not swallow a trailing bare IMDb + // id for another — both merge. + got = ParseFolderIDs("Some Show (2010) [tvdbid-81189] tt1375666") + if got == nil || got.TvdbID != "81189" || got.ImdbID != "tt1375666" { + t.Errorf(`ParseFolderIDs("Some Show (2010) [tvdbid-81189] tt1375666") = %+v, want TvdbID="81189" and ImdbID="tt1375666"`, got) + } + + // An explicit structured imdb tag still wins over a trailing bare id. + got = ParseFolderIDs("Some Movie [imdb-tt1111111] tt2222222") + if got == nil || got.ImdbID != "tt1111111" { + t.Errorf(`ParseFolderIDs("Some Movie [imdb-tt1111111] tt2222222") = %+v, want ImdbID="tt1111111"`, got) } } diff --git a/internal/naming/folderid_test.go b/internal/naming/folderid_test.go index 45960787..c0d30985 100644 --- a/internal/naming/folderid_test.go +++ b/internal/naming/folderid_test.go @@ -11,19 +11,19 @@ func TestParseFolderIDs_BracketedBareImdb(t *testing.T) { {"8-digit tt", "Movie (2024) [tt12345678]", "tt12345678"}, } for _, c := range cases { - got := ParseFolderIDs(c.folder, "movie") + got := ParseFolderIDs(c.folder) if got == nil || got.ImdbID != c.wantImdb { t.Errorf("%s: ParseFolderIDs(%q) = %+v, want ImdbID=%q", c.name, c.folder, got, c.wantImdb) } } // must NOT change existing behavior: - if got := ParseFolderIDs("Movie [imdb-tt1375666]", "movie"); got == nil || got.ImdbID != "tt1375666" { + if got := ParseFolderIDs("Movie [imdb-tt1375666]"); got == nil || got.ImdbID != "tt1375666" { t.Errorf("structured imdb regressed: %+v", got) } - if got := ParseFolderIDs("Some Movie (2020) [BB]", "movie"); got != nil { + if got := ParseFolderIDs("Some Movie (2020) [BB]"); got != nil { t.Errorf("non-tt bracket tag must not parse as id, got %+v", got) } - if got := ParseFolderIDs("17 Blocks (2021)", "movie"); got != nil { + if got := ParseFolderIDs("17 Blocks (2021)"); got != nil { t.Errorf("no-id folder must return nil, got %+v", got) } } diff --git a/internal/naming/group_identity.go b/internal/naming/group_identity.go index 117a01b6..97320024 100644 --- a/internal/naming/group_identity.go +++ b/internal/naming/group_identity.go @@ -47,19 +47,28 @@ func InferGroupIdentity(filePath string, libraryType string, assignment RootAssi } } + // Explicit structured provider IDs (e.g. {tmdb-694938}) anchor the group's + // identity regardless of how folder and file titles relate, so title + // conflicts must not mark the group ambiguous. Renamed releases inside a + // tagged folder ("Override (2021) {tmdb-694938}" containing "R.I.A. + // (2021).mkv") are the common case. + idAnchored := hasStructuredIDAnchor(cleanFilePath, group.ObservedRootPath) + if group.BaseType == "series" { - populateSeriesGroupIdentity(cleanFilePath, libraryType, assignment, &group) + populateSeriesGroupIdentity(cleanFilePath, libraryType, assignment, idAnchored, &group) } else { - populateMovieGroupIdentity(cleanFilePath, assignment, &group) + populateMovieGroupIdentity(cleanFilePath, assignment, idAnchored, &group) } - if ids := ParseFolderIDs(filepath.Base(group.ObservedRootPath), group.BaseType); ids != nil { + // ID persistence must mirror hasStructuredIDAnchor: any evidence strong + // enough to anchor identity must also reach downstream matching. + if ids := ParseFolderIDs(filepath.Base(group.ObservedRootPath)); ids != nil { group.TmdbID = ids.TmdbID group.ImdbID = ids.ImdbID group.TvdbID = ids.TvdbID } if group.TmdbID == "" || group.ImdbID == "" || group.TvdbID == "" { - if ids := ParseStructuredFolderIDs(strings.TrimSuffix(filepath.Base(cleanFilePath), filepath.Ext(cleanFilePath))); ids != nil { + if ids := ParseFolderIDs(strings.TrimSuffix(filepath.Base(cleanFilePath), filepath.Ext(cleanFilePath))); ids != nil { if group.TmdbID == "" { group.TmdbID = ids.TmdbID } @@ -79,7 +88,7 @@ func InferGroupIdentity(filePath string, libraryType string, assignment RootAssi return group } -func populateMovieGroupIdentity(filePath string, assignment RootAssignment, group *GroupIdentity) { +func populateMovieGroupIdentity(filePath string, assignment RootAssignment, idAnchored bool, group *GroupIdentity) { parentDir := filepath.Dir(filePath) parentTitle, parentYear, parentTrusted := parseInferFolderTitleYear(filepath.Base(group.ObservedRootPath)) parentTitle = StripComparisonSafeEditionSuffix(parentTitle) @@ -107,8 +116,12 @@ func populateMovieGroupIdentity(filePath string, assignment RootAssignment, grou case parentTrusted && relation == titleRelationSoft: reasons = append(reasons, "variant_suffix") case parentTrusted && relation == titleRelationHard: - state = "ambiguous" - reasons = append(reasons, "unrelated_title") + if idAnchored { + reasons = append(reasons, "unrelated_title_resolved_by_provider_ids") + } else { + state = "ambiguous" + reasons = append(reasons, "unrelated_title") + } default: if baseTitle == "" { baseTitle = StripComparisonSafeEditionSuffix(stem.Title) @@ -130,7 +143,7 @@ func populateMovieGroupIdentity(filePath string, assignment RootAssignment, grou baseYear = parentYear } - if assignment.HasEpisodePattern && !assignment.HasSeasonStructure && !parentTrusted { + if assignment.HasEpisodePattern && !assignment.HasSeasonStructure && !parentTrusted && !idAnchored { state = "ambiguous" reasons = append(reasons, "episode_pattern") } @@ -154,7 +167,7 @@ func populateMovieGroupIdentity(filePath string, assignment RootAssignment, grou }) } -func populateSeriesGroupIdentity(filePath string, libraryType string, assignment RootAssignment, group *GroupIdentity) { +func populateSeriesGroupIdentity(filePath string, libraryType string, assignment RootAssignment, idAnchored bool, group *GroupIdentity) { ctx := ResolvePathContext(filePath, libraryType) title := assignment.Title year := assignment.Year @@ -179,8 +192,12 @@ func populateSeriesGroupIdentity(filePath string, libraryType string, assignment case titleRelationSoft: reasons = append(reasons, "series_alias") case titleRelationHard: - state = "ambiguous" - reasons = append(reasons, "series_title_conflict") + if idAnchored { + reasons = append(reasons, "series_title_conflict_resolved_by_provider_ids") + } else { + state = "ambiguous" + reasons = append(reasons, "series_title_conflict") + } } if year == 0 { year = observedYear @@ -191,8 +208,12 @@ func populateSeriesGroupIdentity(filePath string, libraryType string, assignment case titleRelationSoft: reasons = append(reasons, "series_assignment_soft_conflict") case titleRelationHard: - state = "ambiguous" - reasons = append(reasons, "series_assignment_conflict") + if idAnchored { + reasons = append(reasons, "series_assignment_conflict_resolved_by_provider_ids") + } else { + state = "ambiguous" + reasons = append(reasons, "series_assignment_conflict") + } } } @@ -206,7 +227,7 @@ func populateSeriesGroupIdentity(filePath string, libraryType string, assignment if assignment.HasEpisodePattern && (assignment.HasSeasonStructure || (ctx != nil && ctx.HasSeasonStructure)) && state != "ambiguous" { confidence = "high" } - if title == "" && assignment.HasEpisodePattern { + if title == "" && assignment.HasEpisodePattern && !idAnchored { state = "ambiguous" reasons = append(reasons, "series_identity_missing") } @@ -229,6 +250,19 @@ func populateSeriesGroupIdentity(filePath string, libraryType string, assignment }) } +// hasStructuredIDAnchor reports whether the observed root folder or the file +// name carries explicit provider IDs — structured tags (e.g. {tmdb-694938}, +// [tvdbid-81189]) or an unambiguous tt-prefixed IMDb id ("Eggs Run (2021) +// tt8049994"). ParseFolderIDs returns only such explicit evidence, which is +// strong enough to anchor identity over a title conflict. +func hasStructuredIDAnchor(filePath, observedRootPath string) bool { + if ParseFolderIDs(filepath.Base(observedRootPath)) != nil { + return true + } + baseNoExt := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) + return ParseFolderIDs(baseNoExt) != nil +} + func deriveObservedRootPath(filePath, candidateRoot string) string { cleanFilePath := filepath.Clean(filePath) cleanRoot := filepath.Clean(candidateRoot) diff --git a/internal/naming/group_identity_test.go b/internal/naming/group_identity_test.go new file mode 100644 index 00000000..f4c0a741 --- /dev/null +++ b/internal/naming/group_identity_test.go @@ -0,0 +1,129 @@ +package naming + +import "testing" + +// A renamed release inside a provider-tagged folder must stay resolved: the +// structured IDs anchor the identity even though folder and file titles are +// unrelated (Radarr keeps the folder name from grab time while the release +// inside carries the retitled name). +func TestInferGroupIdentity_StructuredIDsResolveMovieTitleConflict(t *testing.T) { + group := InferGroupIdentity( + "/movies/20s/On Fire {imdb-tt28078628} {tmdb-1196791}/Soul on Fire (2025) [WEBDL-1080p 8-bit h264 EAC3 5.1]-FLUX.mkv", + "movies", + RootAssignment{ + RootPath: "/movies/20s/On Fire {imdb-tt28078628} {tmdb-1196791}", + InferredType: "movie", + }, + ) + + if got, want := group.State, "resolved"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } + if got, want := group.BaseTitle, "On Fire"; got != want { + t.Fatalf("BaseTitle = %q, want %q", got, want) + } + if got, want := group.TmdbID, "1196791"; got != want { + t.Fatalf("TmdbID = %q, want %q", got, want) + } + if got, want := group.ImdbID, "tt28078628"; got != want { + t.Fatalf("ImdbID = %q, want %q", got, want) + } +} + +// An unbracketed trailing IMDb id anchors identity the same way a structured +// tag does — the tt prefix is unambiguous. Mirrors the dev-library case +// "Eggs Run (2021) tt8049994" whose file carries a different release title. +func TestInferGroupIdentity_TrailingImdbIDResolvesMovieTitleConflict(t *testing.T) { + group := InferGroupIdentity( + "/movies-int/es/Eggs Run (2021) tt8049994/Huevitos Congelados (2022) [WEBDL-1080p]-GRP.mkv", + "movies", + RootAssignment{ + RootPath: "/movies-int/es/Eggs Run (2021) tt8049994", + InferredType: "movie", + }, + ) + + if got, want := group.State, "resolved"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } + if got, want := group.ImdbID, "tt8049994"; got != want { + t.Fatalf("ImdbID = %q, want %q", got, want) + } +} + +// A trailing IMDb id on the file stem (folder carries none) must both anchor +// the identity and be persisted into the group, so downstream matching sees +// the provider ID that justified the resolution. +func TestInferGroupIdentity_FileLevelTrailingImdbIDAnchorsAndPersists(t *testing.T) { + group := InferGroupIdentity( + "/movies/On Fire (2024)/Soul on Fire tt28078628.mkv", + "movies", + RootAssignment{ + RootPath: "/movies/On Fire (2024)", + InferredType: "movie", + }, + ) + + if got, want := group.State, "resolved"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } + if got, want := group.ImdbID, "tt28078628"; got != want { + t.Fatalf("ImdbID = %q, want %q", got, want) + } +} + +// Without provider IDs the same folder/file title conflict must still be +// flagged ambiguous — there is nothing to anchor the identity. +func TestInferGroupIdentity_TitleConflictWithoutIDsStaysAmbiguous(t *testing.T) { + group := InferGroupIdentity( + "/movies/20s/On Fire (2024)/Soul on Fire (2025) [WEBDL-1080p 8-bit h264 EAC3 5.1]-FLUX.mkv", + "movies", + RootAssignment{ + RootPath: "/movies/20s/On Fire (2024)", + InferredType: "movie", + }, + ) + + if got, want := group.State, "ambiguous"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } +} + +func TestInferGroupIdentity_StructuredIDsResolveSeriesTitleConflict(t *testing.T) { + group := InferGroupIdentity( + "/tv/Border Control - Sweden (2023) {tvdb-442777}/Season 01/Gränsbevakarna Sverige S01E01.mkv", + "series", + RootAssignment{ + RootPath: "/tv/Border Control - Sweden (2023) {tvdb-442777}", + InferredType: "series", + Title: "Gränsbevakarna Sverige", + HasSeasonStructure: true, + HasEpisodePattern: true, + }, + ) + + if got, want := group.State, "resolved"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } + if got, want := group.TvdbID, "442777"; got != want { + t.Fatalf("TvdbID = %q, want %q", got, want) + } +} + +func TestInferGroupIdentity_SeriesTitleConflictWithoutIDsStaysAmbiguous(t *testing.T) { + group := InferGroupIdentity( + "/tv/Border Control - Sweden (2023)/Season 01/Gränsbevakarna Sverige S01E01.mkv", + "series", + RootAssignment{ + RootPath: "/tv/Border Control - Sweden (2023)", + InferredType: "series", + Title: "Gränsbevakarna Sverige", + HasSeasonStructure: true, + HasEpisodePattern: true, + }, + ) + + if got, want := group.State, "ambiguous"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } +} diff --git a/internal/naming/movie_identity.go b/internal/naming/movie_identity.go index 9231ee0f..8e8f2b4e 100644 --- a/internal/naming/movie_identity.go +++ b/internal/naming/movie_identity.go @@ -157,7 +157,7 @@ func parseInferFolderTitleYear(name string) (string, int, bool) { year, _ := strconv.Atoi(match[2]) return strings.TrimSpace(match[1]), year, true } - if ids := ParseFolderIDs(name, "movie"); ids != nil || ParseFolderIDs(name, "series") != nil { + if ParseFolderIDs(name) != nil { return strings.TrimSpace(surface), 0, true } return strings.TrimSpace(surface), 0, false diff --git a/internal/naming/root_inference.go b/internal/naming/root_inference.go index f5db7672..431be6ae 100644 --- a/internal/naming/root_inference.go +++ b/internal/naming/root_inference.go @@ -89,7 +89,7 @@ func InferRootAssignments( Year: assignment.Year, }, } - if ids := ParseFolderIDs(filepath.Base(assignment.RootPath), assignment.InferredType); ids != nil { + if ids := ParseFolderIDs(filepath.Base(assignment.RootPath)); ids != nil { agg.hasFolderIDs = true agg.root.TmdbID = ids.TmdbID agg.root.ImdbID = ids.ImdbID @@ -254,7 +254,7 @@ func inferFileRootAssignment( } } - if ids := ParseFolderIDs(filepath.Base(assignment.RootPath), assignment.InferredType); ids != nil { + if ids := ParseFolderIDs(filepath.Base(assignment.RootPath)); ids != nil { assignment.HasFolderIDs = true } @@ -369,7 +369,7 @@ func detectInferMovieFolderEvidence(parentBase string, nameNoExt string, hasSeas if hasSeasonStructure { return false } - if ParseFolderIDs(parentBase, "movie") != nil || ParseFolderIDs(parentBase, "series") != nil { + if ParseFolderIDs(parentBase) != nil { return true } parentTitle, parentYear, trusted := parseInferFolderTitleYear(parentBase) @@ -457,7 +457,7 @@ func promoteCandidateRoot(filePath, currentRoot, title string, year int) (string parentBase := filepath.Base(parent) currentBase := filepath.Base(cleanRoot) - parentIDs := ParseFolderIDs(parentBase, "movie") != nil || ParseFolderIDs(parentBase, "series") != nil + parentIDs := ParseFolderIDs(parentBase) != nil childTitle, childYear := parseInferTitleYear(currentBase) parentTitle, parentYear := parseInferTitleYear(parentBase) diff --git a/internal/playback/session.go b/internal/playback/session.go index 23d0e0ed..18463e29 100644 --- a/internal/playback/session.go +++ b/internal/playback/session.go @@ -31,14 +31,15 @@ type Session struct { TargetBitrateKbps int // requested output bitrate cap for transcodes TranscodeHWAccel string // effective hardware acceleration mode for transcodes - Position float64 - IsPaused bool - HasWebSocket bool - HasRealtimeConnection bool - StartedAt time.Time - UpdatedAt time.Time - LastActivityAt time.Time - activeTransportCount int + Position float64 + IsPaused bool + HasWebSocket bool + HasRealtimeConnection bool + DisableProgressPersistence bool + StartedAt time.Time + UpdatedAt time.Time + LastActivityAt time.Time + activeTransportCount int } // SessionStreamState stores the mutable stream-specific details that can @@ -371,6 +372,23 @@ func (m *SessionManager) SetRealtimeConnection(sessionID string, connected bool) return nil } +// SetProgressPersistenceDisabled controls whether session progress updates and +// stop events should write resume/history state. This is useful for players +// whose resume timeline is not the same as the session's file-local timeline. +func (m *SessionManager) SetProgressPersistenceDisabled(sessionID string, disabled bool) error { + m.mu.Lock() + defer m.mu.Unlock() + + s, ok := m.sessions[sessionID] + if !ok { + return ErrSessionNotFound + } + + s.DisableProgressPersistence = disabled + m.touchSessionLocked(s) + return nil +} + // TouchActivity refreshes the session's activity timestamp without changing // any other playback state. func (m *SessionManager) TouchActivity(sessionID string) error { diff --git a/internal/pluginhost/client.go b/internal/pluginhost/client.go index 89cafc1a..b989fe14 100644 --- a/internal/pluginhost/client.go +++ b/internal/pluginhost/client.go @@ -54,6 +54,11 @@ type ScanSourceClient struct { timeout time.Duration } +type RequestRouterClient struct { + client pluginv1.RequestRouterClient + timeout time.Duration +} + type EventConsumerClient struct { client pluginv1.EventConsumerClient timeout time.Duration @@ -148,6 +153,16 @@ func (c *Client) ScanSource(capabilityID string) (*ScanSourceClient, error) { }, nil } +func (c *Client) RequestRouter(capabilityID string) (*RequestRouterClient, error) { + if err := c.requireCapability("request_router.v1", capabilityID); err != nil { + return nil, err + } + return &RequestRouterClient{ + client: c.rpc.RequestRouter(), + timeout: DefaultRequestRouterTimeout, + }, nil +} + func (c *Client) EventConsumer(capabilityID string) (*EventConsumerClient, error) { if err := c.requireCapability("event_consumer.v1", capabilityID); err != nil { return nil, err @@ -283,6 +298,36 @@ func (c *ScanSourceClient) PollChanges(ctx context.Context, req *pluginv1.PollCh return c.client.PollChanges(callCtx, req) } +func (c *RequestRouterClient) Fulfill(ctx context.Context, req *pluginv1.FulfillRequest) (*pluginv1.FulfillResponse, error) { + callCtx, cancel := ensureDeadline(ctx, c.timeout) + defer cancel() + return c.client.Fulfill(callCtx, req) +} + +func (c *RequestRouterClient) CheckStatus(ctx context.Context, req *pluginv1.CheckStatusRequest) (*pluginv1.CheckStatusResponse, error) { + callCtx, cancel := ensureDeadline(ctx, c.timeout) + defer cancel() + return c.client.CheckStatus(callCtx, req) +} + +func (c *RequestRouterClient) ListConfigOptions(ctx context.Context, req *pluginv1.ListConfigOptionsRequest) (*pluginv1.ListConfigOptionsResponse, error) { + callCtx, cancel := ensureDeadline(ctx, c.timeout) + defer cancel() + return c.client.ListConfigOptions(callCtx, req) +} + +func (c *RequestRouterClient) TestConnection(ctx context.Context, req *pluginv1.TestConnectionRequest) (*pluginv1.TestConnectionResponse, error) { + callCtx, cancel := ensureDeadline(ctx, c.timeout) + defer cancel() + return c.client.TestConnection(callCtx, req) +} + +func (c *RequestRouterClient) Validate(ctx context.Context, req *pluginv1.ValidateRequest) (*pluginv1.ValidateResponse, error) { + callCtx, cancel := ensureDeadline(ctx, c.timeout) + defer cancel() + return c.client.Validate(callCtx, req) +} + func (c *EventConsumerClient) HandleEvent(ctx context.Context, req *pluginv1.HandleEventRequest) (*pluginv1.HandleEventResponse, error) { callCtx, cancel := ensureDeadline(ctx, c.timeout) defer cancel() diff --git a/internal/pluginhost/handshake.go b/internal/pluginhost/handshake.go index f67184a2..c76d777e 100644 --- a/internal/pluginhost/handshake.go +++ b/internal/pluginhost/handshake.go @@ -23,6 +23,9 @@ const ( DefaultEventTimeout = 10 * time.Second DefaultAuthTimeout = 10 * time.Second DefaultRouteTimeout = 10 * time.Second + // DefaultRequestRouterTimeout bounds a single request_router RPC. + // Fulfillment hits remote arr instances, so allow generous headroom. + DefaultRequestRouterTimeout = 60 * time.Second ) func HandshakeConfig() plugin.HandshakeConfig { diff --git a/internal/plugins/service.go b/internal/plugins/service.go index a7f2f0f0..79dcf6b9 100644 --- a/internal/plugins/service.go +++ b/internal/plugins/service.go @@ -10,9 +10,11 @@ import ( "os" "path/filepath" "runtime/debug" + "strconv" "sync" "sync/atomic" + "golang.org/x/sync/singleflight" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" @@ -27,6 +29,7 @@ type pluginClient interface { MediaAnalyzer(capabilityID string) (*pluginhost.MediaAnalyzerClient, error) ScheduledTask(capabilityID string) (*pluginhost.ScheduledTaskClient, error) ScanSource(capabilityID string) (*pluginhost.ScanSourceClient, error) + RequestRouter(capabilityID string) (*pluginhost.RequestRouterClient, error) EventConsumer(capabilityID string) (*pluginhost.EventConsumerClient, error) AuthProvider(capabilityID string) (*pluginhost.AuthProviderClient, error) HTTPRoutes(capabilityID string) (*pluginhost.HTTPRoutesClient, error) @@ -66,6 +69,7 @@ type Service struct { dispatcher *EventDispatcher lifecycleMu sync.RWMutex lifecycleHooks []func(context.Context) + launchGroup singleflight.Group } // SetEventDispatcher wires the EventDispatcher into the Service. The @@ -482,6 +486,18 @@ func (s *Service) ScanSourceClient( return client.ScanSource(capabilityID) } +func (s *Service) RequestRouterClient( + ctx context.Context, + installationID int, + capabilityID string, +) (*pluginhost.RequestRouterClient, error) { + client, err := s.ensureClient(ctx, installationID) + if err != nil { + return nil, err + } + return client.RequestRouter(capabilityID) +} + func (s *Service) ScanSourceClientByPluginID( ctx context.Context, pluginID string, @@ -605,7 +621,25 @@ func (s *Service) ManifestForInstallation( return s.manifestForInstallation(ctx, installationID, false) } +// ensureClient returns a running client for the installation, collapsing +// concurrent first-use of a cold installation into a single launch so a burst of +// callers does not spawn redundant plugin processes (Host.Start releases its lock +// during the slow launch and cannot dedupe). After the flight completes the key +// is freed, so subsequent callers re-run and hit the now-warm cache. func (s *Service) ensureClient(ctx context.Context, installationID int) (pluginClient, error) { + v, err, _ := s.launchGroup.Do(strconv.Itoa(installationID), func() (any, error) { + // Isolate the shared launch from the leader caller's cancellation: other + // waiters depend on this in-flight launch, so a single caller's canceled + // request must not tear it down. Values (tracing, auth) are preserved. + return s.doEnsureClient(context.WithoutCancel(ctx), installationID) + }) + if err != nil { + return nil, err + } + return v.(pluginClient), nil +} + +func (s *Service) doEnsureClient(ctx context.Context, installationID int) (pluginClient, error) { installation, err := s.loadInstallation(ctx, installationID, true) if err != nil { return nil, err diff --git a/internal/plugins/service_connection.go b/internal/plugins/service_connection.go index 941e297f..c5170c18 100644 --- a/internal/plugins/service_connection.go +++ b/internal/plugins/service_connection.go @@ -45,6 +45,16 @@ var runPluginConnectionCheck = func( if err != nil { return err } + capability := metadataProviderConnectionCheckCapability(manifest, capabilityID) + if !metadataProviderSupportsConnectionProbe(capability, "movie") { + slog.Debug( + "skipping metadata provider connection check for unsupported probe type", + "plugin_id", manifest.GetPluginId(), + "capability_id", capabilityID, + "item_type", "movie", + ) + return nil + } metadataClient, err := client.MetadataProvider(capabilityID) if err != nil { @@ -224,3 +234,66 @@ func metadataProviderConnectionCheckCapabilityID(manifest *pluginv1.PluginManife Cause: ErrConnectionTestUnsupported, } } + +func metadataProviderConnectionCheckCapability( + manifest *pluginv1.PluginManifest, + capabilityID string, +) *pluginv1.CapabilityDescriptor { + for _, capability := range manifest.GetCapabilities() { + if capability.GetType() == "metadata_provider.v1" && capability.GetId() == capabilityID { + return capability + } + } + return nil +} + +func metadataProviderSupportsConnectionProbe( + capability *pluginv1.CapabilityDescriptor, + contentType string, +) bool { + priorities, ok := metadataProviderDefaultPriorities(capability) + if !ok { + return true + } + return priorities[contentType] > 0 +} + +func metadataProviderDefaultPriorities( + capability *pluginv1.CapabilityDescriptor, +) (map[string]float64, bool) { + if capability == nil || capability.GetMetadata() == nil { + return nil, false + } + + metadataMap := capability.GetMetadata().AsMap() + raw, ok := metadataMap["default_priority"] + if !ok { + nested, nestedOK := metadataMap["metadata"].(map[string]any) + if !nestedOK { + return nil, false + } + raw, ok = nested["default_priority"] + if !ok { + return nil, false + } + } + + rawMap, ok := raw.(map[string]any) + if !ok { + return nil, false + } + priorities := make(map[string]float64, len(rawMap)) + for key, value := range rawMap { + switch v := value.(type) { + case float64: + priorities[key] = v + case int: + priorities[key] = float64(v) + case int32: + priorities[key] = float64(v) + case int64: + priorities[key] = float64(v) + } + } + return priorities, len(priorities) > 0 +} diff --git a/internal/plugins/service_connection_test.go b/internal/plugins/service_connection_test.go index fc189bf6..192020ae 100644 --- a/internal/plugins/service_connection_test.go +++ b/internal/plugins/service_connection_test.go @@ -7,6 +7,7 @@ import ( "testing" pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + "google.golang.org/protobuf/types/known/structpb" ) type fakeServiceConfigStore struct { @@ -149,6 +150,35 @@ func TestServiceTestGlobalConfigUsesMergedDraftAndStopsTemporaryInstance(t *test } } +func TestRunPluginConnectionCheckSkipsMovieProbeForAudiobookOnlyProvider(t *testing.T) { + metadata, err := structpb.NewStruct(map[string]any{ + "default_priority": map[string]any{ + "audiobook": 2, + }, + }) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + manifest := connectionTestManifest(t, "silo.audiobook-metadata", "0.1.2") + manifest.Capabilities = []*pluginv1.CapabilityDescriptor{ + { + Type: "metadata_provider.v1", + Id: "audiobook-metadata", + DisplayName: "Audiobook Metadata", + Metadata: metadata, + }, + } + client := &fakePluginClient{manifest: manifest} + + if err := runPluginConnectionCheck(context.Background(), client, manifest); err != nil { + t.Fatalf("runPluginConnectionCheck() error = %v", err) + } + if client.metadataProviderCalls != 0 { + t.Fatalf("metadata provider calls = %d, want 0", client.metadataProviderCalls) + } +} + func TestServiceTestGlobalConfigReturnsUnsupportedWithoutStartingPlugin(t *testing.T) { manifest := connectionTestManifest(t, "silo.simple", "0.0.1") manifest.Capabilities = []*pluginv1.CapabilityDescriptor{ diff --git a/internal/plugins/service_hot_reload_test.go b/internal/plugins/service_hot_reload_test.go index 0e81803c..b043f24a 100644 --- a/internal/plugins/service_hot_reload_test.go +++ b/internal/plugins/service_hot_reload_test.go @@ -188,7 +188,8 @@ func (f *fakeServiceHost) Shutdown(context.Context) error { } type fakePluginClient struct { - manifest *pluginv1.PluginManifest + manifest *pluginv1.PluginManifest + metadataProviderCalls int } func (f *fakePluginClient) Manifest() *pluginv1.PluginManifest { @@ -196,6 +197,7 @@ func (f *fakePluginClient) Manifest() *pluginv1.PluginManifest { } func (f *fakePluginClient) MetadataProvider(string) (*pluginhost.MetadataProviderClient, error) { + f.metadataProviderCalls++ return nil, nil } @@ -215,6 +217,10 @@ func (f *fakePluginClient) ScanSource(string) (*pluginhost.ScanSourceClient, err return nil, nil } +func (f *fakePluginClient) RequestRouter(string) (*pluginhost.RequestRouterClient, error) { + return nil, nil +} + func (f *fakePluginClient) EventConsumer(string) (*pluginhost.EventConsumerClient, error) { return nil, nil } diff --git a/internal/plugins/service_singleflight_test.go b/internal/plugins/service_singleflight_test.go new file mode 100644 index 00000000..0f3e4a19 --- /dev/null +++ b/internal/plugins/service_singleflight_test.go @@ -0,0 +1,274 @@ +package plugins + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/pluginhost" +) + +// countingHerdHost simulates the real Host: Start records the launched client so +// a later Client() hits the warm cache; it counts Start calls and can delay each +// launch to force concurrent overlap. +type countingHerdHost struct { + mu sync.Mutex + current map[int]pluginClient + starts int + startResult pluginClient + startErr error + startDelay time.Duration +} + +func (h *countingHerdHost) Client(id int) (pluginClient, error) { + h.mu.Lock() + defer h.mu.Unlock() + if c, ok := h.current[id]; ok { + return c, nil + } + return nil, pluginhost.ErrClientNotFound +} + +func (h *countingHerdHost) Start(_ context.Context, req pluginhost.StartRequest) (pluginClient, error) { + if h.startDelay > 0 { + time.Sleep(h.startDelay) + } + h.mu.Lock() + defer h.mu.Unlock() + h.starts++ + if h.startErr != nil { + return nil, h.startErr + } + if h.current == nil { + h.current = map[int]pluginClient{} + } + h.current[req.InstallationID] = h.startResult + return h.startResult, nil +} + +func (h *countingHerdHost) Stop(id int) error { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.current, id) + return nil +} + +func (h *countingHerdHost) Shutdown(context.Context) error { return nil } + +func (h *countingHerdHost) startCount() int { + h.mu.Lock() + defer h.mu.Unlock() + return h.starts +} + +func TestEnsureClientSingleFlightsConcurrentLaunch(t *testing.T) { + manifest := testPluginManifest(t, "silo.metadb", "0.0.36") + installPath := writeInstalledPluginManifest(t, manifest) + store := newFakeServiceInstallationStore(&Installation{ + ID: 7, PluginID: manifest.GetPluginId(), Version: manifest.GetVersion(), + InstallPath: installPath, Enabled: true, + }) + host := &countingHerdHost{ + startResult: &fakePluginClient{manifest: manifest}, + startDelay: 25 * time.Millisecond, + } + service := &Service{installations: store, host: host} + + const n = 20 + var wg sync.WaitGroup + release := make(chan struct{}) + clients := make([]pluginClient, n) + errs := make([]error, n) + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + <-release + clients[i], errs[i] = service.ensureClient(context.Background(), 7) + }(i) + } + close(release) + wg.Wait() + + if got := host.startCount(); got != 1 { + t.Fatalf("host.Start calls = %d, want 1 (singleflighted)", got) + } + for i := 0; i < n; i++ { + if errs[i] != nil { + t.Fatalf("caller %d err = %v", i, errs[i]) + } + if clients[i] != host.startResult { + t.Fatalf("caller %d got %#v, want shared launched client", i, clients[i]) + } + } + + if _, err := service.ensureClient(context.Background(), 7); err != nil { + t.Fatalf("post-warm ensureClient err = %v", err) + } + if got := host.startCount(); got != 1 { + t.Fatalf("post-warm host.Start calls = %d, want still 1", got) + } +} + +func TestEnsureClientDistinctInstallationsLaunchIndependently(t *testing.T) { + mA := testPluginManifest(t, "silo.metadb", "0.0.36") + mB := testPluginManifest(t, "silo.other", "1.0.0") + pathA := writeInstalledPluginManifest(t, mA) + pathB := writeInstalledPluginManifest(t, mB) + store := newFakeServiceInstallationStore( + &Installation{ID: 7, PluginID: mA.GetPluginId(), Version: mA.GetVersion(), InstallPath: pathA, Enabled: true}, + &Installation{ID: 8, PluginID: mB.GetPluginId(), Version: mB.GetVersion(), InstallPath: pathB, Enabled: true}, + ) + host := &countingHerdHost{startResult: &fakePluginClient{manifest: mA}, startDelay: 10 * time.Millisecond} + service := &Service{installations: store, host: host} + + var wg sync.WaitGroup + for _, id := range []int{7, 8, 7, 8} { + wg.Add(1) + go func(id int) { defer wg.Done(); _, _ = service.ensureClient(context.Background(), id) }(id) + } + wg.Wait() + + if got := host.startCount(); got != 2 { + t.Fatalf("host.Start calls = %d, want 2 (one per installation)", got) + } +} + +// ctxCaptureHost blocks inside Start until released, and records whether the +// launch context was canceled. It lets a test cancel the singleflight leader's +// ctx while a launch is in flight. +type ctxCaptureHost struct { + mu sync.Mutex + current map[int]pluginClient + startResult pluginClient + entered chan struct{} + proceed chan struct{} + sawCancel bool +} + +func (h *ctxCaptureHost) Client(id int) (pluginClient, error) { + h.mu.Lock() + defer h.mu.Unlock() + if c, ok := h.current[id]; ok { + return c, nil + } + return nil, pluginhost.ErrClientNotFound +} + +func (h *ctxCaptureHost) Start(ctx context.Context, req pluginhost.StartRequest) (pluginClient, error) { + close(h.entered) + select { + case <-ctx.Done(): + h.mu.Lock() + h.sawCancel = true + h.mu.Unlock() + return nil, ctx.Err() + case <-h.proceed: + } + h.mu.Lock() + defer h.mu.Unlock() + if h.current == nil { + h.current = map[int]pluginClient{} + } + h.current[req.InstallationID] = h.startResult + return h.startResult, nil +} + +func (h *ctxCaptureHost) Stop(id int) error { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.current, id) + return nil +} + +func (h *ctxCaptureHost) Shutdown(context.Context) error { return nil } + +func (h *ctxCaptureHost) canceledDuringLaunch() bool { + h.mu.Lock() + defer h.mu.Unlock() + return h.sawCancel +} + +// TestEnsureClientLaunchIsolatedFromLeaderCancellation pins that a deduped +// launch is not coupled to the cancellation of whichever caller happened to be +// the singleflight leader: if that caller's request is canceled mid-launch, the +// shared launch (which other waiters depend on) must continue. +func TestEnsureClientLaunchIsolatedFromLeaderCancellation(t *testing.T) { + manifest := testPluginManifest(t, "silo.metadb", "0.0.36") + installPath := writeInstalledPluginManifest(t, manifest) + store := newFakeServiceInstallationStore(&Installation{ + ID: 7, PluginID: manifest.GetPluginId(), Version: manifest.GetVersion(), + InstallPath: installPath, Enabled: true, + }) + host := &ctxCaptureHost{ + startResult: &fakePluginClient{manifest: manifest}, + entered: make(chan struct{}), + proceed: make(chan struct{}), + } + service := &Service{installations: store, host: host} + + ctx, cancel := context.WithCancel(context.Background()) + type result struct { + c pluginClient + err error + } + done := make(chan result, 1) + go func() { + c, err := service.ensureClient(ctx, 7) + done <- result{c, err} + }() + + <-host.entered // launch is in flight inside Start + cancel() // cancel the leader's ctx mid-launch + + select { + case r := <-done: + t.Fatalf("ensureClient returned early after leader cancel (err=%v); launch was coupled to leader ctx", r.err) + case <-time.After(100 * time.Millisecond): + // still launching — isolated from the leader's cancellation, as required + } + + close(host.proceed) + r := <-done + if r.err != nil { + t.Fatalf("ensureClient err = %v, want nil", r.err) + } + if r.c != host.startResult { + t.Fatalf("got %#v, want launched client", r.c) + } + if host.canceledDuringLaunch() { + t.Fatalf("launch ctx was canceled by the leader; expected isolation") + } +} + +func TestEnsureClientFailedLaunchPropagatesToAllCallers(t *testing.T) { + manifest := testPluginManifest(t, "silo.metadb", "0.0.36") + installPath := writeInstalledPluginManifest(t, manifest) + store := newFakeServiceInstallationStore(&Installation{ + ID: 7, PluginID: manifest.GetPluginId(), Version: manifest.GetVersion(), + InstallPath: installPath, Enabled: true, + }) + host := &countingHerdHost{startErr: context.DeadlineExceeded, startDelay: 15 * time.Millisecond} + service := &Service{installations: store, host: host} + + const n = 10 + var wg sync.WaitGroup + release := make(chan struct{}) + errs := make([]error, n) + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { defer wg.Done(); <-release; _, errs[i] = service.ensureClient(context.Background(), 7) }(i) + } + close(release) + wg.Wait() + + for i := 0; i < n; i++ { + if errs[i] == nil { + t.Fatalf("caller %d err = nil, want the launch error", i) + } + } + if got := host.startCount(); got != 1 { + t.Fatalf("failed launch host.Start calls = %d, want 1 (still single-flighted)", got) + } +} diff --git a/internal/plugins/user_identity_lookup.go b/internal/plugins/user_identity_lookup.go index 535bd6d1..7a057427 100644 --- a/internal/plugins/user_identity_lookup.go +++ b/internal/plugins/user_identity_lookup.go @@ -14,6 +14,7 @@ import ( // UIs without reaching back into the browser's localStorage. type UserIdentity struct { Username string + Email string ProfileName string ProfileIsPrimary bool } @@ -44,8 +45,8 @@ func (l *PgUserIdentityLookup) LookupIdentity(ctx context.Context, userID int, p } err := l.pool.QueryRow(ctx, - "SELECT username FROM users WHERE id = $1", userID, - ).Scan(&out.Username) + "SELECT username, COALESCE(email, '') FROM users WHERE id = $1", userID, + ).Scan(&out.Username, &out.Email) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return out, err } @@ -65,3 +66,22 @@ func (l *PgUserIdentityLookup) LookupIdentity(ctx context.Context, userID int, p } return out, nil } + +// RequesterIdentityFromLookup adapts a UserIdentityLookup into the requests +// service's requester-identity resolver (email + username by user id). +type requesterIdentityAdapter struct{ lookup UserIdentityLookup } + +func RequesterIdentityFromLookup(l UserIdentityLookup) *requesterIdentityAdapter { + return &requesterIdentityAdapter{lookup: l} +} + +func (a *requesterIdentityAdapter) ResolveRequester(ctx context.Context, userID int) (string, string, error) { + if a == nil || a.lookup == nil || userID <= 0 { + return "", "", nil + } + id, err := a.lookup.LookupIdentity(ctx, userID, "") + if err != nil { + return "", "", err + } + return id.Email, id.Username, nil +} diff --git a/internal/ratelimit/middleware.go b/internal/ratelimit/middleware.go index 5d52c47b..63d844b4 100644 --- a/internal/ratelimit/middleware.go +++ b/internal/ratelimit/middleware.go @@ -35,6 +35,16 @@ func NewMiddleware(perKey RateLimiter, global RateLimiter, store SettingsStore, } } +// ActiveBackend reports which limiter backend this process is actually +// running, as opposed to the configured backend, which only takes effect +// after a restart. +func (mw *Middleware) ActiveBackend() string { + if mw.isMemory { + return "memory" + } + return "redis" +} + // Init loads config and seeds defaults. Call once at startup. func (mw *Middleware) Init(ctx context.Context) error { if err := SeedDefaults(ctx, mw.store); err != nil { diff --git a/internal/recommendations/accuracy_test.go b/internal/recommendations/accuracy_test.go index 4119f08c..20a30f18 100644 --- a/internal/recommendations/accuracy_test.go +++ b/internal/recommendations/accuracy_test.go @@ -90,6 +90,37 @@ func TestApplyMediaTypeFloorAddsAvailableSupplementalType(t *testing.T) { } } +func TestApplyMediaTypeFloorAddsAvailableAudiobooks(t *testing.T) { + items := []ScoredItem{ + {MediaItemID: "m1", Score: 1.00}, + {MediaItemID: "m2", Score: 0.99}, + {MediaItemID: "m3", Score: 0.98}, + {MediaItemID: "m4", Score: 0.97}, + {MediaItemID: "m5", Score: 0.96}, + {MediaItemID: "m6", Score: 0.95}, + {MediaItemID: "m7", Score: 0.94}, + {MediaItemID: "m8", Score: 0.93}, + {MediaItemID: "m9", Score: 0.92}, + {MediaItemID: "m10", Score: 0.91}, + } + candidates := append([]ScoredItem(nil), items...) + candidates = append(candidates, + ScoredItem{MediaItemID: "a1", Score: 0.90}, + ScoredItem{MediaItemID: "a2", Score: 0.89}, + ) + mediaTypes := map[string]string{ + "m1": "movie", "m2": "movie", "m3": "movie", "m4": "movie", "m5": "movie", + "m6": "movie", "m7": "movie", "m8": "movie", "m9": "movie", "m10": "movie", + "a1": "audiobook", "a2": "audiobook", + } + + mixed := applyMediaTypeFloor(items, candidates, mediaTypes) + + if got := countMediaType(mixed, mediaTypes, "audiobook"); got != 2 { + t.Fatalf("expected 2 audiobooks from supplemental candidates, got %d in %#v", got, mixed) + } +} + func TestApplyMediaTypeFloorNoopsWithoutSupplementalType(t *testing.T) { items := []ScoredItem{ {MediaItemID: "s1", Score: 1.00}, @@ -111,6 +142,32 @@ func TestApplyMediaTypeFloorNoopsWithoutSupplementalType(t *testing.T) { } } +func TestApplyMediaTypeFloorIncludesEbookSupplement(t *testing.T) { + items := []ScoredItem{ + {MediaItemID: "s1", Score: 1.00}, + {MediaItemID: "s2", Score: 0.99}, + {MediaItemID: "s3", Score: 0.98}, + {MediaItemID: "s4", Score: 0.97}, + {MediaItemID: "s5", Score: 0.96}, + } + candidates := append([]ScoredItem(nil), items...) + candidates = append(candidates, ScoredItem{MediaItemID: "e1", Score: 0.95}) + mediaTypes := map[string]string{ + "s1": "series", + "s2": "series", + "s3": "series", + "s4": "series", + "s5": "series", + "e1": "ebook", + } + + mixed := applyMediaTypeFloor(items, candidates, mediaTypes) + + if got := countMediaType(mixed, mediaTypes, "ebook"); got != 1 { + t.Fatalf("expected 1 ebook from supplemental candidates, got %d in %#v", got, mixed) + } +} + func TestApplyGenreCapDoesNotCollapseConcentratedRows(t *testing.T) { items := []ScoredItem{ {MediaItemID: "a", Score: 1.0}, diff --git a/internal/recommendations/embeddings/text.go b/internal/recommendations/embeddings/text.go index bd15e4a5..faa884dc 100644 --- a/internal/recommendations/embeddings/text.go +++ b/internal/recommendations/embeddings/text.go @@ -63,17 +63,20 @@ func BuildEmbeddingText(item *models.MediaItem) string { parts = append(parts, fmt.Sprintf(`"%s"`, item.Tagline)) } - if item.Type == "audiobook" { - // Audiobooks credit author (kind=7) and narrator (kind=8). Cast/ - // director/writer don't apply. Keep the SQL canonical_text builder - // in recommendations/repo.go in sync with this shape. + if item.Type == "audiobook" || item.Type == "ebook" { + // Books credit authors (kind=7). Audiobooks also credit narrators + // (kind=8); ebooks do not. Cast/director/writer don't apply. Keep + // the SQL canonical_text builder in recommendations/repo.go in sync + // with this shape. var authors, narrators []models.ItemPerson for _, p := range item.People { switch p.Kind { case models.PersonKindAuthor: authors = append(authors, p) case models.PersonKindNarrator: - narrators = append(narrators, p) + if item.Type == "audiobook" { + narrators = append(narrators, p) + } } } sortItemPeople(authors) @@ -174,6 +177,8 @@ func mediaTypeLabel(t string) string { return "TV series" case "audiobook": return "audiobook" + case "ebook": + return "ebook" default: return "movie" } diff --git a/internal/recommendations/embeddings/text_test.go b/internal/recommendations/embeddings/text_test.go index b23b303b..3a90e034 100644 --- a/internal/recommendations/embeddings/text_test.go +++ b/internal/recommendations/embeddings/text_test.go @@ -61,6 +61,42 @@ func TestBuildEmbeddingTextAudiobookUsesAuthorAndNarrator(t *testing.T) { } } +func TestBuildEmbeddingTextEbookUsesAuthorWithoutNarratorOrCast(t *testing.T) { + item := &models.MediaItem{ + Title: "A Wizard of Earthsea", + Type: "ebook", + Year: 1968, + Genres: []string{"Fantasy"}, + Overview: "Ged learns the cost of power.", + People: []models.ItemPerson{ + {Person: models.Person{ID: 1, Name: "Ursula K. Le Guin"}, Kind: models.PersonKindAuthor}, + {Person: models.Person{ID: 2, Name: "Narrator Should Not Apply"}, Kind: models.PersonKindNarrator}, + {Person: models.Person{ID: 3, Name: "Actor Should Not Apply"}, Kind: models.PersonKindActor, Character: "Ged"}, + {Person: models.Person{ID: 4, Name: "Writer Should Not Apply"}, Kind: models.PersonKindWriter}, + }, + } + + text := BuildEmbeddingText(item) + + if !strings.Contains(text, "Fantasy ebook about Ged learns the cost of power.") { + t.Fatalf("ebook lede missing or wrong: %q", text) + } + if !strings.Contains(text, "Written by Ursula K. Le Guin") { + t.Fatalf("ebook author missing: %q", text) + } + for _, unwanted := range []string{ + "Narrated by", + "Narrator Should Not Apply", + "Cast:", + "Actor Should Not Apply", + "Writer Should Not Apply", + } { + if strings.Contains(text, unwanted) { + t.Fatalf("ebook text should not contain %q: %q", unwanted, text) + } + } +} + func TestBuildEmbeddingTextSortsActorsBeforeTopFive(t *testing.T) { item := &models.MediaItem{ Title: "Example", diff --git a/internal/recommendations/personal.go b/internal/recommendations/personal.go index c0b15be0..d51ac658 100644 --- a/internal/recommendations/personal.go +++ b/internal/recommendations/personal.go @@ -11,7 +11,7 @@ import ( const aggregateMediaTypeFloorDivisor = 5 -var aggregateSupplementMediaTypes = []string{"movie", "series"} +var aggregateSupplementMediaTypes = []string{"movie", "series", "audiobook", "ebook"} // ForYou returns personalised recommendations grouped by taste clusters. // For cold-start users, non-personalized rows are returned. diff --git a/internal/recommendations/repo.go b/internal/recommendations/repo.go index 47d7920a..21c05c1a 100644 --- a/internal/recommendations/repo.go +++ b/internal/recommendations/repo.go @@ -39,21 +39,63 @@ func ensureCanonicalDimensions(vec []float32) ([]float32, error) { const embeddingLockSettingKey = "recommendations.embedding_lock" const minHNSWEfSearch = 200 -const tasteSeedCandidateQuery = ` - WITH watched_counts AS ( - SELECT COALESCE(e.series_id, wp.media_item_id) AS item_id, - COUNT(DISTINCT wp.user_id::text || ':' || COALESCE(wp.profile_id, '')) AS watch_count - FROM user_watch_progress wp - LEFT JOIN episodes e ON e.content_id = wp.media_item_id - WHERE (wp.completed = true OR (wp.duration_seconds > 0 AND wp.position_seconds / wp.duration_seconds >= 0.5)) - AND wp.updated_at > NOW() - INTERVAL '180 days' - GROUP BY 1 +// watchedActivityCTE unifies video watch progress and ebook reader progress +// into one activity stream. Episodes roll up to their parent series via +// COALESCE(e.series_id, ...). The ebook completed flag is derived from the +// shared finished-progress threshold. +var watchedActivityCTE = fmt.Sprintf(` +watched_activity AS ( + SELECT COALESCE(e.series_id, wp.media_item_id) AS item_id, + wp.media_item_id AS leaf_item_id, + wp.user_id, + wp.profile_id, + wp.user_id::text || ':' || COALESCE(wp.profile_id, '') AS watcher_id, + wp.updated_at, + wp.completed AS completed + FROM user_watch_progress wp + LEFT JOIN episodes e ON e.content_id = wp.media_item_id + WHERE (wp.completed = true OR (wp.duration_seconds > 0 AND wp.position_seconds / wp.duration_seconds >= 0.5)) + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = wp.user_id + AND hhi.profile_id = wp.profile_id + AND hhi.media_item_id = wp.media_item_id + AND wp.updated_at <= hhi.hidden_before + ) + UNION ALL + SELECT erp.content_id AS item_id, + erp.content_id AS leaf_item_id, + erp.user_id, + erp.profile_id, + erp.user_id::text || ':' || COALESCE(erp.profile_id, '') AS watcher_id, + erp.updated_at, + erp.progress >= %s AS completed + FROM ebook_reader_progress erp + WHERE erp.progress >= 0.5 + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = erp.user_id + AND hhi.profile_id = erp.profile_id + AND hhi.media_item_id = erp.content_id + AND erp.updated_at <= hhi.hidden_before + ) +)`, catalog.EbookFinishedProgressThresholdSQL) + +var tasteSeedCandidateQuery = fmt.Sprintf(` + WITH %s, + watched_counts AS ( + SELECT item_id, COUNT(DISTINCT watcher_id) AS watch_count + FROM watched_activity + WHERE updated_at > NOW() - INTERVAL '180 days' + GROUP BY item_id ) SELECT mi.content_id FROM media_items mi LEFT JOIN watched_counts wc ON wc.item_id = mi.content_id - WHERE mi.status = 'matched' - AND mi.type IN ('movie', 'series') + WHERE %s + AND mi.type IN ('movie', 'series', 'audiobook', 'ebook') AND mi.poster_path IS NOT NULL AND mi.poster_path <> '' ORDER BY COALESCE(wc.watch_count, 0) DESC, @@ -66,7 +108,7 @@ const tasteSeedCandidateQuery = ` CASE WHEN mi.rating_tmdb < 9.5 THEN mi.rating_tmdb END DESC NULLS LAST, mi.year DESC NULLS LAST, mi.content_id ASC - LIMIT $1 OFFSET $2` + LIMIT $1 OFFSET $2`, watchedActivityCTE, recommendationItemEligibilityWhereClause("mi")) // Repo provides database operations for the recommendation system. type Repo struct { @@ -120,6 +162,18 @@ type EmbeddingTextCandidate struct { CanonicalText string } +func embeddingEligibilityWhereClause() string { + return recommendationItemEligibilityWhereClause("mi") +} + +func recommendationItemEligibilityWhereClause(alias string) string { + alias = strings.TrimSpace(alias) + if alias == "" { + alias = "media_items" + } + return fmt.Sprintf("(%s.status = 'matched' OR %s.type = 'audiobook' OR %s.type = 'ebook')", alias, alias, alias) +} + // UpsertEmbedding stores or updates an embedding for a media item. func (r *Repo) UpsertEmbedding(ctx context.Context, itemID string, embedding []float32, model, canonicalText string) error { padded, err := ensureCanonicalDimensions(embedding) @@ -279,7 +333,7 @@ func (r *Repo) findTasteProfileCandidates( } conditions := []string{ - "mi.status = 'matched'", + "(mi.status = 'matched' OR mi.type = 'audiobook')", "e.media_item_id != ALL($2)", } args := []any{pgvector.NewVector(embedding), excludeIDs} @@ -416,18 +470,18 @@ func (r *Repo) findTasteProfileCandidates( // ItemsNeedingEmbedding returns content IDs of media items that either have no // embedding or whose embedding was generated with a different model. -// Audiobooks bypass the status='matched' gate because they don't go through -// the external-provider match pipeline; their tag-derived metadata is -// authoritative as soon as the scan completes. +// Books bypass the status='matched' gate because their scanner/plugin-derived +// metadata is authoritative as soon as the scan/enrichment completes. func (r *Repo) ItemsNeedingEmbedding(ctx context.Context, currentModel string, limit int) ([]string, error) { - rows, err := r.pool.Query(ctx, ` + query := fmt.Sprintf(` SELECT mi.content_id FROM media_items mi LEFT JOIN media_item_embeddings e ON e.media_item_id = mi.content_id - WHERE (mi.status = 'matched' OR mi.type = 'audiobook') + WHERE %s AND (e.media_item_id IS NULL OR e.model != $1) LIMIT $2 - `, currentModel, limit) + `, embeddingEligibilityWhereClause()) + rows, err := r.pool.Query(ctx, query, currentModel, limit) if err != nil { return nil, fmt.Errorf("items needing embedding: %w", err) } @@ -448,11 +502,11 @@ func (r *Repo) ItemsNeedingEmbedding(ctx context.Context, currentModel string, l } func (r *Repo) ListEmbeddingTextCandidates(ctx context.Context, afterID, currentModel string, limit int) ([]EmbeddingTextCandidate, error) { - rows, err := r.pool.Query(ctx, ` + query := fmt.Sprintf(` -- Keep current_text in sync with embeddings.BuildEmbeddingText. This lets -- the embedding job page over only missing, model-stale, or text-stale rows. - -- Audiobook lines map to embeddings.mediaTypeLabel + the author/narrator - -- branch in BuildEmbeddingText; any divergence here forces every audiobook + -- Book lines map to embeddings.mediaTypeLabel + the author/narrator + -- branch in BuildEmbeddingText; any divergence here forces book items -- to re-embed every job run. WITH text_candidates AS ( SELECT mi.content_id, @@ -461,11 +515,11 @@ func (r *Repo) ListEmbeddingTextCandidates(ctx context.Context, afterID, current array_to_string(array_remove(ARRAY[ CASE WHEN cardinality(COALESCE(mi.genres, ARRAY[]::text[])) > 0 AND COALESCE(mi.overview, '') <> '' - THEN array_to_string(mi.genres, ', ') || ' ' || CASE WHEN mi.type = 'series' THEN 'TV series' WHEN mi.type = 'audiobook' THEN 'audiobook' ELSE 'movie' END || ' about ' || substr(mi.overview, 1, 1000) + THEN array_to_string(mi.genres, ', ') || ' ' || CASE WHEN mi.type = 'series' THEN 'TV series' WHEN mi.type = 'audiobook' THEN 'audiobook' WHEN mi.type = 'ebook' THEN 'ebook' ELSE 'movie' END || ' about ' || substr(mi.overview, 1, 1000) WHEN cardinality(COALESCE(mi.genres, ARRAY[]::text[])) > 0 - THEN array_to_string(mi.genres, ', ') || ' ' || CASE WHEN mi.type = 'series' THEN 'TV series' WHEN mi.type = 'audiobook' THEN 'audiobook' ELSE 'movie' END + THEN array_to_string(mi.genres, ', ') || ' ' || CASE WHEN mi.type = 'series' THEN 'TV series' WHEN mi.type = 'audiobook' THEN 'audiobook' WHEN mi.type = 'ebook' THEN 'ebook' ELSE 'movie' END WHEN COALESCE(mi.overview, '') <> '' - THEN CASE WHEN mi.type = 'series' THEN 'TV series' WHEN mi.type = 'audiobook' THEN 'audiobook' ELSE 'movie' END || '. ' || substr(mi.overview, 1, 1000) + THEN CASE WHEN mi.type = 'series' THEN 'TV series' WHEN mi.type = 'audiobook' THEN 'audiobook' WHEN mi.type = 'ebook' THEN 'ebook' ELSE 'movie' END || '. ' || substr(mi.overview, 1, 1000) ELSE NULL END, CASE WHEN COALESCE(mi.year, 0) > 0 @@ -474,11 +528,11 @@ func (r *Repo) ListEmbeddingTextCandidates(ctx context.Context, afterID, current END, CASE WHEN COALESCE(mi.content_rating, '') <> '' THEN 'Rated ' || mi.content_rating ELSE NULL END, CASE WHEN COALESCE(mi.tagline, '') <> '' THEN '"' || mi.tagline || '"' ELSE NULL END, - CASE WHEN mi.type <> 'audiobook' AND actors.names <> '' THEN 'Cast: ' || actors.names ELSE NULL END, - CASE WHEN mi.type <> 'audiobook' AND directors.names <> '' THEN 'Directed by ' || directors.names ELSE NULL END, + CASE WHEN mi.type NOT IN ('audiobook', 'ebook') AND actors.names <> '' THEN 'Cast: ' || actors.names ELSE NULL END, + CASE WHEN mi.type NOT IN ('audiobook', 'ebook') AND directors.names <> '' THEN 'Directed by ' || directors.names ELSE NULL END, CASE - WHEN mi.type = 'audiobook' AND authors.names <> '' THEN 'Written by ' || authors.names - WHEN mi.type <> 'audiobook' AND writers.names <> '' THEN 'Written by ' || writers.names + WHEN mi.type IN ('audiobook', 'ebook') AND authors.names <> '' THEN 'Written by ' || authors.names + WHEN mi.type NOT IN ('audiobook', 'ebook') AND writers.names <> '' THEN 'Written by ' || writers.names ELSE NULL END, CASE WHEN mi.type = 'audiobook' AND narrators.names <> '' THEN 'Narrated by ' || narrators.names ELSE NULL END, @@ -536,7 +590,7 @@ func (r *Repo) ListEmbeddingTextCandidates(ctx context.Context, afterID, current WHERE ip.content_id = mi.content_id AND ip.kind = 8 ) narrators ON TRUE - WHERE (mi.status = 'matched' OR mi.type = 'audiobook') + WHERE %s AND ($1 = '' OR mi.content_id > $1) ) SELECT mi.content_id, @@ -548,7 +602,8 @@ func (r *Repo) ListEmbeddingTextCandidates(ctx context.Context, afterID, current OR mi.canonical_text IS DISTINCT FROM mi.current_text ORDER BY mi.content_id LIMIT $3 - `, afterID, currentModel, limit) + `, embeddingEligibilityWhereClause()) + rows, err := r.pool.Query(ctx, query, afterID, currentModel, limit) if err != nil { return nil, fmt.Errorf("embedding text candidates: %w", err) } @@ -583,7 +638,8 @@ func (r *Repo) EmbeddingCount(ctx context.Context) (int, error) { // (used by the admin worker UI) lines up with what the job actually does. func (r *Repo) TotalMediaItemCount(ctx context.Context) (int, error) { var count int - err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM media_items WHERE status = 'matched' OR type = 'audiobook'`).Scan(&count) + query := fmt.Sprintf(`SELECT COUNT(*) FROM media_items mi WHERE %s`, embeddingEligibilityWhereClause()) + err := r.pool.QueryRow(ctx, query).Scan(&count) if err != nil { return 0, fmt.Errorf("total media item count: %w", err) } @@ -1137,6 +1193,46 @@ func (r *Repo) GetWatchProgressForUser(ctx context.Context, userID int, profileI return result, rows.Err() } +// ebookReaderProgressForUserQuery maps ebook reader progress onto the +// WatchProgressRow shape (progress as position with duration 1). Rows hidden +// via user_history_hidden_items are excluded with the same semantics as +// GetWatchProgressForUser: hidden when updated_at <= hidden_before, visible +// again once new reading activity lands after hidden_before. +var ebookReaderProgressForUserQuery = fmt.Sprintf(` + SELECT content_id, + progress::double precision AS position_seconds, + 1::double precision AS duration_seconds, + progress >= %s AS completed, + updated_at + FROM ebook_reader_progress + WHERE user_id = $1 AND profile_id = $2 + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = ebook_reader_progress.user_id + AND hhi.profile_id = ebook_reader_progress.profile_id + AND hhi.media_item_id = ebook_reader_progress.content_id + AND ebook_reader_progress.updated_at <= hhi.hidden_before + )`, catalog.EbookFinishedProgressThresholdSQL) + +func (r *Repo) GetEbookReaderProgressForUser(ctx context.Context, userID int, profileID string) ([]WatchProgressRow, error) { + rows, err := r.pool.Query(ctx, ebookReaderProgressForUserQuery, userID, profileID) + if err != nil { + return nil, fmt.Errorf("get ebook reader progress: %w", err) + } + defer rows.Close() + + var result []WatchProgressRow + for rows.Next() { + var wp WatchProgressRow + if err := rows.Scan(&wp.MediaItemID, &wp.PositionSeconds, &wp.DurationSeconds, &wp.Completed, &wp.UpdatedAt); err != nil { + return nil, fmt.Errorf("scan ebook reader progress: %w", err) + } + result = append(result, wp) + } + return result, rows.Err() +} + // RewatchCount holds the number of completed watches for an item. type RewatchCount struct { MediaItemID string @@ -1177,33 +1273,38 @@ func (r *Repo) GetRewatchCounts(ctx context.Context, userID int, profileID strin return result, rows.Err() } +// itemWatchersQuery feeds co-watch matrix computation. watched_activity rolls +// episodes up to their parent series, so a single watcher can produce many +// rows for the same item; the GROUP BY collapses them to one row per +// (watcher, item) before ranking and aggregation so a lone binge-watcher +// cannot satisfy the minimum-watchers threshold ($2) by itself and the +// per-user recency cap ($1) counts distinct items rather than raw rows. +// +// Watcher identity is (user_id, profile_id) for the Jaccard math — profiles of +// one account legitimately have distinct tastes — but the minimum-watchers +// floor ($2) is a sparsity/privacy threshold and must count distinct login +// ACCOUNTS: one household account with N profiles must not satisfy it alone, +// so the HAVING clause counts DISTINCT user_id rather than watcher rows. +var itemWatchersQuery = fmt.Sprintf(` + WITH %s, + user_watches AS ( + SELECT user_id, + watcher_id, + item_id AS media_item_id, + ROW_NUMBER() OVER (PARTITION BY user_id, profile_id ORDER BY MAX(updated_at) DESC) AS rn + FROM watched_activity + GROUP BY user_id, profile_id, watcher_id, item_id + ) + SELECT media_item_id, ARRAY_AGG(watcher_id) AS watchers + FROM user_watches + WHERE rn <= $1 + GROUP BY media_item_id + HAVING COUNT(DISTINCT user_id) >= $2`, watchedActivityCTE) + // ItemWatchers maps item_id to profile identities that watched >= 50% progress. // Used for co-watch matrix computation. func (r *Repo) GetItemWatchers(ctx context.Context, minWatchers int, maxPerUser int) (map[string][]string, error) { - // Get all watch progress entries where progress >= 50% or completed - rows, err := r.pool.Query(ctx, ` - WITH user_watches AS ( - SELECT user_id::text || ':' || COALESCE(profile_id, '') AS watcher_id, - media_item_id, - ROW_NUMBER() OVER (PARTITION BY user_id, profile_id ORDER BY updated_at DESC) AS rn - FROM user_watch_progress - WHERE (completed = true - OR (duration_seconds > 0 AND position_seconds / duration_seconds >= 0.5)) - AND NOT EXISTS ( - SELECT 1 - FROM user_history_hidden_items hhi - WHERE hhi.user_id = user_watch_progress.user_id - AND hhi.profile_id = user_watch_progress.profile_id - AND hhi.media_item_id = user_watch_progress.media_item_id - AND user_watch_progress.updated_at <= hhi.hidden_before - ) - ) - SELECT media_item_id, ARRAY_AGG(watcher_id) AS watchers - FROM user_watches - WHERE rn <= $1 - GROUP BY media_item_id - HAVING COUNT(*) >= $2`, - maxPerUser, minWatchers) + rows, err := r.pool.Query(ctx, itemWatchersQuery, maxPerUser, minWatchers) if err != nil { return nil, fmt.Errorf("get item watchers: %w", err) } @@ -1223,33 +1324,23 @@ func (r *Repo) GetItemWatchers(ctx context.Context, minWatchers int, maxPerUser // --- Cold Start Queries --- -// GetPopularItems returns the most-watched series/movies over the given number of days. +// GetPopularItems returns the most-played media items over the given number of days. // Episodes are resolved to their parent series. func (r *Repo) GetPopularItems(ctx context.Context, days, limit int) ([]ScoredItem, error) { - rows, err := r.pool.Query(ctx, ` - WITH watched_items AS ( - SELECT COALESCE(e.series_id, wp.media_item_id) AS item_id, - wp.user_id::text || ':' || COALESCE(wp.profile_id, '') AS watcher_id - FROM user_watch_progress wp - LEFT JOIN episodes e ON e.content_id = wp.media_item_id - WHERE (wp.completed = true OR (wp.duration_seconds > 0 AND wp.position_seconds / wp.duration_seconds >= 0.5)) - AND wp.updated_at > NOW() - ($1 || ' days')::interval - AND NOT EXISTS ( - SELECT 1 - FROM user_history_hidden_items hhi - WHERE hhi.user_id = wp.user_id - AND hhi.profile_id = wp.profile_id - AND hhi.media_item_id = wp.media_item_id - AND wp.updated_at <= hhi.hidden_before - ) + query := fmt.Sprintf(` + WITH %s, + watched_items AS ( + SELECT item_id, watcher_id + FROM watched_activity + WHERE updated_at > NOW() - ($1 || ' days')::interval ) SELECT wi.item_id, COUNT(DISTINCT wi.watcher_id) AS watch_count FROM watched_items wi JOIN media_items mi ON mi.content_id = wi.item_id GROUP BY wi.item_id ORDER BY watch_count DESC - LIMIT $2`, - fmt.Sprintf("%d", days), limit) + LIMIT $2`, watchedActivityCTE) + rows, err := r.pool.Query(ctx, query, fmt.Sprintf("%d", days), limit) if err != nil { return nil, fmt.Errorf("get popular items: %w", err) } @@ -1270,15 +1361,18 @@ func (r *Repo) GetPopularItems(ctx context.Context, days, limit int) ([]ScoredIt } // GetRecentlyAddedItems returns items added within the given number of days. +// Audiobooks and ebooks bypass the matched-status gate because their +// scan-derived metadata is authoritative before any external-provider match +// exists. func (r *Repo) GetRecentlyAddedItems(ctx context.Context, days, limit int) ([]ScoredItem, error) { - rows, err := r.pool.Query(ctx, ` - SELECT content_id, created_at - FROM media_items - WHERE status = 'matched' - AND created_at > NOW() - ($1 || ' days')::interval - ORDER BY created_at DESC - LIMIT $2`, - fmt.Sprintf("%d", days), limit) + query := fmt.Sprintf(` + SELECT mi.content_id, mi.created_at + FROM media_items mi + WHERE %s + AND mi.created_at > NOW() - ($1 || ' days')::interval + ORDER BY mi.created_at DESC + LIMIT $2`, recommendationItemEligibilityWhereClause("mi")) + rows, err := r.pool.Query(ctx, query, fmt.Sprintf("%d", days), limit) if err != nil { return nil, fmt.Errorf("get recently added: %w", err) } @@ -1325,7 +1419,7 @@ func (r *Repo) GetTopRatedItems(ctx context.Context, minRatings, limit int) ([]S return items, rows.Err() } -// GetTasteSeedCandidates returns movie/series content IDs ordered for the +// GetTasteSeedCandidates returns movie/series/audiobook content IDs ordered for the // taste-seeding picker: server engagement first (most-watched in the last // 180 days), then rating reliability and rating score, then recency. This keeps // fresh servers from front-loading single-vote TMDB 10.0 obscurities while @@ -1352,21 +1446,11 @@ func (r *Repo) GetTasteSeedCandidates(ctx context.Context, limit, offset int) ([ // GetTopGenres returns the most popular genres by watch count. // Episodes are resolved to their parent series for genre lookup. func (r *Repo) GetTopGenres(ctx context.Context, limit int) ([]string, error) { - rows, err := r.pool.Query(ctx, ` - WITH watched_items AS ( - SELECT COALESCE(e.series_id, wp.media_item_id) AS item_id, - wp.user_id::text || ':' || COALESCE(wp.profile_id, '') AS watcher_id - FROM user_watch_progress wp - LEFT JOIN episodes e ON e.content_id = wp.media_item_id - WHERE (wp.completed = true OR (wp.duration_seconds > 0 AND wp.position_seconds / wp.duration_seconds >= 0.5)) - AND NOT EXISTS ( - SELECT 1 - FROM user_history_hidden_items hhi - WHERE hhi.user_id = wp.user_id - AND hhi.profile_id = wp.profile_id - AND hhi.media_item_id = wp.media_item_id - AND wp.updated_at <= hhi.hidden_before - ) + query := fmt.Sprintf(` + WITH %s, + watched_items AS ( + SELECT item_id, watcher_id + FROM watched_activity ) SELECT g.genre, COUNT(DISTINCT wi.watcher_id) AS watchers FROM watched_items wi @@ -1374,8 +1458,8 @@ func (r *Repo) GetTopGenres(ctx context.Context, limit int) ([]string, error) { CROSS JOIN LATERAL UNNEST(mi.genres) AS g(genre) GROUP BY g.genre ORDER BY watchers DESC - LIMIT $1`, - limit) + LIMIT $1`, watchedActivityCTE) + rows, err := r.pool.Query(ctx, query, limit) if err != nil { return nil, fmt.Errorf("get top genres: %w", err) } @@ -1393,23 +1477,13 @@ func (r *Repo) GetTopGenres(ctx context.Context, limit int) ([]string, error) { return genres, rows.Err() } -// GetGenreSamplerItems returns the most-watched series/movies in a specific genre. +// GetGenreSamplerItems returns the most-played media items in a specific genre. func (r *Repo) GetGenreSamplerItems(ctx context.Context, genre string, limit int) ([]ScoredItem, error) { - rows, err := r.pool.Query(ctx, ` - WITH watched_items AS ( - SELECT COALESCE(e.series_id, wp.media_item_id) AS item_id, - wp.user_id::text || ':' || COALESCE(wp.profile_id, '') AS watcher_id - FROM user_watch_progress wp - LEFT JOIN episodes e ON e.content_id = wp.media_item_id - WHERE (wp.completed = true OR (wp.duration_seconds > 0 AND wp.position_seconds / wp.duration_seconds >= 0.5)) - AND NOT EXISTS ( - SELECT 1 - FROM user_history_hidden_items hhi - WHERE hhi.user_id = wp.user_id - AND hhi.profile_id = wp.profile_id - AND hhi.media_item_id = wp.media_item_id - AND wp.updated_at <= hhi.hidden_before - ) + query := fmt.Sprintf(` + WITH %s, + watched_items AS ( + SELECT item_id, watcher_id + FROM watched_activity ) SELECT wi.item_id, COUNT(DISTINCT wi.watcher_id) AS watch_count FROM watched_items wi @@ -1417,8 +1491,8 @@ func (r *Repo) GetGenreSamplerItems(ctx context.Context, genre string, limit int WHERE $1 = ANY(mi.genres) GROUP BY wi.item_id ORDER BY watch_count DESC - LIMIT $2`, - genre, limit) + LIMIT $2`, watchedActivityCTE) + rows, err := r.pool.Query(ctx, query, genre, limit) if err != nil { return nil, fmt.Errorf("get genre sampler items: %w", err) } @@ -1614,24 +1688,15 @@ func (r *Repo) GetAllUsersWithTasteProfiles(ctx context.Context) ([]StaleProfile return profiles, rows.Err() } -// GetWatchedItemIDs returns content IDs of series/movies the user has watched +// GetWatchedItemIDs returns content IDs of media items the user has watched // (>= 50% progress or completed). Episodes are resolved to their parent series. func (r *Repo) GetWatchedItemIDs(ctx context.Context, userID int, profileID string) ([]string, error) { - rows, err := r.pool.Query(ctx, ` - SELECT DISTINCT COALESCE(e.series_id, wp.media_item_id) AS item_id - FROM user_watch_progress wp - LEFT JOIN episodes e ON e.content_id = wp.media_item_id - WHERE wp.user_id = $1 AND wp.profile_id = $2 - AND (wp.completed = true OR (wp.duration_seconds > 0 AND wp.position_seconds / wp.duration_seconds >= 0.5)) - AND NOT EXISTS ( - SELECT 1 - FROM user_history_hidden_items hhi - WHERE hhi.user_id = wp.user_id - AND hhi.profile_id = wp.profile_id - AND hhi.media_item_id = wp.media_item_id - AND wp.updated_at <= hhi.hidden_before - )`, - userID, profileID) + query := fmt.Sprintf(` + WITH %s + SELECT DISTINCT item_id + FROM watched_activity + WHERE user_id = $1 AND profile_id = $2`, watchedActivityCTE) + rows, err := r.pool.Query(ctx, query, userID, profileID) if err != nil { return nil, fmt.Errorf("get watched item IDs: %w", err) } @@ -1695,21 +1760,15 @@ func (r *Repo) GetRecentCompletedItemIDs(ctx context.Context, userID int, profil return []string{}, nil } - rows, err := r.pool.Query(ctx, ` - SELECT media_item_id - FROM user_watch_progress + query := fmt.Sprintf(` + WITH %s + SELECT leaf_item_id + FROM watched_activity WHERE user_id = $1 AND profile_id = $2 AND completed = true - AND NOT EXISTS ( - SELECT 1 - FROM user_history_hidden_items hhi - WHERE hhi.user_id = user_watch_progress.user_id - AND hhi.profile_id = user_watch_progress.profile_id - AND hhi.media_item_id = user_watch_progress.media_item_id - AND user_watch_progress.updated_at <= hhi.hidden_before - ) - ORDER BY updated_at DESC, media_item_id ASC + ORDER BY updated_at DESC, leaf_item_id ASC LIMIT $3 - `, userID, profileID, limit) + `, watchedActivityCTE) + rows, err := r.pool.Query(ctx, query, userID, profileID, limit) if err != nil { return nil, fmt.Errorf("get recent completed item IDs: %w", err) } diff --git a/internal/recommendations/repo_test.go b/internal/recommendations/repo_test.go index da5e52ca..a67d84dd 100644 --- a/internal/recommendations/repo_test.go +++ b/internal/recommendations/repo_test.go @@ -24,6 +24,128 @@ func TestTasteSeedCandidateQueryOrdersByReliableColdStartSignals(t *testing.T) { } } +func TestTasteSeedCandidateQueryIncludesEbooks(t *testing.T) { + query := strings.Join(strings.Fields(tasteSeedCandidateQuery), " ") + + for _, term := range []string{ + "(mi.status = 'matched' OR mi.type = 'audiobook' OR mi.type = 'ebook')", + "mi.type IN ('movie', 'series', 'audiobook', 'ebook')", + } { + if !strings.Contains(query, term) { + t.Fatalf("taste seed candidate query missing %q: %s", term, query) + } + } +} + +func TestItemWatchersQueryCountsDistinctWatchersPerItem(t *testing.T) { + query := strings.Join(strings.Fields(itemWatchersQuery), " ") + + // watched_activity rolls episodes up to their parent series, so one + // watcher can emit many rows per item. The pairs must be deduplicated + // before ROW_NUMBER ranking and the HAVING watcher count, otherwise a + // single binge-watcher inflates both. + assertQueryTermsInOrder(t, query, + "ROW_NUMBER() OVER (PARTITION BY user_id, profile_id ORDER BY MAX(updated_at) DESC) AS rn", + "GROUP BY user_id, profile_id, watcher_id, item_id", + "WHERE rn <= $1", + "GROUP BY media_item_id", + "HAVING COUNT(DISTINCT user_id) >= $2", + ) +} + +func TestItemWatchersQueryMinWatchersCountsDistinctAccounts(t *testing.T) { + query := strings.Join(strings.Fields(itemWatchersQuery), " ") + + // The minimum-watchers floor is a sparsity/privacy threshold: one login + // account with N household profiles must not satisfy it by itself, so the + // HAVING clause counts distinct user_id (accounts), not watcher rows. The + // per-profile watcher_id array is still aggregated for Jaccard math. + assertQueryTermsInOrder(t, query, + "SELECT user_id, watcher_id, item_id AS media_item_id", + "ARRAY_AGG(watcher_id) AS watchers", + "HAVING COUNT(DISTINCT user_id) >= $2", + ) + if strings.Contains(query, "HAVING COUNT(*) >= $2") { + t.Fatal("minWatchers floor must not count per-profile watcher rows") + } +} + +func TestEbookReaderProgressForUserQueryGatesHiddenHistory(t *testing.T) { + query := strings.Join(strings.Fields(ebookReaderProgressForUserQuery), " ") + + // Mirrors GetWatchProgressForUser: rows the user hid stay hidden while + // updated_at <= hidden_before; reading activity after hidden_before + // surfaces again. + assertQueryTermsInOrder(t, query, + "FROM ebook_reader_progress", + "NOT EXISTS", + "FROM user_history_hidden_items hhi", + "hhi.media_item_id = ebook_reader_progress.content_id", + "ebook_reader_progress.updated_at <= hhi.hidden_before", + ) +} + +func TestWatchedActivityCTEIncludesEbookReaderProgress(t *testing.T) { + query := strings.Join(strings.Fields(watchedActivityCTE), " ") + + for _, term := range []string{ + "FROM ebook_reader_progress erp", + "erp.progress >= 0.5", + "erp.progress >= 0.9 AS completed", + "hhi.media_item_id = erp.content_id", + } { + if !strings.Contains(query, term) { + t.Fatalf("watched activity CTE missing %q: %s", term, query) + } + } +} + +func TestEmbeddingEligibilityWhereClauseIncludesBookTypes(t *testing.T) { + clause := embeddingEligibilityWhereClause() + + for _, term := range []string{"mi.status = 'matched'", "mi.type = 'audiobook'", "mi.type = 'ebook'"} { + if !strings.Contains(clause, term) { + t.Fatalf("embedding eligibility clause missing %q: %s", term, clause) + } + } +} + +func TestEmbeddingEligibilityWhereClauseUsesMediaItemsAlias(t *testing.T) { + clause := embeddingEligibilityWhereClause() + + if strings.Contains(clause, " type =") || strings.Contains(clause, " status =") { + t.Fatalf("embedding eligibility clause should qualify media_items columns: %s", clause) + } +} + +func TestRecommendationItemEligibilityWhereClauseIncludesBookTypes(t *testing.T) { + clause := recommendationItemEligibilityWhereClause("mi") + + for _, term := range []string{"mi.status = 'matched'", "mi.type = 'audiobook'", "mi.type = 'ebook'"} { + if !strings.Contains(clause, term) { + t.Fatalf("recommendation item eligibility clause missing %q: %s", term, clause) + } + } +} + +func TestRecommendationItemEligibilityWhereClauseDefaultsAlias(t *testing.T) { + clause := recommendationItemEligibilityWhereClause("") + + if !strings.Contains(clause, "media_items.status = 'matched'") { + t.Fatalf("default eligibility alias missing media_items qualification: %s", clause) + } +} + +func TestTasteProfileRefreshSubjectsQueryIncludesEbookReaderProgress(t *testing.T) { + query := strings.Join(strings.Fields(tasteProfileRefreshSubjectsQuery), " ") + + assertQueryTermsInOrder(t, query, + "SELECT DISTINCT user_id, profile_id FROM user_watch_progress", + "UNION", + "SELECT DISTINCT user_id, profile_id FROM ebook_reader_progress", + ) +} + func assertQueryTermsInOrder(t *testing.T, query string, terms ...string) { t.Helper() diff --git a/internal/recommendations/signals.go b/internal/recommendations/signals.go index c73b4e5a..ee462a95 100644 --- a/internal/recommendations/signals.go +++ b/internal/recommendations/signals.go @@ -14,6 +14,7 @@ const signalPageSize = 1000 type signalRepo interface { GetWatchedItemIDSet(ctx context.Context, userID int, profileID string) (map[string]struct{}, error) GetWatchProgressForUser(ctx context.Context, userID int, profileID string) ([]WatchProgressRow, error) + GetEbookReaderProgressForUser(ctx context.Context, userID int, profileID string) ([]WatchProgressRow, error) GetRecentCompletedItemIDs(ctx context.Context, userID int, profileID string, limit int) ([]string, error) GetRewatchCounts(ctx context.Context, userID int, profileID string) ([]RewatchCount, error) ResolveCanonicalItemIDSet(ctx context.Context, contentIDs []string) (map[string]struct{}, error) @@ -69,6 +70,15 @@ func (s *SignalReader) WatchedItemIDSet(ctx context.Context, userID int, profile }); err != nil { return nil, err } + ebookProgress, err := s.repo.GetEbookReaderProgressForUser(ctx, userID, profileID) + if err != nil { + return nil, err + } + for _, wp := range ebookProgress { + if wp.Completed || watchedProgressThresholdMet(wp.PositionSeconds, wp.DurationSeconds) { + rawIDs = append(rawIDs, wp.MediaItemID) + } + } return s.repo.ResolveCanonicalItemIDSet(ctx, rawIDs) } @@ -97,6 +107,11 @@ func (s *SignalReader) WatchProgressForUser(ctx context.Context, userID int, pro }); err != nil { return nil, err } + ebookProgress, err := s.repo.GetEbookReaderProgressForUser(ctx, userID, profileID) + if err != nil { + return nil, err + } + rows = append(rows, ebookProgress...) return rows, nil } @@ -118,20 +133,38 @@ func (s *SignalReader) RecentCompletedItemIDs(ctx context.Context, userID int, p if err != nil { return nil, fmt.Errorf("list completed progress from store: %w", err) } - sort.SliceStable(progress, func(i, j int) bool { - left := parseSignalTime(progress[i].UpdatedAt, time.Time{}) - right := parseSignalTime(progress[j].UpdatedAt, time.Time{}) - if !left.Equal(right) { - return left.After(right) - } - return progress[i].MediaItemID < progress[j].MediaItemID - }) - - ids := make([]string, 0, min(limit, len(progress))) + completed := make([]WatchProgressRow, 0, len(progress)) for _, wp := range progress { if !wp.Completed { continue } + completed = append(completed, WatchProgressRow{ + MediaItemID: wp.MediaItemID, + Completed: true, + UpdatedAt: parseSignalTime(wp.UpdatedAt, time.Time{}), + }) + } + ebookProgress, err := s.repo.GetEbookReaderProgressForUser(ctx, userID, profileID) + if err != nil { + return nil, err + } + for _, wp := range ebookProgress { + if !wp.Completed { + continue + } + completed = append(completed, wp) + } + sort.SliceStable(completed, func(i, j int) bool { + left := completed[i].UpdatedAt + right := completed[j].UpdatedAt + if !left.Equal(right) { + return left.After(right) + } + return completed[i].MediaItemID < completed[j].MediaItemID + }) + + ids := make([]string, 0, min(limit, len(completed))) + for _, wp := range completed { ids = append(ids, wp.MediaItemID) if len(ids) == limit { break diff --git a/internal/recommendations/signals_test.go b/internal/recommendations/signals_test.go index a3b54480..ae8f54ee 100644 --- a/internal/recommendations/signals_test.go +++ b/internal/recommendations/signals_test.go @@ -14,6 +14,7 @@ type fakeSignalRepo struct { fallbackWatched map[string]struct{} fallbackProgress []WatchProgressRow + ebookProgress []WatchProgressRow fallbackRecentCompleted []string fallbackRewatches []RewatchCount } @@ -26,6 +27,10 @@ func (r *fakeSignalRepo) GetWatchProgressForUser(context.Context, int, string) ( return r.fallbackProgress, nil } +func (r *fakeSignalRepo) GetEbookReaderProgressForUser(context.Context, int, string) ([]WatchProgressRow, error) { + return r.ebookProgress, nil +} + func (r *fakeSignalRepo) GetRecentCompletedItemIDs(context.Context, int, string, int) ([]string, error) { return r.fallbackRecentCompleted, nil } @@ -161,6 +166,52 @@ func TestSignalReaderWatchedSetCanonicalizesStoreProgress(t *testing.T) { } } +func TestSignalReaderWatchedSetIncludesEbookReaderProgress(t *testing.T) { + store := &fakeSignalStore{} + repo := &fakeSignalRepo{ + ebookProgress: []WatchProgressRow{ + {MediaItemID: "ebook-half", PositionSeconds: 0.6, DurationSeconds: 1, UpdatedAt: time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)}, + {MediaItemID: "ebook-low", PositionSeconds: 0.2, DurationSeconds: 1, UpdatedAt: time.Date(2026, 6, 1, 11, 0, 0, 0, time.UTC)}, + }, + } + reader := NewSignalReader(repo, fakeSignalProvider{store: store}) + + watched, err := reader.WatchedItemIDSet(context.Background(), 7, "p1") + if err != nil { + t.Fatalf("WatchedItemIDSet returned error: %v", err) + } + + if _, ok := watched["ebook-half"]; !ok { + t.Fatalf("expected ebook-half in watched set, got %#v", watched) + } + if _, ok := watched["ebook-low"]; ok { + t.Fatalf("did not expect low-progress ebook in watched set: %#v", watched) + } +} + +func TestSignalReaderWatchProgressIncludesEbookReaderProgress(t *testing.T) { + store := &fakeSignalStore{progress: []userstore.WatchProgress{ + {ProfileID: "p1", MediaItemID: "movie", PositionSeconds: 60, DurationSeconds: 100, UpdatedAt: "2026-06-01T10:00:00Z"}, + }} + repo := &fakeSignalRepo{ + ebookProgress: []WatchProgressRow{ + {MediaItemID: "ebook", PositionSeconds: 0.42, DurationSeconds: 1, Completed: false, UpdatedAt: time.Date(2026, 6, 1, 11, 0, 0, 0, time.UTC)}, + }, + } + reader := NewSignalReader(repo, fakeSignalProvider{store: store}) + + progress, err := reader.WatchProgressForUser(context.Background(), 7, "p1") + if err != nil { + t.Fatalf("WatchProgressForUser returned error: %v", err) + } + + if !slices.ContainsFunc(progress, func(row WatchProgressRow) bool { + return row.MediaItemID == "ebook" && row.PositionSeconds == 0.42 && row.DurationSeconds == 1 + }) { + t.Fatalf("ebook progress missing from rows: %#v", progress) + } +} + func TestSignalReaderRecentCompletedUsesStoreUpdatedOrder(t *testing.T) { store := &fakeSignalStore{progress: []userstore.WatchProgress{ {ProfileID: "p1", MediaItemID: "older", Completed: true, UpdatedAt: "2026-05-01T10:00:00Z"}, @@ -181,6 +232,29 @@ func TestSignalReaderRecentCompletedUsesStoreUpdatedOrder(t *testing.T) { } } +func TestSignalReaderRecentCompletedIncludesEbookReaderProgress(t *testing.T) { + store := &fakeSignalStore{progress: []userstore.WatchProgress{ + {ProfileID: "p1", MediaItemID: "movie", Completed: true, UpdatedAt: "2026-06-01T10:00:00Z"}, + }} + repo := &fakeSignalRepo{ + ebookProgress: []WatchProgressRow{ + {MediaItemID: "ebook-done", PositionSeconds: 0.95, DurationSeconds: 1, Completed: true, UpdatedAt: time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)}, + {MediaItemID: "ebook-open", PositionSeconds: 0.4, DurationSeconds: 1, Completed: false, UpdatedAt: time.Date(2026, 6, 4, 10, 0, 0, 0, time.UTC)}, + }, + } + reader := NewSignalReader(repo, fakeSignalProvider{store: store}) + + ids, err := reader.RecentCompletedItemIDs(context.Background(), 7, "p1", 3) + if err != nil { + t.Fatalf("RecentCompletedItemIDs returned error: %v", err) + } + + want := []string{"ebook-done", "movie"} + if !slices.Equal(ids, want) { + t.Fatalf("recent completed = %#v, want %#v", ids, want) + } +} + func TestSignalReaderRewatchCountsAggregatesCompletedHistory(t *testing.T) { store := &fakeSignalStore{history: []userstore.WatchHistoryEntry{ {ProfileID: "p1", MediaItemID: "rewatched", Completed: true, WatchedAt: "2026-05-01T10:00:00Z"}, diff --git a/internal/recommendations/taste_signals.go b/internal/recommendations/taste_signals.go index 5fa9f95d..028e063a 100644 --- a/internal/recommendations/taste_signals.go +++ b/internal/recommendations/taste_signals.go @@ -12,6 +12,7 @@ const ( canonicalKindSeries canonicalContentKind = "series" canonicalKindSeason canonicalContentKind = "season" canonicalKindEpisode canonicalContentKind = "episode" + canonicalKindEbook canonicalContentKind = "ebook" ) const ( @@ -310,7 +311,11 @@ func buildCanonicalImplicitSignals(progress []WatchProgressRow, rewatches []Rewa if timestamp.After(builder.lastSignalAt) { builder.lastSignalAt = timestamp } - case canonicalKindMovie, canonicalKindSeries: + case canonicalKindMovie, canonicalKindSeries, canonicalKindEbook: + // Ebooks are their own canonical entity. Reader progress rows carry + // progress as position with duration 1, so implicitWatchWeight + // treats the reading ratio exactly like a movie's + // position/duration ratio (finished book == finished movie). signals[signal.Ref.CanonicalID] += signal.Weight * timeDecay(timestamp, now, halfLife) } } diff --git a/internal/recommendations/taste_signals_test.go b/internal/recommendations/taste_signals_test.go new file mode 100644 index 00000000..d6ebc6ae --- /dev/null +++ b/internal/recommendations/taste_signals_test.go @@ -0,0 +1,71 @@ +package recommendations + +import ( + "math" + "testing" + "time" +) + +func TestBuildCanonicalImplicitSignalsIncludesEbookCompletion(t *testing.T) { + now := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + halfLife := 90.0 + + // Ebook reader progress rows map progress onto position with duration 1, + // so a finished book carries the same ratio shape as a finished movie. + progress := []WatchProgressRow{ + {MediaItemID: "ebook-1", PositionSeconds: 0.95, DurationSeconds: 1, Completed: true, UpdatedAt: now}, + {MediaItemID: "movie-1", PositionSeconds: 5400, DurationSeconds: 5700, Completed: true, UpdatedAt: now}, + } + refs := map[string]canonicalContentRef{ + "ebook-1": {Kind: canonicalKindEbook, CanonicalID: "ebook-1"}, + "movie-1": {Kind: canonicalKindMovie, CanonicalID: "movie-1"}, + } + + signals, completed := buildCanonicalImplicitSignals(progress, nil, refs, now, halfLife) + + ebookWeight, ok := signals["ebook-1"] + if !ok { + t.Fatal("expected ebook completion to produce a canonical implicit signal") + } + movieWeight, ok := signals["movie-1"] + if !ok { + t.Fatal("expected movie completion to produce a canonical implicit signal") + } + if math.Abs(ebookWeight-movieWeight) > 1e-9 { + t.Fatalf("ebook completion weight %v should match movie completion weight %v", ebookWeight, movieWeight) + } + if math.Abs(ebookWeight-WeightWatchHigh) > 1e-9 { + t.Fatalf("ebook completion weight = %v, want WeightWatchHigh (%v) with no decay at t=now", ebookWeight, WeightWatchHigh) + } + if _, ok := completed["ebook-1"]; !ok { + t.Fatal("expected completed ebook to enter the completed set") + } +} + +func TestBuildCanonicalImplicitSignalsWeightsPartialEbookLikeMovie(t *testing.T) { + now := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + halfLife := 90.0 + + progress := []WatchProgressRow{ + // Mid-read book (60%) mirrors a movie watched to 60%. + {MediaItemID: "ebook-mid", PositionSeconds: 0.6, DurationSeconds: 1, UpdatedAt: now}, + // Abandoned book (<15%) mirrors an abandoned movie: negative signal. + {MediaItemID: "ebook-abandoned", PositionSeconds: 0.05, DurationSeconds: 1, UpdatedAt: now}, + } + refs := map[string]canonicalContentRef{ + "ebook-mid": {Kind: canonicalKindEbook, CanonicalID: "ebook-mid"}, + "ebook-abandoned": {Kind: canonicalKindEbook, CanonicalID: "ebook-abandoned"}, + } + + signals, completed := buildCanonicalImplicitSignals(progress, nil, refs, now, halfLife) + + if got := signals["ebook-mid"]; math.Abs(got-WeightWatchMed) > 1e-9 { + t.Fatalf("mid-read ebook weight = %v, want WeightWatchMed (%v)", got, WeightWatchMed) + } + if got := signals["ebook-abandoned"]; math.Abs(got-WeightWatchLow) > 1e-9 { + t.Fatalf("abandoned ebook weight = %v, want WeightWatchLow (%v)", got, WeightWatchLow) + } + if len(completed) != 0 { + t.Fatalf("no row was completed, got completed set %v", completed) + } +} diff --git a/internal/recommendations/worker.go b/internal/recommendations/worker.go index bc509b52..2a64dc1b 100644 --- a/internal/recommendations/worker.go +++ b/internal/recommendations/worker.go @@ -31,6 +31,19 @@ type Worker struct { cancelFunc context.CancelFunc } +const tasteProfileRefreshSubjectsQuery = ` + SELECT DISTINCT user_id, profile_id FROM user_ratings + UNION + SELECT DISTINCT user_id, profile_id FROM user_taste_profiles + UNION + SELECT DISTINCT user_id, profile_id FROM user_watch_progress + UNION + SELECT DISTINCT user_id, profile_id FROM ebook_reader_progress + UNION + SELECT DISTINCT user_id, profile_id FROM user_favorites + UNION + SELECT DISTINCT user_id, profile_id FROM user_watchlist` + // NewWorker creates a new recommendation Worker. func NewWorker(engine *Engine, embeddingsCron, tasteProfilesCron, cowatchCron, recommendationsCron string) (*Worker, error) { w := &Worker{ @@ -266,16 +279,7 @@ func (w *Worker) doTasteProfiles() { defer cancel() slog.Info("starting taste profile refresh") - rows, err := w.engine.pool.Query(ctx, ` - SELECT DISTINCT user_id, profile_id FROM user_ratings - UNION - SELECT DISTINCT user_id, profile_id FROM user_taste_profiles - UNION - SELECT DISTINCT user_id, profile_id FROM user_watch_progress - UNION - SELECT DISTINCT user_id, profile_id FROM user_favorites - UNION - SELECT DISTINCT user_id, profile_id FROM user_watchlist`) + rows, err := w.engine.pool.Query(ctx, tasteProfileRefreshSubjectsQuery) if err != nil { slog.Error("taste profile query failed", "error", err) return diff --git a/internal/requests/arrclient/resources.go b/internal/requests/arrclient/resources.go index eee11bc6..790ca012 100644 --- a/internal/requests/arrclient/resources.go +++ b/internal/requests/arrclient/resources.go @@ -24,51 +24,31 @@ type TagResource struct { Label string `json:"label"` } -func ListRootFolders(ctx context.Context, client *Client) ([]mediarequests.IntegrationRootFolder, error) { +// ListRootFolders returns the arr root folders. It yields the local +// RootFolderResource shape directly; the schema-driven config form consumes +// generic RouterOptions from the plugin instead of arr-specific option types. +func ListRootFolders(ctx context.Context, client *Client) ([]RootFolderResource, error) { var resources []RootFolderResource if err := client.GetJSON(ctx, "/api/v3/rootfolder", &resources); err != nil { return nil, err } - out := make([]mediarequests.IntegrationRootFolder, 0, len(resources)) - for _, resource := range resources { - out = append(out, mediarequests.IntegrationRootFolder{ - Path: resource.Path, - FreeSpace: resource.FreeSpace, - TotalSpace: resource.TotalSpace, - Accessible: resource.Accessible, - }) - } - return out, nil + return resources, nil } -func ListQualityProfiles(ctx context.Context, client *Client) ([]mediarequests.IntegrationQualityProfile, error) { +func ListQualityProfiles(ctx context.Context, client *Client) ([]QualityProfileResource, error) { var resources []QualityProfileResource if err := client.GetJSON(ctx, "/api/v3/qualityprofile", &resources); err != nil { return nil, err } - out := make([]mediarequests.IntegrationQualityProfile, 0, len(resources)) - for _, resource := range resources { - out = append(out, mediarequests.IntegrationQualityProfile{ - ID: resource.ID, - Name: resource.Name, - }) - } - return out, nil + return resources, nil } -func ListTags(ctx context.Context, client *Client) ([]mediarequests.IntegrationTag, error) { +func ListTags(ctx context.Context, client *Client) ([]TagResource, error) { var resources []TagResource if err := client.GetJSON(ctx, "/api/v3/tag", &resources); err != nil { return nil, err } - out := make([]mediarequests.IntegrationTag, 0, len(resources)) - for _, resource := range resources { - out = append(out, mediarequests.IntegrationTag{ - ID: resource.ID, - Label: resource.Label, - }) - } - return out, nil + return resources, nil } // AcceptedWithoutResponse returns a FulfillmentResult marking the submission diff --git a/internal/requests/errors.go b/internal/requests/errors.go index 84998d95..52b46638 100644 --- a/internal/requests/errors.go +++ b/internal/requests/errors.go @@ -28,3 +28,16 @@ func (e QuotaError) Error() string { func (e QuotaError) Unwrap() error { return ErrQuotaExceeded } + +// ValidationError carries plugin Validate results back to the API layer. +type ValidationError struct { + FieldErrors map[string]string + FormError string +} + +func (e *ValidationError) Error() string { + if e.FormError != "" { + return e.FormError + } + return "validation failed" +} diff --git a/internal/requests/provider.go b/internal/requests/provider.go new file mode 100644 index 00000000..9c0bdb79 --- /dev/null +++ b/internal/requests/provider.go @@ -0,0 +1,249 @@ +package requests + +import ( + "context" + "fmt" + "strconv" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +// RequestRouterProvider fulfills a whole request and checks target status via a +// request_router.v1 plugin. The host owns governance (which qualities) and the +// target records; the provider is the plugin boundary. +type RequestRouterProvider interface { + Fulfill(ctx context.Context, installationID int, capabilityID string, req Request, qualities []Quality, conns []ResolvedRouterConnection) ([]RouterTarget, string, error) + CheckStatus(ctx context.Context, installationID int, capabilityID string, req Request, targets []RouterTargetRef, conns []ResolvedRouterConnection) ([]RouterTargetStatus, error) + ListConfigOptions(ctx context.Context, installationID int, capabilityID string, conn ResolvedRouterConnection) (map[string][]RouterOption, error) + TestConnection(ctx context.Context, installationID int, capabilityID string, conn ResolvedRouterConnection) (bool, string, error) + Validate(ctx context.Context, installationID int, capabilityID string, conn ResolvedRouterConnection, siblings []ResolvedRouterConnection) (fieldErrors map[string]string, formError string, err error) +} + +// ResolvedRouterConnection is a connection with plaintext credentials + parsed config. +type ResolvedRouterConnection struct { + ID string + BaseURL string + APIKey string + Config map[string]any +} + +type RouterTarget struct { + Quality Quality + ConnectionID string + ExternalID string + ExternalStatus string + Status Status + Message string +} + +type RouterTargetRef struct { + Quality Quality + ConnectionID string + ExternalID string +} + +type RouterTargetStatus struct { + Quality Quality + ConnectionID string + Status Status + ExternalStatus string + Message string +} + +type RouterOption struct { + Value string `json:"value"` + Label string `json:"label"` +} + +// RouterClientResolver yields a per-(installation, capability) router client. +type RouterClientResolver interface { + RequestRouterClient(ctx context.Context, installationID int, capabilityID string) (RouterClient, error) +} + +// RouterClient mirrors *pluginhost.RequestRouterClient (exported so the api +// package can adapt the concrete type across packages without an import cycle). +type RouterClient interface { + Fulfill(ctx context.Context, req *pluginv1.FulfillRequest) (*pluginv1.FulfillResponse, error) + CheckStatus(ctx context.Context, req *pluginv1.CheckStatusRequest) (*pluginv1.CheckStatusResponse, error) + ListConfigOptions(ctx context.Context, req *pluginv1.ListConfigOptionsRequest) (*pluginv1.ListConfigOptionsResponse, error) + TestConnection(ctx context.Context, req *pluginv1.TestConnectionRequest) (*pluginv1.TestConnectionResponse, error) + Validate(ctx context.Context, req *pluginv1.ValidateRequest) (*pluginv1.ValidateResponse, error) +} + +type pluginRouterProvider struct{ resolver RouterClientResolver } + +func NewPluginRouterProvider(r RouterClientResolver) RequestRouterProvider { + return &pluginRouterProvider{resolver: r} +} + +func routerDescriptor(req Request) *pluginv1.RequestDescriptor { + ids := map[string]string{} + if req.TMDBID != 0 { + ids["tmdb"] = strconv.Itoa(req.TMDBID) + } + if req.TVDBID != nil { + ids["tvdb"] = strconv.Itoa(*req.TVDBID) + } + if req.IMDbID != "" { + ids["imdb"] = req.IMDbID + } + year := 0 + if req.Year != nil { + year = *req.Year + } + return &pluginv1.RequestDescriptor{ + MediaType: string(req.MediaType), + Title: req.Title, + Year: int32(year), + ExternalIds: ids, + IsAnime: req.IsAnime, + RequesterUserId: int64(req.RequestedByUserID), + RequesterProfileId: req.RequestedByProfileID, + RequesterEmail: req.RequesterEmail, + RequesterUsername: req.RequesterUsername, + } +} + +func routerProtoConn(c ResolvedRouterConnection) (*pluginv1.RouterConnection, error) { + cfg, err := structpb.NewStruct(c.Config) + if err != nil { + return nil, fmt.Errorf("router: encode connection %s config: %w", c.ID, err) + } + return &pluginv1.RouterConnection{Id: c.ID, BaseUrl: c.BaseURL, ApiKey: c.APIKey, Config: cfg}, nil +} + +func routerProtoConns(conns []ResolvedRouterConnection) ([]*pluginv1.RouterConnection, error) { + out := make([]*pluginv1.RouterConnection, 0, len(conns)) + for _, c := range conns { + pc, err := routerProtoConn(c) + if err != nil { + return nil, err + } + out = append(out, pc) + } + return out, nil +} + +func (p *pluginRouterProvider) Fulfill(ctx context.Context, installationID int, capabilityID string, req Request, qualities []Quality, conns []ResolvedRouterConnection) ([]RouterTarget, string, error) { + client, err := p.resolver.RequestRouterClient(ctx, installationID, capabilityID) + if err != nil { + return nil, "", err + } + qs := make([]*pluginv1.RequestedQuality, 0, len(qualities)) + for _, q := range qualities { + qs = append(qs, &pluginv1.RequestedQuality{Id: string(q), Is4K: q == Quality2160p}) + } + pconns, err := routerProtoConns(conns) + if err != nil { + return nil, "", err + } + resp, err := client.Fulfill(ctx, &pluginv1.FulfillRequest{ + CapabilityId: capabilityID, Request: routerDescriptor(req), Qualities: qs, Connections: pconns, + }) + if err != nil { + return nil, "", err + } + targets := make([]RouterTarget, 0, len(resp.GetTargets())) + for _, t := range resp.GetTargets() { + targets = append(targets, RouterTarget{ + Quality: Quality(t.GetQuality()), ConnectionID: t.GetConnectionId(), + ExternalID: t.GetExternalId(), ExternalStatus: t.GetExternalStatus(), + Status: Status(t.GetStatus()), Message: t.GetMessage(), + }) + } + return targets, resp.GetMessage(), nil +} + +func (p *pluginRouterProvider) CheckStatus(ctx context.Context, installationID int, capabilityID string, req Request, targets []RouterTargetRef, conns []ResolvedRouterConnection) ([]RouterTargetStatus, error) { + client, err := p.resolver.RequestRouterClient(ctx, installationID, capabilityID) + if err != nil { + return nil, err + } + refs := make([]*pluginv1.TargetRef, 0, len(targets)) + for _, t := range targets { + refs = append(refs, &pluginv1.TargetRef{Quality: string(t.Quality), ConnectionId: t.ConnectionID, ExternalId: t.ExternalID}) + } + pconns, err := routerProtoConns(conns) + if err != nil { + return nil, err + } + resp, err := client.CheckStatus(ctx, &pluginv1.CheckStatusRequest{ + CapabilityId: capabilityID, Request: routerDescriptor(req), Targets: refs, Connections: pconns, + }) + if err != nil { + return nil, err + } + out := make([]RouterTargetStatus, 0, len(resp.GetStatuses())) + for _, st := range resp.GetStatuses() { + out = append(out, RouterTargetStatus{ + Quality: Quality(st.GetQuality()), ConnectionID: st.GetConnectionId(), + Status: Status(st.GetStatus()), ExternalStatus: st.GetExternalStatus(), Message: st.GetMessage(), + }) + } + return out, nil +} + +func (p *pluginRouterProvider) ListConfigOptions(ctx context.Context, installationID int, capabilityID string, conn ResolvedRouterConnection) (map[string][]RouterOption, error) { + client, err := p.resolver.RequestRouterClient(ctx, installationID, capabilityID) + if err != nil { + return nil, err + } + pconn, err := routerProtoConn(conn) + if err != nil { + return nil, err + } + resp, err := client.ListConfigOptions(ctx, &pluginv1.ListConfigOptionsRequest{ + CapabilityId: capabilityID, Connection: pconn, + }) + if err != nil { + return nil, err + } + out := map[string][]RouterOption{} + for field, list := range resp.GetOptionsByField() { + opts := make([]RouterOption, 0, len(list.GetOptions())) + for _, o := range list.GetOptions() { + opts = append(opts, RouterOption{Value: o.GetValue(), Label: o.GetLabel()}) + } + out[field] = opts + } + return out, nil +} + +func (p *pluginRouterProvider) TestConnection(ctx context.Context, installationID int, capabilityID string, conn ResolvedRouterConnection) (bool, string, error) { + client, err := p.resolver.RequestRouterClient(ctx, installationID, capabilityID) + if err != nil { + return false, "", err + } + pconn, err := routerProtoConn(conn) + if err != nil { + return false, "", err + } + resp, err := client.TestConnection(ctx, &pluginv1.TestConnectionRequest{ + CapabilityId: capabilityID, Connection: pconn, + }) + if err != nil { + return false, "", err + } + return resp.GetOk(), resp.GetMessage(), nil +} + +func (p *pluginRouterProvider) Validate(ctx context.Context, installationID int, capabilityID string, conn ResolvedRouterConnection, siblings []ResolvedRouterConnection) (map[string]string, string, error) { + client, err := p.resolver.RequestRouterClient(ctx, installationID, capabilityID) + if err != nil { + return nil, "", err + } + pc, err := routerProtoConn(conn) + if err != nil { + return nil, "", err + } + sibs, err := routerProtoConns(siblings) + if err != nil { + return nil, "", err + } + resp, err := client.Validate(ctx, &pluginv1.ValidateRequest{CapabilityId: capabilityID, Connection: pc, Siblings: sibs}) + if err != nil { + return nil, "", err + } + return resp.GetFieldErrors(), resp.GetFormError(), nil +} diff --git a/internal/requests/provider_test.go b/internal/requests/provider_test.go new file mode 100644 index 00000000..137dd620 --- /dev/null +++ b/internal/requests/provider_test.go @@ -0,0 +1,139 @@ +package requests + +import ( + "context" + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" +) + +type fakeRouterClient struct { + lastReq *pluginv1.FulfillRequest + lastCheckReq *pluginv1.CheckStatusRequest + statuses []*pluginv1.TargetStatus + optionsByField map[string]*pluginv1.ConfigOptionList + validateResp *pluginv1.ValidateResponse +} + +func (f *fakeRouterClient) Fulfill(_ context.Context, req *pluginv1.FulfillRequest) (*pluginv1.FulfillResponse, error) { + f.lastReq = req + return &pluginv1.FulfillResponse{Targets: []*pluginv1.FulfillmentTarget{ + {Quality: "1080p", ConnectionId: "c1", ExternalId: "7", Status: "queued"}, + }}, nil +} +func (f *fakeRouterClient) CheckStatus(_ context.Context, req *pluginv1.CheckStatusRequest) (*pluginv1.CheckStatusResponse, error) { + f.lastCheckReq = req + return &pluginv1.CheckStatusResponse{Statuses: f.statuses}, nil +} +func (f *fakeRouterClient) ListConfigOptions(context.Context, *pluginv1.ListConfigOptionsRequest) (*pluginv1.ListConfigOptionsResponse, error) { + return &pluginv1.ListConfigOptionsResponse{OptionsByField: f.optionsByField}, nil +} +func (f *fakeRouterClient) TestConnection(context.Context, *pluginv1.TestConnectionRequest) (*pluginv1.TestConnectionResponse, error) { + return &pluginv1.TestConnectionResponse{Ok: true}, nil +} +func (f *fakeRouterClient) Validate(context.Context, *pluginv1.ValidateRequest) (*pluginv1.ValidateResponse, error) { + return f.validateResp, nil +} + +type fakeRouterResolver struct{ c RouterClient } + +func (r fakeRouterResolver) RequestRouterClient(context.Context, int, string) (RouterClient, error) { + return r.c, nil +} + +func TestPluginRouterProviderFulfillTranslates(t *testing.T) { + fc := &fakeRouterClient{} + p := NewPluginRouterProvider(fakeRouterResolver{c: fc}) + year := 2020 + req := Request{MediaType: MediaTypeMovie, TMDBID: 42, Title: "X", Year: &year} + targets, msg, err := p.Fulfill(context.Background(), 1, "arr", req, []Quality{Quality1080p}, + []ResolvedRouterConnection{{ID: "c1", BaseURL: "http://r", APIKey: "k", Config: map[string]any{"service_kind": "radarr"}}}) + if err != nil { + t.Fatalf("Fulfill: %v", err) + } + if msg != "" || len(targets) != 1 || targets[0].Status != StatusQueued || targets[0].ExternalID != "7" { + t.Fatalf("unexpected result: %+v msg=%q", targets, msg) + } + gotQ := fc.lastReq.GetQualities() + if fc.lastReq.GetRequest().GetExternalIds()["tmdb"] != "42" || len(gotQ) != 1 { + t.Fatalf("descriptor/qualities not forwarded: %+v", fc.lastReq) + } + if gotQ[0].GetId() != "1080p" || gotQ[0].GetIs4K() { + t.Fatalf("1080p should be id=1080p is4k=false, got %+v", gotQ[0]) + } +} + +func TestPluginRouterProviderCheckStatusTranslates(t *testing.T) { + fc := &fakeRouterClient{statuses: []*pluginv1.TargetStatus{ + {Quality: "1080p", ConnectionId: "c1", Status: "downloading", ExternalStatus: "grabbed", Message: "in queue"}, + {Quality: "2160p", ConnectionId: "c2", Status: "completed", ExternalStatus: "imported", Message: ""}, + }} + p := NewPluginRouterProvider(fakeRouterResolver{c: fc}) + req := Request{MediaType: MediaTypeMovie, TMDBID: 42, Title: "X"} + refs := []RouterTargetRef{ + {Quality: Quality1080p, ConnectionID: "c1", ExternalID: "7"}, + {Quality: Quality2160p, ConnectionID: "c2", ExternalID: "8"}, + } + out, err := p.CheckStatus(context.Background(), 1, "arr", req, refs, + []ResolvedRouterConnection{{ID: "c1", BaseURL: "http://r", APIKey: "k", Config: map[string]any{"service_kind": "radarr"}}}) + if err != nil { + t.Fatalf("CheckStatus: %v", err) + } + if len(out) != 2 { + t.Fatalf("expected 2 statuses, got %d", len(out)) + } + if out[0].Quality != Quality1080p || out[0].ConnectionID != "c1" || + out[0].Status != StatusDownloading || out[0].ExternalStatus != "grabbed" || out[0].Message != "in queue" { + t.Fatalf("status[0] mismapped: %+v", out[0]) + } + if out[1].Quality != Quality2160p || out[1].ConnectionID != "c2" || out[1].Status != StatusCompleted || out[1].ExternalStatus != "imported" { + t.Fatalf("status[1] mismapped: %+v", out[1]) + } + if got := fc.lastCheckReq.GetTargets(); len(got) != 2 || + got[0].GetConnectionId() != "c1" || got[0].GetExternalId() != "7" || + got[1].GetConnectionId() != "c2" || got[1].GetExternalId() != "8" { + t.Fatalf("refs not forwarded: %+v", fc.lastCheckReq.GetTargets()) + } +} + +func TestPluginRouterProviderListConfigOptionsTranslates(t *testing.T) { + fc := &fakeRouterClient{optionsByField: map[string]*pluginv1.ConfigOptionList{ + "root_folder": {Options: []*pluginv1.ConfigOption{ + {Value: "/movies", Label: "Movies"}, + {Value: "/movies4k", Label: "Movies 4K"}, + }}, + "quality_profile_id": {Options: []*pluginv1.ConfigOption{ + {Value: "1", Label: "HD-1080p"}, + }}, + }} + p := NewPluginRouterProvider(fakeRouterResolver{c: fc}) + out, err := p.ListConfigOptions(context.Background(), 1, "arr", + ResolvedRouterConnection{ID: "c1", BaseURL: "http://r", APIKey: "k", Config: map[string]any{"service_kind": "radarr"}}) + if err != nil { + t.Fatalf("ListConfigOptions: %v", err) + } + if len(out) != 2 { + t.Fatalf("expected 2 fields, got %d: %+v", len(out), out) + } + rf := out["root_folder"] + if len(rf) != 2 || rf[0].Value != "/movies" || rf[0].Label != "Movies" || rf[1].Value != "/movies4k" || rf[1].Label != "Movies 4K" { + t.Fatalf("root_folder mismapped: %+v", rf) + } + qp := out["quality_profile_id"] + if len(qp) != 1 || qp[0].Value != "1" || qp[0].Label != "HD-1080p" { + t.Fatalf("quality_profile_id mismapped: %+v", qp) + } +} + +func TestPluginRouterProviderValidateTranslates(t *testing.T) { + fc := &fakeRouterClient{validateResp: &pluginv1.ValidateResponse{ + FieldErrors: map[string]string{"is_default": "cannot be 4K"}, FormError: "", + }} + p := NewPluginRouterProvider(fakeRouterResolver{c: fc}) + fe, form, err := p.Validate(context.Background(), 1, "arr", + ResolvedRouterConnection{ID: "c1", Config: map[string]any{"service_kind": "radarr"}}, + nil) + if err != nil || form != "" || fe["is_default"] != "cannot be 4K" { + t.Fatalf("unexpected: fe=%v form=%q err=%v", fe, form, err) + } +} diff --git a/internal/requests/radarr/client.go b/internal/requests/radarr/client.go deleted file mode 100644 index 7f6f7624..00000000 --- a/internal/requests/radarr/client.go +++ /dev/null @@ -1,176 +0,0 @@ -package radarr - -import ( - "context" - "fmt" - "net/http" - "net/url" - "strconv" - - mediarequests "github.com/Silo-Server/silo-server/internal/requests" - "github.com/Silo-Server/silo-server/internal/requests/arrclient" -) - -type Client struct { - httpClient *http.Client -} - -type movieResource struct { - ID int `json:"id,omitempty"` - Title string `json:"title,omitempty"` - TMDBID int `json:"tmdbId,omitempty"` - Year int `json:"year,omitempty"` - TitleSlug string `json:"titleSlug,omitempty"` - QualityProfileID int `json:"qualityProfileId,omitempty"` - RootFolderPath string `json:"rootFolderPath,omitempty"` - Monitored bool `json:"monitored"` - MinimumAvailability string `json:"minimumAvailability,omitempty"` - Tags []int `json:"tags,omitempty"` - AddOptions addMovieOptions `json:"addOptions,omitempty"` -} - -type addMovieOptions struct { - SearchForMovie bool `json:"searchForMovie"` - Monitor string `json:"monitor,omitempty"` -} - -func NewClient(httpClient *http.Client) *Client { - return &Client{httpClient: httpClient} -} - -func (c *Client) ListMovieIntegrationOptions(ctx context.Context, integration mediarequests.Integration) (*mediarequests.IntegrationOptions, error) { - client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient) - rootFolders, err := arrclient.ListRootFolders(ctx, client) - if err != nil { - return nil, err - } - qualityProfiles, err := arrclient.ListQualityProfiles(ctx, client) - if err != nil { - return nil, err - } - tags, err := arrclient.ListTags(ctx, client) - if err != nil { - return nil, err - } - return &mediarequests.IntegrationOptions{ - Kind: "radarr", - RootFolders: rootFolders, - QualityProfiles: qualityProfiles, - Tags: tags, - }, nil -} - -func (c *Client) SubmitMovie(ctx context.Context, req mediarequests.Request, integration mediarequests.Integration) (mediarequests.FulfillmentResult, error) { - if req.MediaType != mediarequests.MediaTypeMovie { - return mediarequests.FulfillmentResult{}, fmt.Errorf("radarr: request is not a movie") - } - if integration.QualityProfileID == nil { - return mediarequests.FulfillmentResult{}, fmt.Errorf("radarr: quality profile is required") - } - - client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient) - movie, err := c.lookupMovie(ctx, client, req.TMDBID) - if err != nil { - return mediarequests.FulfillmentResult{}, err - } - movie.RootFolderPath = integration.RootFolder - movie.QualityProfileID = *integration.QualityProfileID - movie.Monitored = arrclient.BoolOption(integration.Options, "monitored", true) - movie.MinimumAvailability = arrclient.StringOption(integration.Options, "minimum_availability", "released") - movie.Tags = integration.Tags - movie.AddOptions = addMovieOptions{ - SearchForMovie: arrclient.BoolOption( - integration.Options, - "search_for_movie", - arrclient.BoolOption(integration.Options, "search_on_add", true), - ), - Monitor: arrclient.StringOption(integration.Options, "monitor", "movieOnly"), - } - - var created movieResource - if err := client.PostJSON(ctx, "/api/v3/movie", movie, &created); err != nil { - if !arrclient.IsEmptyOrTruncatedDecodeError(err) { - return mediarequests.FulfillmentResult{}, err - } - // POST accepted but Radarr returned an empty body. Recover the - // new movie's Radarr ID by listing movies filtered by TMDB ID; - // without the ID the reconcile loop cannot advance the request. - if found, lookErr := c.findMovieByTMDBID(ctx, client, req.TMDBID); lookErr == nil && found.ID > 0 { - return resultFromMovie(found), nil - } - return arrclient.AcceptedWithoutResponse("radarr"), nil - } - return resultFromMovie(created), nil -} - -func (c *Client) findMovieByTMDBID(ctx context.Context, client *arrclient.Client, tmdbID int) (movieResource, error) { - values := url.Values{} - values.Set("tmdbId", strconv.Itoa(tmdbID)) - var matches []movieResource - if err := client.GetJSON(ctx, "/api/v3/movie?"+values.Encode(), &matches); err != nil { - return movieResource{}, err - } - for _, m := range matches { - if m.ID > 0 { - return m, nil - } - } - return movieResource{}, fmt.Errorf("radarr: movie not found after add for tmdb_id %d", tmdbID) -} - -func (c *Client) CheckMovieStatus(ctx context.Context, req mediarequests.Request, integration mediarequests.Integration) (mediarequests.FulfillmentStatus, error) { - client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient) - movieID, _ := strconv.Atoi(req.ExternalID) - if movieID <= 0 { - return mediarequests.FulfillmentStatus{ - Status: mediarequests.StatusQueued, - IntegrationKind: "radarr", - ExternalStatus: "external_id_unavailable", - }, nil - } - - queues, err := c.queueDetails(ctx, client, movieID) - if err != nil { - return mediarequests.FulfillmentStatus{}, err - } - evaluation := arrclient.EvaluateQueue(queues) - return arrclient.StatusFromQueueEvaluation("radarr", movieID, evaluation), nil -} - -func (c *Client) lookupMovie(ctx context.Context, client *arrclient.Client, tmdbID int) (movieResource, error) { - values := url.Values{} - values.Set("tmdbId", strconv.Itoa(tmdbID)) - // Radarr's /api/v3/movie/lookup/tmdb returns a single MovieResource, unlike - // /api/v3/movie/lookup which returns an array. Missing IDs return non-2xx - // (handled as HTTPError upstream), so a successful response is the movie. - var movie movieResource - if err := client.GetJSON(ctx, "/api/v3/movie/lookup/tmdb?"+values.Encode(), &movie); err != nil { - return movieResource{}, err - } - if movie.TMDBID == 0 { - movie.TMDBID = tmdbID - } - return movie, nil -} - -func (c *Client) queueDetails(ctx context.Context, client *arrclient.Client, movieID int) ([]arrclient.QueueResource, error) { - values := url.Values{} - values.Set("movieId", strconv.Itoa(movieID)) - var queues []arrclient.QueueResource - if err := client.GetJSON(ctx, "/api/v3/queue/details?"+values.Encode(), &queues); err != nil { - return nil, err - } - return queues, nil -} - -func resultFromMovie(movie movieResource) mediarequests.FulfillmentResult { - externalID := "" - if movie.ID > 0 { - externalID = strconv.Itoa(movie.ID) - } - return mediarequests.FulfillmentResult{ - IntegrationKind: "radarr", - ExternalID: externalID, - ExternalStatus: "queued", - } -} diff --git a/internal/requests/radarr/client_test.go b/internal/requests/radarr/client_test.go deleted file mode 100644 index 63e11d0b..00000000 --- a/internal/requests/radarr/client_test.go +++ /dev/null @@ -1,236 +0,0 @@ -package radarr - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - mediarequests "github.com/Silo-Server/silo-server/internal/requests" -) - -func TestSubmitMovieAddsLookupResult(t *testing.T) { - qualityProfileID := 7 - var posted movieResource - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Api-Key"); got != "radarr-key" { - t.Fatalf("X-Api-Key = %q, want radarr-key", got) - } - switch r.URL.Path { - case "/api/v3/movie": - if r.Method == http.MethodGet { - if got := r.URL.Query().Get("tmdbId"); got != "550" { - t.Fatalf("tmdbId = %q, want 550", got) - } - w.Write([]byte(`[]`)) - return - } - if r.Method == http.MethodPost { - if err := json.NewDecoder(r.Body).Decode(&posted); err != nil { - t.Fatalf("decode posted movie: %v", err) - } - w.Write([]byte(`{"id":42,"tmdbId":550}`)) - return - } - case "/api/v3/movie/lookup/tmdb": - if got := r.URL.Query().Get("tmdbId"); got != "550" { - t.Fatalf("lookup tmdbId = %q, want 550", got) - } - w.Write([]byte(`{"title":"Fight Club","tmdbId":550,"titleSlug":"fight-club"}`)) - return - } - t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) - })) - defer server.Close() - - client := NewClient(server.Client()) - result, err := client.SubmitMovie(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeMovie, - TMDBID: 550, - Title: "Fight Club", - }, mediarequests.Integration{ - Kind: "radarr", - BaseURL: server.URL, - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - Options: map[string]any{ - "search_on_add": false, - }, - }) - if err != nil { - t.Fatalf("SubmitMovie returned error: %v", err) - } - if result.ExternalID != "42" || result.IntegrationKind != "radarr" { - t.Fatalf("result = %+v, want radarr external id 42", result) - } - if posted.RootFolderPath != "/movies" || posted.QualityProfileID != qualityProfileID { - t.Fatalf("posted movie = %+v, missing root folder/quality profile", posted) - } - if posted.AddOptions.SearchForMovie { - t.Fatalf("searchForMovie = true, want false") - } -} - -func TestSubmitMovieRecoversFromEmptyAddResponse(t *testing.T) { - qualityProfileID := 7 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/v3/movie/lookup/tmdb": - w.Write([]byte(`{"title":"Fight Club","tmdbId":550,"titleSlug":"fight-club"}`)) - case "/api/v3/movie": - if r.Method == http.MethodPost { - w.WriteHeader(http.StatusCreated) - return - } - if r.Method == http.MethodGet { - if got := r.URL.Query().Get("tmdbId"); got != "550" { - t.Fatalf("tmdbId = %q, want 550", got) - } - w.Write([]byte(`[{"id":99,"tmdbId":550,"title":"Fight Club"}]`)) - return - } - default: - t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) - } - })) - defer server.Close() - - client := NewClient(server.Client()) - result, err := client.SubmitMovie(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeMovie, - TMDBID: 550, - Title: "Fight Club", - }, mediarequests.Integration{ - Kind: "radarr", - BaseURL: server.URL, - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }) - if err != nil { - t.Fatalf("SubmitMovie returned error: %v", err) - } - if result.ExternalID != "99" { - t.Fatalf("ExternalID = %q, want 99 (recovered after empty 201)", result.ExternalID) - } - if result.ExternalStatus != "queued" { - t.Fatalf("ExternalStatus = %q, want queued", result.ExternalStatus) - } -} - -func TestSubmitMovieFallsBackWhenEmptyResponseAndLookupFails(t *testing.T) { - qualityProfileID := 7 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/v3/movie/lookup/tmdb": - w.Write([]byte(`{"title":"Fight Club","tmdbId":550,"titleSlug":"fight-club"}`)) - case "/api/v3/movie": - if r.Method == http.MethodPost { - w.WriteHeader(http.StatusCreated) - return - } - if r.Method == http.MethodGet { - w.Write([]byte(`[]`)) - return - } - default: - t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) - } - })) - defer server.Close() - - client := NewClient(server.Client()) - result, err := client.SubmitMovie(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeMovie, - TMDBID: 550, - Title: "Fight Club", - }, mediarequests.Integration{ - Kind: "radarr", - BaseURL: server.URL, - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }) - if err != nil { - t.Fatalf("SubmitMovie returned error: %v", err) - } - if result.ExternalStatus != "accepted_without_response" { - t.Fatalf("ExternalStatus = %q, want accepted_without_response", result.ExternalStatus) - } -} - -func TestCheckMovieStatusReadsQueueDetails(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Api-Key"); got != "radarr-key" { - t.Fatalf("X-Api-Key = %q, want radarr-key", got) - } - if r.URL.Path != "/api/v3/queue/details" { - t.Fatalf("unexpected path %s", r.URL.Path) - } - if got := r.URL.Query().Get("movieId"); got != "42" { - t.Fatalf("movieId = %q, want 42", got) - } - w.Write([]byte(`[{"movieId":42,"status":"downloading","trackedDownloadState":"downloading"}]`)) - })) - defer server.Close() - - client := NewClient(server.Client()) - status, err := client.CheckMovieStatus(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeMovie, - TMDBID: 550, - ExternalID: "42", - }, mediarequests.Integration{ - Kind: "radarr", - BaseURL: server.URL, - APIKeyRef: "radarr-key", - }) - if err != nil { - t.Fatalf("CheckMovieStatus returned error: %v", err) - } - if status.Status != mediarequests.StatusDownloading || status.ExternalStatus != "downloading/downloading" { - t.Fatalf("status = %+v, want downloading", status) - } -} - -func TestListMovieIntegrationOptionsLoadsChoices(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Api-Key"); got != "radarr-key" { - t.Fatalf("X-Api-Key = %q, want radarr-key", got) - } - switch r.URL.Path { - case "/api/v3/rootfolder": - w.Write([]byte(`[{"path":"/movies","freeSpace":123,"totalSpace":456,"accessible":true}]`)) - case "/api/v3/qualityprofile": - w.Write([]byte(`[{"id":7,"name":"HD-1080p"}]`)) - case "/api/v3/tag": - w.Write([]byte(`[{"id":2,"label":"requests"}]`)) - default: - t.Fatalf("unexpected path %s", r.URL.Path) - } - })) - defer server.Close() - - client := NewClient(server.Client()) - options, err := client.ListMovieIntegrationOptions(context.Background(), mediarequests.Integration{ - Kind: "radarr", - BaseURL: server.URL, - APIKeyRef: "radarr-key", - }) - if err != nil { - t.Fatalf("ListMovieIntegrationOptions returned error: %v", err) - } - if options.Kind != "radarr" { - t.Fatalf("kind = %q, want radarr", options.Kind) - } - if len(options.RootFolders) != 1 || options.RootFolders[0].Path != "/movies" { - t.Fatalf("root folders = %+v, want /movies", options.RootFolders) - } - if len(options.QualityProfiles) != 1 || options.QualityProfiles[0].ID != 7 { - t.Fatalf("quality profiles = %+v, want id 7", options.QualityProfiles) - } - if len(options.Tags) != 1 || options.Tags[0].ID != 2 { - t.Fatalf("tags = %+v, want id 2", options.Tags) - } -} diff --git a/internal/requests/repository.go b/internal/requests/repository.go index 3a932451..89ceae42 100644 --- a/internal/requests/repository.go +++ b/internal/requests/repository.go @@ -26,14 +26,10 @@ func NewRepository(pool *pgxpool.Pool, cipher *secret.Cipher) *Repository { return &Repository{pool: pool, cipher: cipher} } -// apiKeyAAD binds a request_integrations api_key_ref ciphertext to its row. func apiKeyAAD(id string) string { return secret.RowAAD("request_integrations", "api_key_ref", id) } -// encryptAPIKey encrypts a non-empty, trimmed API key bound to the integration -// id. An empty key returns "" so the update path's keep-existing CASE sentinel -// still fires (and an absent key stays absent). func (r *Repository) encryptAPIKey(id, apiKey string) (string, error) { apiKey = strings.TrimSpace(apiKey) if apiKey == "" { @@ -471,13 +467,12 @@ func (r *Repository) SetOutcome(ctx context.Context, id string, outcome Outcome, return req, nil } -const integrationColumns = `id, kind, name, enabled, base_url, api_key_ref, - root_folder, quality_profile_id, tags, is_4k, is_default, is_default_4k, - anime_enabled, anime_quality_profile_id, anime_root_folder, anime_tags, - options, last_check_at, last_check_status, last_check_error, updated_at` +const integrationColumns = `id, name, enabled, base_url, api_key_ref, + last_check_at, last_check_status, last_check_error, updated_at, + capability_id, installation_id, supported_media_types, plugin_config` func (r *Repository) ListIntegrations(ctx context.Context) ([]Integration, error) { - rows, err := r.pool.Query(ctx, `SELECT `+integrationColumns+` FROM request_integrations ORDER BY kind, name`) + rows, err := r.pool.Query(ctx, `SELECT `+integrationColumns+` FROM request_integrations ORDER BY name`) if err != nil { return nil, fmt.Errorf("list request integrations: %w", err) } @@ -522,12 +517,20 @@ func (r *Repository) UpdateIntegration(ctx context.Context, i Integration) (*Int // tx) so the same SQL is reused by the plain create path and the transactional // SaveIntegrationWithDefaults path. func (r *Repository) insertIntegration(ctx context.Context, exec requestExecutor, i Integration) (*Integration, error) { - if i.Options == nil { - i.Options = map[string]any{} + if i.PluginConfig == nil { + i.PluginConfig = map[string]any{} } - options, err := json.Marshal(i.Options) + pluginConfig, err := json.Marshal(i.PluginConfig) if err != nil { - return nil, fmt.Errorf("marshal options: %w", err) + return nil, fmt.Errorf("marshal plugin config: %w", err) + } + // capability_id is the capability sub-id ("arr"/"seerr"), validated non-empty + // upstream in validateInstance; persist it verbatim (never default it to the + // capability type, which the plugin runtime can't resolve). + capabilityID := strings.TrimSpace(i.CapabilityID) + supportedMediaTypes := i.SupportedMediaTypes + if supportedMediaTypes == nil { + supportedMediaTypes = []string{} } apiKeyRef, err := r.encryptAPIKey(i.ID, i.APIKeyRef) if err != nil { @@ -535,17 +538,13 @@ func (r *Repository) insertIntegration(ctx context.Context, exec requestExecutor } row := exec.QueryRow(ctx, ` INSERT INTO request_integrations ( - id, kind, name, enabled, base_url, api_key_ref, root_folder, - quality_profile_id, tags, is_4k, is_default, is_default_4k, - anime_enabled, anime_quality_profile_id, anime_root_folder, anime_tags, - options, updated_at) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17, now()) + id, name, enabled, base_url, api_key_ref, + capability_id, installation_id, supported_media_types, + plugin_config, updated_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9, now()) RETURNING `+integrationColumns, - i.ID, i.Kind, strings.TrimSpace(i.Name), i.Enabled, strings.TrimSpace(i.BaseURL), - apiKeyRef, strings.TrimSpace(i.RootFolder), i.QualityProfileID, - int32Slice(i.Tags), i.Is4K, i.IsDefault, i.IsDefault4K, i.AnimeEnabled, - i.AnimeQualityProfileID, strings.TrimSpace(i.AnimeRootFolder), int32Slice(i.AnimeTags), - options) + i.ID, strings.TrimSpace(i.Name), i.Enabled, strings.TrimSpace(i.BaseURL), + apiKeyRef, capabilityID, i.InstallationID, supportedMediaTypes, pluginConfig) out, err := r.scanIntegration(row) if err != nil { return nil, fmt.Errorf("create request integration: %w", err) @@ -556,12 +555,20 @@ func (r *Repository) insertIntegration(ctx context.Context, exec requestExecutor // updateIntegration runs the integration UPDATE against any executor (pool or // tx) so the plain update path and SaveIntegrationWithDefaults share the SQL. func (r *Repository) updateIntegration(ctx context.Context, exec requestExecutor, i Integration) (*Integration, error) { - if i.Options == nil { - i.Options = map[string]any{} + if i.PluginConfig == nil { + i.PluginConfig = map[string]any{} } - options, err := json.Marshal(i.Options) + pluginConfig, err := json.Marshal(i.PluginConfig) if err != nil { - return nil, fmt.Errorf("marshal options: %w", err) + return nil, fmt.Errorf("marshal plugin config: %w", err) + } + // capability_id is the capability sub-id ("arr"/"seerr"), validated non-empty + // upstream in validateInstance; persist it verbatim (never default it to the + // capability type, which the plugin runtime can't resolve). + capabilityID := strings.TrimSpace(i.CapabilityID) + supportedMediaTypes := i.SupportedMediaTypes + if supportedMediaTypes == nil { + supportedMediaTypes = []string{} } // Encrypt the incoming key; an empty result preserves the keep-existing CASE // (a blank edit leaves the stored key untouched). @@ -573,17 +580,12 @@ func (r *Repository) updateIntegration(ctx context.Context, exec requestExecutor UPDATE request_integrations SET name=$2, enabled=$3, base_url=$4, api_key_ref = CASE WHEN $5 = '' THEN api_key_ref ELSE $5 END, - root_folder=$6, quality_profile_id=$7, tags=$8, is_4k=$9, - is_default=$10, is_default_4k=$11, anime_enabled=$12, - anime_quality_profile_id=$13, anime_root_folder=$14, anime_tags=$15, - options=$16, updated_at=now() + capability_id=$6, installation_id=$7, + supported_media_types=$8, plugin_config=$9, updated_at=now() WHERE id=$1 RETURNING `+integrationColumns, i.ID, strings.TrimSpace(i.Name), i.Enabled, strings.TrimSpace(i.BaseURL), - apiKeyRef, strings.TrimSpace(i.RootFolder), i.QualityProfileID, - int32Slice(i.Tags), i.Is4K, i.IsDefault, i.IsDefault4K, i.AnimeEnabled, - i.AnimeQualityProfileID, strings.TrimSpace(i.AnimeRootFolder), int32Slice(i.AnimeTags), - options) + apiKeyRef, capabilityID, i.InstallationID, supportedMediaTypes, pluginConfig) out, err := r.scanIntegration(row) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -594,9 +596,10 @@ func (r *Repository) updateIntegration(ctx context.Context, exec requestExecutor return &out, nil } -// SaveIntegrationWithDefaults clears the conflicting kind default(s) and -// creates/updates the instance in a single transaction so a save failure can -// never leave the kind with zero defaults. +// SaveIntegrationWithDefaults creates/updates the instance in a single +// transaction. The host no longer enforces a single HD/4K default per kind; +// the request_router plugin's RouteTargets picks the first is_default +// connection from plugin_config, so the save path is a plain insert-or-update. func (r *Repository) SaveIntegrationWithDefaults(ctx context.Context, in Integration, isCreate bool) (*Integration, error) { tx, err := r.pool.Begin(ctx) if err != nil { @@ -604,17 +607,6 @@ func (r *Repository) SaveIntegrationWithDefaults(ctx context.Context, in Integra } defer tx.Rollback(ctx) - if in.IsDefault { - if err := r.ClearDefault(ctx, tx, in.Kind, false, in.ID); err != nil { - return nil, err - } - } - if in.IsDefault4K { - if err := r.ClearDefault(ctx, tx, in.Kind, true, in.ID); err != nil { - return nil, err - } - } - var out *Integration if isCreate { out, err = r.insertIntegration(ctx, tx, in) @@ -669,21 +661,6 @@ func (r *Repository) DeleteIntegration(ctx context.Context, id string) error { return nil } -// ClearDefault unsets the HD (or 4K) default flag for every instance of a kind -// except excludeID, so saving an instance that is itself the new default does -// not clear its own freshly-written flag. -func (r *Repository) ClearDefault(ctx context.Context, exec requestExecutor, kind string, fourK bool, excludeID string) error { - col := "is_default" - if fourK { - col = "is_default_4k" - } - _, err := exec.Exec(ctx, `UPDATE request_integrations SET `+col+` = false WHERE kind = $1 AND id <> $2`, kind, excludeID) - if err != nil { - return fmt.Errorf("clear default: %w", err) - } - return nil -} - func (r *Repository) recordEvent(ctx context.Context, exec requestExecutor, requestID, eventType string, actor Viewer, message string) error { var actorUserID any if actor.UserID > 0 { @@ -794,15 +771,13 @@ type integrationScanner interface { func (r *Repository) scanIntegration(row integrationScanner) (Integration, error) { var i Integration - var quality, animeQuality sql.NullInt64 - var tags, animeTags []int32 - var optionsRaw []byte + var installationID sql.NullInt64 + var pluginConfigRaw []byte var lastCheckAt sql.NullTime if err := row.Scan( - &i.ID, &i.Kind, &i.Name, &i.Enabled, &i.BaseURL, &i.APIKeyRef, - &i.RootFolder, &quality, &tags, &i.Is4K, &i.IsDefault, &i.IsDefault4K, - &i.AnimeEnabled, &animeQuality, &i.AnimeRootFolder, &animeTags, - &optionsRaw, &lastCheckAt, &i.LastCheckStatus, &i.LastCheckError, &i.UpdatedAt, + &i.ID, &i.Name, &i.Enabled, &i.BaseURL, &i.APIKeyRef, + &lastCheckAt, &i.LastCheckStatus, &i.LastCheckError, &i.UpdatedAt, + &i.CapabilityID, &installationID, &i.SupportedMediaTypes, &pluginConfigRaw, ); err != nil { return Integration{}, err } @@ -814,42 +789,20 @@ func (r *Repository) scanIntegration(row integrationScanner) (Integration, error return Integration{}, fmt.Errorf("decrypt request integration %s api key: %w", i.ID, err) } i.APIKeyRef = apiKey - if quality.Valid { - v := int(quality.Int64) - i.QualityProfileID = &v + if installationID.Valid { + v := int(installationID.Int64) + i.InstallationID = &v } - if animeQuality.Valid { - v := int(animeQuality.Int64) - i.AnimeQualityProfileID = &v - } - i.Tags = intsFromInt32(tags) - i.AnimeTags = intsFromInt32(animeTags) - if len(optionsRaw) > 0 { - if err := json.Unmarshal(optionsRaw, &i.Options); err != nil { - return Integration{}, fmt.Errorf("unmarshal request integration options for %s: %w", i.ID, err) + if len(pluginConfigRaw) > 0 { + if err := json.Unmarshal(pluginConfigRaw, &i.PluginConfig); err != nil { + return Integration{}, fmt.Errorf("unmarshal request integration plugin config for %s: %w", i.ID, err) } } - if i.Options == nil { - i.Options = map[string]any{} + if i.PluginConfig == nil { + i.PluginConfig = map[string]any{} } if lastCheckAt.Valid { i.LastCheckAt = &lastCheckAt.Time } return i, nil } - -func int32Slice(values []int) []int32 { - out := make([]int32, 0, len(values)) - for _, value := range values { - out = append(out, int32(value)) - } - return out -} - -func intsFromInt32(values []int32) []int { - out := make([]int, 0, len(values)) - for _, value := range values { - out = append(out, int(value)) - } - return out -} diff --git a/internal/requests/repository_crypto_test.go b/internal/requests/repository_crypto_test.go new file mode 100644 index 00000000..315ee68e --- /dev/null +++ b/internal/requests/repository_crypto_test.go @@ -0,0 +1,59 @@ +package requests + +import ( + "testing" + + "github.com/Silo-Server/silo-server/internal/secret" +) + +// TestEncryptAPIKeyRoundTripAndAAD guards the security-critical invariants of the +// at-rest credential cipher adopted from #95: encryptAPIKey must produce +// ciphertext that DecryptIfEncrypted inverts under the SAME id-bound AAD that +// secret's backfill writes, a blank key must encrypt to "" (the keep-existing +// update sentinel), and the AAD must be row-bound. A drift in apiKeyAAD or the +// envelope would silently corrupt or leak every stored connection api key with no +// other test catching it (there is no DB harness for scanIntegration). +func TestEncryptAPIKeyRoundTripAndAAD(t *testing.T) { + cipher, err := secret.New([]byte("unit-test-master-key-with-at-least-32-bytes")) + if err != nil { + t.Fatalf("secret.New: %v", err) + } + r := &Repository{cipher: cipher} + + const id = "integ-1" + const key = "00178d9a3893480cbf122d29f7aca1a0" + + enc, err := r.encryptAPIKey(id, key) + if err != nil { + t.Fatalf("encryptAPIKey: %v", err) + } + if enc == "" || enc == key { + t.Fatalf("encryptAPIKey did not encrypt: %q", enc) + } + + // The AAD must equal what secret.BackfillReferencedColumns wrote, or rows + // encrypted by the startup backfill fail to decrypt on read. + if got, want := apiKeyAAD(id), secret.RowAAD("request_integrations", "api_key_ref", id); got != want { + t.Fatalf("apiKeyAAD = %q, want %q (must match #95 backfill)", got, want) + } + + dec, err := cipher.DecryptIfEncrypted(enc, apiKeyAAD(id)) + if err != nil { + t.Fatalf("DecryptIfEncrypted: %v", err) + } + if dec != key { + t.Fatalf("round-trip = %q, want %q", dec, key) + } + + // A blank/whitespace key encrypts to "" so updateIntegration's + // CASE WHEN $5 = '' sentinel preserves the stored ciphertext. + if blank, err := r.encryptAPIKey(id, " "); err != nil || blank != "" { + t.Fatalf("encryptAPIKey(blank) = %q, %v; want \"\", nil (keep-existing sentinel)", blank, err) + } + + // AAD is row-bound: ciphertext from one row must not authenticate under + // another row's id. + if _, err := cipher.DecryptIfEncrypted(enc, apiKeyAAD("integ-2")); err == nil { + t.Fatal("ciphertext decrypted under a different id's AAD — AAD is not row-bound") + } +} diff --git a/internal/requests/routing.go b/internal/requests/routing.go deleted file mode 100644 index 918957b7..00000000 --- a/internal/requests/routing.go +++ /dev/null @@ -1,85 +0,0 @@ -package requests - -import "github.com/Silo-Server/silo-server/internal/access" - -// plannedTarget is a routing decision: which instance, at which quality, with -// which profile/folder/tags resolved (standard vs anime). -type plannedTarget struct { - Instance Integration - Quality Quality - IsAnime bool -} - -func integrationKindForMediaType(mediaType MediaType) string { - if mediaType == MediaTypeSeries { - return "sonarr" - } - return "radarr" -} - -// routeTargets decides the fulfillment targets for an approved request. -// 1080p is always desired; 2160p is added when the requester's ceiling allows -// 4K OR force-dual is on. A quality is emitted only if its default instance -// exists for the kind. -func routeTargets(req Request, ceiling string, settings Settings, instances []Integration) []plannedTarget { - kind := integrationKindForMediaType(req.MediaType) - - wants4K := settings.ForceDualQuality || access.CompareQuality(ceiling, access.PlaybackQuality4K) >= 0 - - var hd, uhd *Integration - for i := range instances { - in := instances[i] - if in.Kind != kind || !in.Enabled { - continue - } - if in.IsDefault && hd == nil { - hd = &instances[i] - } - if in.IsDefault4K && uhd == nil { - uhd = &instances[i] - } - } - - var out []plannedTarget - if hd != nil { - out = append(out, plannedTarget{Instance: *hd, Quality: Quality1080p, IsAnime: req.IsAnime && hd.AnimeEnabled}) - } - if wants4K && uhd != nil { - out = append(out, plannedTarget{Instance: *uhd, Quality: Quality2160p, IsAnime: req.IsAnime && uhd.AnimeEnabled}) - } - return out -} - -// resolveInstance returns a copy of the instance with root folder / quality -// profile / tags (and Sonarr series_type) set for standard vs anime fulfillment. -func resolveInstance(pt plannedTarget) Integration { - in := pt.Instance - if in.Options == nil { - in.Options = map[string]any{} - } else { - clone := make(map[string]any, len(in.Options)) - for k, v := range in.Options { - clone[k] = v - } - in.Options = clone - } - if pt.IsAnime { - // Anime fields are overrides: only replace the standard value when the - // anime counterpart is set, so an admin can enable anime detection while - // reusing the standard root folder / quality profile / tags for any field - // they leave blank (rather than clearing them into an invalid submission). - if in.AnimeRootFolder != "" { - in.RootFolder = in.AnimeRootFolder - } - if in.AnimeQualityProfileID != nil { - in.QualityProfileID = in.AnimeQualityProfileID - } - if len(in.AnimeTags) > 0 { - in.Tags = in.AnimeTags - } - if in.Kind == "sonarr" { - in.Options["series_type"] = "anime" - } - } - return in -} diff --git a/internal/requests/routing_test.go b/internal/requests/routing_test.go deleted file mode 100644 index 8e855154..00000000 --- a/internal/requests/routing_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package requests - -import "testing" - -func inst(kind, id string, def, def4k, anime bool) Integration { - qp := 1 - return Integration{ - ID: id, Kind: kind, Name: id, Enabled: true, BaseURL: "http://x", - APIKeyRef: "k", RootFolder: "/std", QualityProfileID: &qp, - Is4K: def4k, IsDefault: def, IsDefault4K: def4k, AnimeEnabled: anime, - } -} - -func TestRouteTargets(t *testing.T) { - hd := inst("radarr", "hd", true, false, false) - uhd := inst("radarr", "uhd", false, true, false) - hdAnime := inst("radarr", "hda", true, false, true) - - cases := []struct { - name string - req Request - ceiling string - force bool - instances []Integration - want []Quality - wantAnime bool - }{ - {"hd only, sd user", Request{MediaType: MediaTypeMovie}, "1080p", false, []Integration{hd, uhd}, []Quality{Quality1080p}, false}, - {"4k user dual", Request{MediaType: MediaTypeMovie}, "2160p", false, []Integration{hd, uhd}, []Quality{Quality1080p, Quality2160p}, false}, - {"force dual overrides role", Request{MediaType: MediaTypeMovie}, "1080p", true, []Integration{hd, uhd}, []Quality{Quality1080p, Quality2160p}, false}, - {"4k user but no 4k default", Request{MediaType: MediaTypeMovie}, "2160p", false, []Integration{hd}, []Quality{Quality1080p}, false}, - {"no hd default", Request{MediaType: MediaTypeMovie}, "2160p", false, []Integration{uhd}, []Quality{Quality2160p}, false}, - {"anime on anime-enabled hd", Request{MediaType: MediaTypeMovie, IsAnime: true}, "1080p", false, []Integration{hdAnime}, []Quality{Quality1080p}, true}, - {"empty ceiling hd only", Request{MediaType: MediaTypeMovie}, "", false, []Integration{hd, uhd}, []Quality{Quality1080p}, false}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := routeTargets(tc.req, tc.ceiling, Settings{ForceDualQuality: tc.force}, tc.instances) - if len(got) != len(tc.want) { - t.Fatalf("got %d targets, want %d (%v)", len(got), len(tc.want), got) - } - for i, q := range tc.want { - if got[i].Quality != q { - t.Fatalf("target %d quality = %s, want %s", i, got[i].Quality, q) - } - if got[i].IsAnime != tc.wantAnime { - t.Fatalf("target %d isAnime = %v, want %v", i, got[i].IsAnime, tc.wantAnime) - } - } - }) - } -} diff --git a/internal/requests/service.go b/internal/requests/service.go index d8d3f986..9bd57125 100644 --- a/internal/requests/service.go +++ b/internal/requests/service.go @@ -27,30 +27,6 @@ type TMDBExternalIDClient interface { const externalIDHydrationConcurrency = 4 -type MovieFulfillmentAdapter interface { - SubmitMovie(ctx context.Context, req Request, integration Integration) (FulfillmentResult, error) -} - -type SeriesFulfillmentAdapter interface { - SubmitSeries(ctx context.Context, req Request, integration Integration) (FulfillmentResult, error) -} - -type MovieStatusAdapter interface { - CheckMovieStatus(ctx context.Context, req Request, integration Integration) (FulfillmentStatus, error) -} - -type SeriesStatusAdapter interface { - CheckSeriesStatus(ctx context.Context, req Request, integration Integration) (FulfillmentStatus, error) -} - -type MovieIntegrationOptionsAdapter interface { - ListMovieIntegrationOptions(ctx context.Context, integration Integration) (*IntegrationOptions, error) -} - -type SeriesIntegrationOptionsAdapter interface { - ListSeriesIntegrationOptions(ctx context.Context, integration Integration) (*IntegrationOptions, error) -} - type EntitlementResolver interface { // MaxPlaybackQuality returns the requester's effective playback-quality // ceiling (already combining account- and profile-level caps). Empty string @@ -58,14 +34,20 @@ type EntitlementResolver interface { MaxPlaybackQuality(ctx context.Context, userID int, profileID string) (string, error) } +// RequesterIdentityResolver resolves a requesting user id into the identity a +// per-user request_router plugin needs (e.g. Seerr attribution by email). +type RequesterIdentityResolver interface { + ResolveRequester(ctx context.Context, userID int) (email, username string, err error) +} + type Service struct { - store Store - tmdb TMDBClient - presence PresenceResolver - movieAdapter MovieFulfillmentAdapter - seriesAdapter SeriesFulfillmentAdapter - entitlements EntitlementResolver - Now func() time.Time + store Store + tmdb TMDBClient + presence PresenceResolver + router RequestRouterProvider + entitlements EntitlementResolver + requesterIdentity RequesterIdentityResolver + Now func() time.Time } type DiscoverySection struct { @@ -86,13 +68,28 @@ func NewService(store Store, tmdbClient TMDBClient, presence PresenceResolver) * } } -func (s *Service) SetFulfillmentAdapters(movie MovieFulfillmentAdapter, series SeriesFulfillmentAdapter) { - s.movieAdapter = movie - s.seriesAdapter = series -} +func (s *Service) SetRouterProvider(p RequestRouterProvider) { s.router = p } func (s *Service) SetEntitlementResolver(r EntitlementResolver) { s.entitlements = r } +func (s *Service) SetRequesterIdentityResolver(r RequesterIdentityResolver) { + s.requesterIdentity = r +} + +// populateRequesterIdentity fills req.RequesterEmail/Username from the resolver. +// Nil resolver or any error leaves them empty (the plugin then behaves as admin). +func (s *Service) populateRequesterIdentity(ctx context.Context, req *Request) { + if s.requesterIdentity == nil || req.RequestedByUserID <= 0 { + return + } + email, username, err := s.requesterIdentity.ResolveRequester(ctx, req.RequestedByUserID) + if err != nil { + slog.WarnContext(ctx, "requests: requester identity resolve failed; attributing to admin", "user_id", req.RequestedByUserID, "error", err) + return + } + req.RequesterEmail, req.RequesterUsername = email, username +} + func (s *Service) requesterCeiling(ctx context.Context, userID int, profileID string) string { if s.entitlements == nil { return "" // no resolver -> unlimited (1080p baseline still applies) @@ -104,6 +101,94 @@ func (s *Service) requesterCeiling(ctx context.Context, userID int, profileID st return q } +// allowedQualities returns the qualities a request may receive: 1080p always, +// plus 2160p when force-dual is on or the requester's entitlement ceiling allows 4K. +func (s *Service) allowedQualities(ctx context.Context, req Request, settings Settings) []Quality { + out := []Quality{Quality1080p} + ceiling := s.requesterCeiling(ctx, req.RequestedByUserID, req.RequestedByProfileID) + // QualityAllowed treats an empty ceiling as "no cap" (the "Any" preset), so a + // requester with unlimited playback quality correctly gets 4K. A raw + // CompareQuality would rank "" as the LOWEST quality and wrongly drop 4K. + if settings.ForceDualQuality || access.QualityAllowed(access.PlaybackQuality4K, ceiling) { + out = append(out, Quality2160p) + } + return out +} + +// fulfillContext caches the global fulfillment inputs for one reconcile cycle +// (or a single Approve/Retry) so integrations and settings are fetched once +// instead of per request. API keys need no cache here: the repository decrypts +// api_key_ref on read, so Integration.APIKeyRef already holds the literal key. +type fulfillContext struct { + integrations []Integration + settings Settings +} + +func (s *Service) newFulfillContext(ctx context.Context) (*fulfillContext, error) { + integrations, err := s.store.ListIntegrations(ctx) + if err != nil { + return nil, err + } + settings, err := s.store.GetSettings(ctx) + if err != nil { + return nil, err + } + return &fulfillContext{integrations: integrations, settings: settings}, nil +} + +// resolveRouterConnections turns enabled request_router integrations that serve +// the given media type into ResolvedRouterConnections (api key resolved to +// plaintext, plugin_config attached), and returns the installation+capability to +// dispatch to. +// +// It filters by media type to match the integrationConfigured auto-approve gate +// (so a series-only connection is never used for a movie request). Multi- +// installation routing isn't supported yet: it picks the first eligible +// connection's installation and includes ONLY connections belonging to it, so a +// second installation's resolved plaintext credentials are never handed to the +// first plugin. A connection whose api key cannot be resolved (or resolves empty) +// is skipped rather than aborting the whole request — a sibling healthy +// connection can still fulfill it, and an unauthenticated request is never sent. +func (s *Service) resolveRouterConnections(ctx context.Context, fc *fulfillContext, mediaType MediaType) ([]ResolvedRouterConnection, int, string, error) { + var conns []ResolvedRouterConnection + installationID, capabilityID := 0, "" + chosen := false + for _, in := range fc.integrations { + if !eligibleRouterConnection(in, mediaType) { + continue + } + // Contain to the first chosen (installation, capability): a plugin may + // expose more than one request_router capability, and a connection of a + // different capability must never be handed to the chosen one. + if chosen && (*in.InstallationID != installationID || in.CapabilityID != capabilityID) { + continue + } + // in.APIKeyRef was decrypted by the repo on read; empty means unconfigured. + apiKey := strings.TrimSpace(in.APIKeyRef) + if apiKey == "" { + slog.WarnContext(ctx, "requests: skipping router connection with no api key", "connection_id", in.ID) + continue + } + // Lock on the first SUCCESSFULLY resolved connection so a skipped + // bad-key connection never pins the installation/capability. + if !chosen { + installationID, capabilityID, chosen = *in.InstallationID, in.CapabilityID, true + } + conns = append(conns, ResolvedRouterConnection{ID: in.ID, BaseURL: in.BaseURL, APIKey: apiKey, Config: in.PluginConfig}) + } + return conns, installationID, capabilityID, nil +} + +// eligibleRouterConnection reports whether a connection is a candidate fulfillment +// backend for the media type: enabled, bound to an installation, and naming a +// capability sub-id that serves the media type. resolveRouterConnections (which +// then resolves credentials) and integrationConfigured (the auto-approval gate) +// share this predicate so the two cannot drift. +func eligibleRouterConnection(in Integration, mediaType MediaType) bool { + return in.Enabled && in.CapabilityID != "" && in.InstallationID != nil && + integrationSupportsMediaType(in, mediaType) +} + func (s *Service) Search(ctx context.Context, viewer Viewer, query string, mediaType MediaType, page int) (*MediaPage, error) { if s == nil || s.store == nil || s.tmdb == nil { return nil, fmt.Errorf("request service is not configured") @@ -371,7 +456,7 @@ func (s *Service) CreateRequest(ctx context.Context, viewer Viewer, input Create return nil, err } if req.Status == StatusApproved { - return s.submitApprovedRequest(ctx, *req, viewer) + return s.submitApprovedRequest(ctx, *req, viewer, nil) } return req, nil } @@ -507,7 +592,7 @@ func (s *Service) Approve(ctx context.Context, viewer Viewer, id string) (*Reque if err != nil { return nil, err } - return s.submitApprovedRequest(ctx, *approved, viewer) + return s.submitApprovedRequest(ctx, *approved, viewer, nil) } func (s *Service) Decline(ctx context.Context, viewer Viewer, id, reason string) (*Request, error) { @@ -576,69 +661,16 @@ func (s *Service) Retry(ctx context.Context, viewer Viewer, id string) (*Request if req.Outcome != OutcomeFailed { return nil, ErrInvalidState } - active, err := s.store.SetOutcome(ctx, req.ID, OutcomeActive, viewer, "retry requested") + if _, err := s.store.SetOutcome(ctx, req.ID, OutcomeActive, viewer, "retry requested"); err != nil { + return nil, err + } + // submitApprovedRequest only re-submits qualities lacking a healthy target, so + // it is idempotent; gate it on the approved status it expects. + active, err := s.store.SetStatus(ctx, req.ID, StatusApproved, viewer) if err != nil { return nil, err } - - instances, err := s.store.ListIntegrations(ctx) - if err != nil { - return nil, err - } - settings, err := s.store.GetSettings(ctx) - if err != nil { - return nil, err - } - existing, err := s.store.ListTargets(ctx, active.ID) - if err != nil { - return nil, err - } - ceiling := s.requesterCeiling(ctx, active.RequestedByUserID, active.RequestedByProfileID) - planned := routeTargets(*active, ceiling, settings, instances) - if len(planned) == 0 { - return s.markSubmissionFailed(ctx, active.ID, viewer, - fmt.Errorf("no %s instance configured for the requested quality", - integrationKindForMediaType(active.MediaType))) - } - - return s.submitPlannedTargets(ctx, *active, planned, existing, viewer) -} - -// submitPlannedTargets submits each planned target idempotently against the -// already-recorded targets: a non-failed target for a quality is left alone, a -// failed one is deleted and re-submitted, and a missing one is submitted fresh. -// This is shared by Retry and the reconcile-driven submit so a re-run never -// violates the UNIQUE(request_id, quality) constraint. -func (s *Service) submitPlannedTargets(ctx context.Context, req Request, planned []plannedTarget, existing []Target, actor Viewer) (*Request, error) { - hasOK := make(map[Quality]bool) - for _, t := range existing { - if t.Status != StatusFailed { - hasOK[t.Quality] = true - } - } - - latest := &req - for _, pt := range planned { - if hasOK[pt.Quality] { - continue // healthy target already exists for this quality - } - // remove the stale failed target for this quality before re-submitting - for _, t := range existing { - if t.Quality == pt.Quality && t.Status == StatusFailed { - if err := s.store.DeleteTarget(ctx, t.ID); err != nil { - return nil, err - } - } - } - updated, err := s.submitPlannedTarget(ctx, req, pt, actor) - if err != nil { - return nil, err - } - if updated != nil { - latest = updated - } - } - return latest, nil + return s.submitApprovedRequest(ctx, *active, viewer, nil) } func (s *Service) ReconcileRequests(ctx context.Context, limit int) (ReconcileResult, error) { @@ -652,12 +684,16 @@ func (s *Service) ReconcileRequests(ctx context.Context, limit int) (ReconcileRe if err != nil { return ReconcileResult{}, err } + fc, err := s.newFulfillContext(ctx) + if err != nil { + return ReconcileResult{}, err + } result := ReconcileResult{Checked: len(candidates)} for _, req := range candidates { if err := ctx.Err(); err != nil { return result, err } - change, err := s.reconcileRequest(ctx, *req) + change, err := s.reconcileRequest(ctx, *req, fc) if err != nil { slog.WarnContext(ctx, "request reconcile failed", "request_id", req.ID, @@ -765,14 +801,17 @@ func (s *Service) CreateIntegration(ctx context.Context, viewer Viewer, in Integ if !viewer.IsAdmin { return nil, ErrForbidden } - if err := validateInstance(&in); err != nil { - return nil, err - } id, err := idgen.NextID() if err != nil { return nil, err } in.ID = id + if err := validateInstance(&in); err != nil { + return nil, err + } + if err := s.validateViaPlugin(ctx, in); err != nil { + return nil, err + } return s.store.SaveIntegrationWithDefaults(ctx, in, true) } @@ -786,9 +825,86 @@ func (s *Service) UpdateIntegration(ctx context.Context, viewer Viewer, in Integ if err := validateInstance(&in); err != nil { return nil, err } + if err := s.validateViaPlugin(ctx, in); err != nil { + return nil, err + } return s.store.SaveIntegrationWithDefaults(ctx, in, false) } +// validateViaPlugin asks the bound request_router plugin to validate the +// connection config on save. Field/form errors are surfaced as *ValidationError +// so the API layer can render them inline. +func (s *Service) validateViaPlugin(ctx context.Context, in Integration) error { + if s.router == nil || in.InstallationID == nil { + return nil + } + // On UPDATE the client omits api_key_ref ("leave blank to keep saved key"), + // so we would otherwise validate against an empty credential. Mirror + // LoadIntegrationOptions's backfill: load the stored row by id and reuse the + // saved (already-decrypted) api key (and BaseURL/PluginConfig if also blank). + // Nil-safe — a brand-new id has no stored row, so just proceed with what the + // body carries. + if strings.TrimSpace(in.APIKeyRef) == "" && strings.TrimSpace(in.ID) != "" { + stored, err := s.store.GetIntegration(ctx, in.ID) + if err != nil && !errors.Is(err, ErrNotFound) { + return err + } + if stored != nil { + // Don't pair a stored API key with a caller-changed base URL: require the + // key to be re-entered when the server URL changes (defense against + // exfiltrating a stored, API-unreadable key to an attacker-supplied URL). + if strings.TrimSpace(in.BaseURL) != "" && strings.TrimSpace(in.BaseURL) != strings.TrimSpace(stored.BaseURL) { + return &ValidationError{FieldErrors: map[string]string{"api_key_ref": "re-enter the API key when changing the base URL"}} + } + in.APIKeyRef = stored.APIKeyRef + if strings.TrimSpace(in.BaseURL) == "" { + in.BaseURL = stored.BaseURL + } + if in.PluginConfig == nil { + in.PluginConfig = stored.PluginConfig + } + } + } + // in.APIKeyRef is the decrypted literal (from the body, or backfilled from the + // stored row above). + apiKey := strings.TrimSpace(in.APIKeyRef) + conn := ResolvedRouterConnection{ID: in.ID, BaseURL: in.BaseURL, APIKey: apiKey, Config: in.PluginConfig} + siblings, err := s.siblingConnections(ctx, in) + if err != nil { + return err + } + fe, form, err := s.router.Validate(ctx, *in.InstallationID, in.CapabilityID, conn, siblings) + if err != nil { + return err + } + if len(fe) > 0 || form != "" { + return &ValidationError{FieldErrors: fe, FormError: form} + } + return nil +} + +// siblingConnections returns the other connections bound to the same plugin +// installation as `in` (self excluded), carrying only id + config so a plugin +// can enforce cross-connection rules without the host resolving sibling +// credentials. +func (s *Service) siblingConnections(ctx context.Context, in Integration) ([]ResolvedRouterConnection, error) { + if in.InstallationID == nil { + return nil, nil + } + all, err := s.store.ListIntegrations(ctx) + if err != nil { + return nil, err + } + var out []ResolvedRouterConnection + for _, other := range all { + if other.ID == in.ID || other.InstallationID == nil || *other.InstallationID != *in.InstallationID { + continue + } + out = append(out, ResolvedRouterConnection{ID: other.ID, Config: other.PluginConfig}) + } + return out, nil +} + func (s *Service) DeleteIntegration(ctx context.Context, viewer Viewer, id string) error { if !viewer.IsAdmin { return ErrForbidden @@ -797,78 +913,68 @@ func (s *Service) DeleteIntegration(ctx context.Context, viewer Viewer, id strin } func validateInstance(in *Integration) error { - in.Kind = strings.TrimSpace(in.Kind) - if in.Kind != "radarr" && in.Kind != "sonarr" { - return fmt.Errorf("%w: kind must be radarr or sonarr", ErrInvalidInput) - } if strings.TrimSpace(in.Name) == "" { return fmt.Errorf("%w: name is required", ErrInvalidInput) } - if in.IsDefault && in.Is4K { - return fmt.Errorf("%w: the HD default cannot be a 4K server", ErrInvalidInput) + // capability_id carries the capability SUB-ID ("arr"/"seerr"), not the type: + // the host resolves the plugin via requireCapability("request_router.v1", id), + // which keys on (type, id), so storing the type "request_router.v1" here + // resolves nothing. Matches the scan_source/metadata convention + // (autoscan_sources.capability_id = "arr"). The bound plugin's Validate RPC is + // the authority on whether the sub-id names a real capability. + in.CapabilityID = strings.TrimSpace(in.CapabilityID) + if in.CapabilityID == "" { + return fmt.Errorf("%w: capability_id is required", ErrInvalidInput) } - if in.IsDefault4K && !in.Is4K { - return fmt.Errorf("%w: the 4K default must be a 4K server", ErrInvalidInput) + if in.InstallationID == nil { + return fmt.Errorf("%w: installation_id is required", ErrInvalidInput) } + // The is_default/is_4k/is_default_4k cross-field consistency check is owned by + // the request_router plugin's Validate RPC, which surfaces it as an inline + // field error (better UX than a generic host 400). See validateViaPlugin. return nil } -func (s *Service) LoadIntegrationOptions(ctx context.Context, viewer Viewer, integration Integration) (*IntegrationOptions, error) { +func (s *Service) LoadIntegrationOptions(ctx context.Context, viewer Viewer, integration Integration) (map[string][]RouterOption, error) { if !viewer.IsAdmin { return nil, ErrForbidden } - // For a saved instance the request body carries only the path id (no kind and - // often no creds), so resolve the saved row by id and backfill what the body - // omitted. This makes "Test connection" reuse the correct per-instance key - // (each kind can have multiple instances) instead of borrowing a sibling's. + // For a saved instance the request body carries only the path id (no creds and + // often no plugin wiring), so resolve the saved row by id and backfill what the + // body omitted. This makes "Test connection" reuse the correct per-instance key + // (each plugin can have multiple connections) instead of borrowing a sibling's. if id := strings.TrimSpace(integration.ID); id != "" && id != "new" { stored, err := s.store.GetIntegration(ctx, id) if err != nil && !errors.Is(err, ErrNotFound) { return nil, err } if stored != nil { - if strings.TrimSpace(integration.Kind) == "" { - integration.Kind = stored.Kind - } + submittedBaseURL := strings.TrimSpace(integration.BaseURL) + storedBaseURL := strings.TrimSpace(stored.BaseURL) if strings.TrimSpace(integration.BaseURL) == "" { integration.BaseURL = stored.BaseURL } - if strings.TrimSpace(integration.APIKeyRef) == "" { + if strings.TrimSpace(integration.APIKeyRef) == "" && (submittedBaseURL == "" || submittedBaseURL == storedBaseURL) { integration.APIKeyRef = stored.APIKeyRef } + if strings.TrimSpace(integration.CapabilityID) == "" { + integration.CapabilityID = stored.CapabilityID + } + if integration.InstallationID == nil { + integration.InstallationID = stored.InstallationID + } + if integration.PluginConfig == nil { + integration.PluginConfig = stored.PluginConfig + } } } - normalized, err := normalizeIntegrationConnection(integration) - if err != nil { - return nil, err - } - if strings.TrimSpace(normalized.BaseURL) == "" { - return nil, fmt.Errorf("%w: base_url is required", ErrInvalidInput) - } - if strings.TrimSpace(normalized.APIKeyRef) == "" { - return nil, fmt.Errorf("%w: api_key_ref is required", ErrInvalidInput) - } - // APIKeyRef is already the literal key: a saved instance was decrypted by - // GetIntegration above; a new instance carries the key the admin just typed. - resolved := normalized - - switch resolved.Kind { - case "radarr": - adapter, ok := s.movieAdapter.(MovieIntegrationOptionsAdapter) - if !ok { - return nil, fmt.Errorf("request radarr integration options are not configured") - } - return adapter.ListMovieIntegrationOptions(ctx, resolved) - case "sonarr": - adapter, ok := s.seriesAdapter.(SeriesIntegrationOptionsAdapter) - if !ok { - return nil, fmt.Errorf("request sonarr integration options are not configured") - } - return adapter.ListSeriesIntegrationOptions(ctx, resolved) - default: - return nil, fmt.Errorf("%w: invalid integration kind", ErrInvalidInput) + apiKey := strings.TrimSpace(integration.APIKeyRef) + if s.router == nil || integration.InstallationID == nil { + return nil, fmt.Errorf("no fulfillment backend configured") } + conn := ResolvedRouterConnection{ID: integration.ID, BaseURL: integration.BaseURL, APIKey: apiKey, Config: integration.PluginConfig} + return s.router.ListConfigOptions(ctx, *integration.InstallationID, integration.CapabilityID, conn) } func (s *Service) EffectivePolicy(ctx context.Context, userID int) (EffectivePolicy, error) { @@ -1139,105 +1245,247 @@ func (s *Service) detectRequestAnime(ctx context.Context, mediaType MediaType, t return detectAnime(detail.KeywordIDs) } +// integrationConfigured reports whether a fulfillment backend exists for the +// media type, gating auto-approval (pending vs approved). It uses the same +// router-connection selection as resolveRouterConnections — an enabled +// request_router.v1 connection with an installation — and additionally honors a +// connection's declared media-type support so a movie request only auto-approves +// when a router connection supporting "movie" exists. func (s *Service) integrationConfigured(ctx context.Context, mediaType MediaType) (bool, error) { instances, err := s.store.ListIntegrations(ctx) if err != nil { return false, err } - kind := integrationKindForMediaType(mediaType) for _, in := range instances { - if in.Kind == kind && in.Enabled && (in.IsDefault || in.IsDefault4K) && integrationIsConfigured(in) { + if eligibleRouterConnection(in, mediaType) && + strings.TrimSpace(in.BaseURL) != "" && strings.TrimSpace(in.APIKeyRef) != "" { return true, nil } } return false, nil } -func (s *Service) submitApprovedRequest(ctx context.Context, req Request, actor Viewer) (*Request, error) { +// integrationSupportsMediaType reports whether a router connection serves the +// given media type. An empty SupportedMediaTypes is treated as "supports all". +func integrationSupportsMediaType(in Integration, mediaType MediaType) bool { + if len(in.SupportedMediaTypes) == 0 { + return true + } + for _, mt := range in.SupportedMediaTypes { + if mt == string(mediaType) { + return true + } + } + return false +} + +func (s *Service) submitApprovedRequest(ctx context.Context, req Request, actor Viewer, fc *fulfillContext) (*Request, error) { if req.Outcome != OutcomeActive || req.Status != StatusApproved { return &req, nil } - instances, err := s.store.ListIntegrations(ctx) + if s.router == nil { + return s.markSubmissionFailed(ctx, req.ID, actor, fmt.Errorf("no fulfillment backend configured")) + } + if fc == nil { + built, err := s.newFulfillContext(ctx) + if err != nil { + return nil, err + } + fc = built + } + conns, installationID, capabilityID, err := s.resolveRouterConnections(ctx, fc, req.MediaType) if err != nil { return nil, err } - settings, err := s.store.GetSettings(ctx) - if err != nil { - return nil, err + if len(conns) == 0 { + // Distinguish "no backend at all" from the migration breakage where an + // existing connection row exists but its installation_id is NULL (the row + // predates the plugin install and was never re-bound). + msg := "no fulfillment backend configured" + for _, in := range fc.integrations { + if in.Enabled && in.CapabilityID != "" && in.InstallationID == nil { + msg = "request backend connection is not bound to a plugin installation; re-save it in admin" + break + } + } + return s.markSubmissionFailed(ctx, req.ID, actor, errors.New(msg)) } - ceiling := s.requesterCeiling(ctx, req.RequestedByUserID, req.RequestedByProfileID) - planned := routeTargets(req, ceiling, settings, instances) - if len(planned) == 0 { - return s.markSubmissionFailed(ctx, req.ID, actor, - fmt.Errorf("no %s instance configured for the requested quality", - integrationKindForMediaType(req.MediaType))) - } - - // Reconcile can re-run submit while the request is still 'approved'; skip - // qualities that already have a live target and replace failed ones so the - // UNIQUE(request_id, quality) constraint is never violated. existing, err := s.store.ListTargets(ctx, req.ID) if err != nil { return nil, err } - return s.submitPlannedTargets(ctx, req, planned, existing, actor) -} - -// submitPlannedTarget creates a target row, submits it to the adapter, and -// records the result (queued or failed) via UpdateTargetStatus (which recomputes -// the request aggregate). Returns the latest request snapshot. -func (s *Service) submitPlannedTarget(ctx context.Context, req Request, pt plannedTarget, actor Viewer) (*Request, error) { - resolved := resolveInstance(pt) - // The repo already decrypted the stored key; a blank one means unconfigured. - if strings.TrimSpace(resolved.APIKeyRef) == "" { - return s.createFailedTarget(ctx, req, pt, resolved, "missing api key", actor) + healthy := map[Quality]bool{} + for _, t := range existing { + if t.Status != StatusFailed { + healthy[t.Quality] = true + } } - - target, err := s.store.CreateTarget(ctx, Target{ - RequestID: req.ID, IntegrationID: resolved.ID, IntegrationKind: resolved.Kind, - Quality: pt.Quality, IsAnime: pt.IsAnime, Status: StatusQueued, - }) + allowed := s.allowedQualities(ctx, req, fc.settings) + if !fc.settings.ForceDualQuality { + allowed = filterUnconfiguredOptionalQualities(allowed, conns) + } + var want []Quality + for _, q := range allowed { + if !healthy[q] { + want = append(want, q) + } + } + if len(want) == 0 { + return &req, nil + } + for _, t := range existing { // drop stale failed targets for the qualities we re-submit + if t.Status == StatusFailed { + for _, q := range want { + if t.Quality == q { + if err := s.store.DeleteTarget(ctx, t.ID); err != nil { + return nil, err + } + } + } + } + } + s.populateRequesterIdentity(ctx, &req) + targets, msg, err := s.router.Fulfill(ctx, installationID, capabilityID, req, want, conns) if err != nil { return nil, err } - - result, serr := s.submitTarget(ctx, req, resolved) - if serr != nil { - return s.store.UpdateTargetStatus(ctx, target.ID, StatusFailed, "", "", serr.Error(), actor) + if len(targets) == 0 { + if msg == "" { + msg = "fulfillment backend created no targets" + } + return s.markSubmissionFailed(ctx, req.ID, actor, errors.New(msg)) } - return s.store.UpdateTargetStatus(ctx, target.ID, StatusQueued, - result.ExternalID, result.ExternalStatus, "", actor) + connKind := connectionKindByID(conns) + latest := &req + // The plugin is an out-of-process trust boundary: validate every returned + // target against the DB CHECK constraints (quality, status) and skip any + // quality that is duplicated in the batch or already has a healthy target, so + // a misbehaving plugin can't violate UNIQUE(request_id, quality) and wedge the + // request. + validQuality := map[Quality]bool{Quality1080p: true, Quality2160p: true} + validStatus := map[Status]bool{StatusQueued: true, StatusDownloading: true, StatusCompleted: true, StatusFailed: true} + returned := map[Quality]bool{} + for _, rt := range targets { + if !validQuality[rt.Quality] { + slog.WarnContext(ctx, "requests: plugin returned unknown quality; skipping", "request_id", req.ID, "quality", string(rt.Quality)) + continue + } + if returned[rt.Quality] || healthy[rt.Quality] { + continue // dup-in-batch, or a healthy target already exists for this quality + } + if rt.ConnectionID != "" { + if _, ok := connKind[rt.ConnectionID]; !ok { + slog.WarnContext(ctx, "requests: plugin returned unknown connection id; skipping target", "request_id", req.ID, "connection_id", rt.ConnectionID) + continue + } + } + returned[rt.Quality] = true + created, err := s.store.CreateTarget(ctx, Target{ + RequestID: req.ID, IntegrationID: rt.ConnectionID, IntegrationKind: connKind[rt.ConnectionID], + Quality: rt.Quality, IsAnime: req.IsAnime, Status: StatusQueued, + }) + if err != nil { + return nil, err + } + status := rt.Status + if status == "" || !validStatus[status] { + status = StatusQueued // coerce unknown/empty status to the DB-valid default + } + updated, err := s.store.UpdateTargetStatus(ctx, created.ID, status, rt.ExternalID, rt.ExternalStatus, rt.Message, actor) + if err != nil { + return nil, err + } + if updated != nil { + latest = updated + } + } + // Any wanted quality the plugin did not fulfill is recorded as a failed target + // rather than silently dropped, so it stays visible and Retry re-attempts it + // (a failed target is not "healthy"). + const noTargetMsg = "fulfillment backend returned no target for this quality" + for _, q := range want { + if returned[q] { + continue + } + created, err := s.store.CreateTarget(ctx, Target{ + RequestID: req.ID, Quality: q, IsAnime: req.IsAnime, Status: StatusFailed, LastError: noTargetMsg, + }) + if err != nil { + return nil, err + } + updated, err := s.store.UpdateTargetStatus(ctx, created.ID, StatusFailed, "", "", noTargetMsg, actor) + if err != nil { + return nil, err + } + if updated != nil { + latest = updated + } + } + return latest, nil } -func (s *Service) createFailedTarget(ctx context.Context, req Request, pt plannedTarget, resolved Integration, msg string, actor Viewer) (*Request, error) { - target, err := s.store.CreateTarget(ctx, Target{ - RequestID: req.ID, IntegrationID: resolved.ID, IntegrationKind: resolved.Kind, - Quality: pt.Quality, IsAnime: pt.IsAnime, Status: StatusFailed, LastError: msg, - }) - if err != nil { - return nil, err +// connectionKindByID maps each connection id to its plugin-declared service kind +// (e.g. "radarr"/"sonarr") from PluginConfig["service_kind"], for the +// integration_kind column on persisted targets. Missing kinds map to "". +func connectionKindByID(conns []ResolvedRouterConnection) map[string]string { + out := make(map[string]string, len(conns)) + for _, c := range conns { + out[c.ID] = "" + if c.Config != nil { + if kind, ok := c.Config["service_kind"].(string); ok { + out[c.ID] = kind + } + } } - return s.store.UpdateTargetStatus(ctx, target.ID, StatusFailed, "", "", msg, actor) + return out } -// submitTarget calls the correct adapter with a per-target Request copy. The -// adapters read root folder/quality/tags (and Sonarr series_type) from the -// resolved Integration. -func (s *Service) submitTarget(ctx context.Context, req Request, resolved Integration) (FulfillmentResult, error) { - switch req.MediaType { - case MediaTypeMovie: - if s.movieAdapter == nil { - return FulfillmentResult{}, fmt.Errorf("no movie adapter configured") +func filterUnconfiguredOptionalQualities(qualities []Quality, conns []ResolvedRouterConnection) []Quality { + out := make([]Quality, 0, len(qualities)) + for _, q := range qualities { + if q == Quality2160p && !routerQualityConfigured(q, conns) { + continue } - return s.movieAdapter.SubmitMovie(ctx, req, resolved) - case MediaTypeSeries: - if s.seriesAdapter == nil { - return FulfillmentResult{}, fmt.Errorf("no series adapter configured") - } - return s.seriesAdapter.SubmitSeries(ctx, req, resolved) - default: - return FulfillmentResult{}, fmt.Errorf("unsupported media type %q", req.MediaType) + out = append(out, q) } + return out +} + +func routerQualityConfigured(q Quality, conns []ResolvedRouterConnection) bool { + usesTieredDefaults := false + for _, conn := range conns { + if conn.Config == nil { + continue + } + if hasRouterQualityKey(conn.Config) { + usesTieredDefaults = true + } + if q == Quality2160p && boolConfig(conn.Config, "is_default_4k") { + return true + } + } + // Generic request_router implementations may not expose arr-style HD/4K + // default flags. In that case, preserve the host's requested qualities and + // let the plugin decide what it can fulfill. + return !usesTieredDefaults +} + +func hasRouterQualityKey(config map[string]any) bool { + for _, key := range []string{"is_default", "is_default_4k", "is_4k"} { + if _, ok := config[key]; ok { + return true + } + } + return false +} + +func boolConfig(config map[string]any, key string) bool { + v, ok := config[key] + if !ok { + return false + } + b, ok := v.(bool) + return ok && b } func (s *Service) markSubmissionFailed(ctx context.Context, requestID string, actor Viewer, submitErr error) (*Request, error) { @@ -1259,7 +1507,7 @@ const ( reconcileFailed reconcileChange = "failed" ) -func (s *Service) reconcileRequest(ctx context.Context, req Request) (reconcileChange, error) { +func (s *Service) reconcileRequest(ctx context.Context, req Request, fc *fulfillContext) (reconcileChange, error) { completed, err := s.requestAvailable(ctx, req) if err != nil { return reconcileUnchanged, err @@ -1286,7 +1534,7 @@ func (s *Service) reconcileRequest(ctx context.Context, req Request) (reconcileC } if req.Status == StatusApproved { - updated, err := s.submitApprovedRequest(ctx, req, Viewer{}) + updated, err := s.submitApprovedRequest(ctx, req, Viewer{}, fc) if err != nil { return reconcileUnchanged, err } @@ -1304,41 +1552,51 @@ func (s *Service) reconcileRequest(ctx context.Context, req Request) (reconcileC if err != nil { return reconcileUnchanged, err } - instances, err := s.store.ListIntegrations(ctx) + if s.router == nil { + return reconcileUnchanged, nil + } + conns, installationID, capabilityID, err := s.resolveRouterConnections(ctx, fc, req.MediaType) if err != nil { return reconcileUnchanged, err } - byID := make(map[string]Integration, len(instances)) - for _, in := range instances { - byID[in.ID] = in + if len(conns) == 0 { + return reconcileUnchanged, nil } - change := reconcileUnchanged + + var refs []RouterTargetRef for _, t := range targets { if t.Status == StatusCompleted || t.Status == StatusFailed { continue } - in, ok := byID[t.IntegrationID] - if !ok { + refs = append(refs, RouterTargetRef{Quality: t.Quality, ConnectionID: t.IntegrationID, ExternalID: t.ExternalID}) + } + if len(refs) == 0 { + return reconcileUnchanged, nil + } + + statuses, err := s.router.CheckStatus(ctx, installationID, capabilityID, req, refs, conns) + if err != nil { + return reconcileUnchanged, err + } + + change := reconcileUnchanged + for _, st := range statuses { + // Match the returned status to the live target by (quality, connection). + var target *Target + for i := range targets { + if targets[i].Quality == st.Quality && targets[i].IntegrationID == st.ConnectionID { + target = &targets[i] + break + } + } + if target == nil || target.Status == StatusCompleted || target.Status == StatusFailed { continue } - // in.APIKeyRef was decrypted by ListIntegrations; skip if unconfigured. - if strings.TrimSpace(in.APIKeyRef) == "" { + newStatus := st.Status + if newStatus == "" || newStatus == target.Status { continue } - probe := req - probe.ExternalID = t.ExternalID - st, err := s.checkFulfillmentStatus(ctx, probe, in) - if err != nil { - continue - } - if st.Status == "" && st.Outcome == "" { - continue - } - newStatus := targetStatusFromFulfillment(st) - if newStatus == t.Status { - continue - } - if _, err := s.store.UpdateTargetStatus(ctx, t.ID, newStatus, st.ExternalID, st.ExternalStatus, "", Viewer{}); err != nil { + if _, err := s.store.UpdateTargetStatus(ctx, target.ID, newStatus, "", st.ExternalStatus, st.Message, Viewer{}); err != nil { return reconcileUnchanged, err } switch newStatus { @@ -1357,20 +1615,6 @@ func (s *Service) reconcileRequest(ctx context.Context, req Request) (reconcileC return change, nil } -func targetStatusFromFulfillment(st FulfillmentStatus) Status { - switch st.Status { - case StatusCompleted: - return StatusCompleted - case StatusDownloading: - return StatusDownloading - default: - if st.Outcome == OutcomeFailed { - return StatusFailed - } - return StatusQueued - } -} - // hasLiveTargets reports whether the request has any non-terminal (queued or // downloading) fulfillment target. func (s *Service) hasLiveTargets(ctx context.Context, requestID string) (bool, error) { @@ -1394,33 +1638,6 @@ func (s *Service) requestAvailable(ctx context.Context, req Request) (bool, erro return matches[req.TMDBID].Available, nil } -func (s *Service) checkFulfillmentStatus(ctx context.Context, req Request, resolved Integration) (FulfillmentStatus, error) { - switch req.MediaType { - case MediaTypeMovie: - checker, ok := s.movieAdapter.(MovieStatusAdapter) - if !ok { - return FulfillmentStatus{}, nil - } - return checker.CheckMovieStatus(ctx, req, resolved) - case MediaTypeSeries: - checker, ok := s.seriesAdapter.(SeriesStatusAdapter) - if !ok { - return FulfillmentStatus{}, nil - } - return checker.CheckSeriesStatus(ctx, req, resolved) - default: - return FulfillmentStatus{}, nil - } -} - -func integrationIsConfigured(integration Integration) bool { - return integration.Enabled && - strings.TrimSpace(integration.BaseURL) != "" && - strings.TrimSpace(integration.APIKeyRef) != "" && - strings.TrimSpace(integration.RootFolder) != "" && - integration.QualityProfileID != nil -} - func (s *Service) now() time.Time { if s.Now != nil { return s.Now() @@ -1529,21 +1746,6 @@ func normalizeUserLimit(limit UserLimit) (UserLimit, error) { return limit, nil } -func normalizeIntegrationConnection(integration Integration) (Integration, error) { - integration.Kind = strings.ToLower(strings.TrimSpace(integration.Kind)) - switch integration.Kind { - case "radarr", "sonarr": - default: - return Integration{}, fmt.Errorf("%w: invalid integration kind", ErrInvalidInput) - } - integration.BaseURL = strings.TrimRight(strings.TrimSpace(integration.BaseURL), "/") - integration.APIKeyRef = strings.TrimSpace(integration.APIKeyRef) - if integration.Options == nil { - integration.Options = map[string]any{} - } - return integration, nil -} - func normalizeMediaType(mediaType MediaType) (MediaType, error) { switch MediaType(strings.ToLower(strings.TrimSpace(string(mediaType)))) { case MediaTypeMovie: diff --git a/internal/requests/service_test.go b/internal/requests/service_test.go index f6be525f..b7550a83 100644 --- a/internal/requests/service_test.go +++ b/internal/requests/service_test.go @@ -157,27 +157,21 @@ func TestCreateRequestAutoApprovalRequiresConfiguredIntegration(t *testing.T) { } } -func TestCreateRequestAutoApprovesWithConfiguredIntegration(t *testing.T) { +// TestCreateRequestAutoApprovalEmptyKeyTreatedAsUnconfigured guards that a router +// connection that is enabled + bound but has no api key (empty after the repo's +// decrypt) reads as "not configured": auto-approval is declined and the request +// stays pending, rather than being auto-approved and then failing submission when +// resolveRouterConnections skips the keyless connection. This pins the empty-key +// check in integrationConfigured against the skip in resolveRouterConnections so +// the two can't drift at the public CreateRequest surface. +func TestCreateRequestAutoApprovalEmptyKeyTreatedAsUnconfigured(t *testing.T) { store := newFakeStore() store.settings.RequestsEnabled = true store.settings.GlobalAutoApprovalEnabled = true - qualityProfileID := 1 - store.integrations = []Integration{{ - Kind: "radarr", - Enabled: true, - IsDefault: true, - BaseURL: "http://radarr.local", - APIKeyRef: "request.radarr.api_key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }} - adapter := &fakeMovieAdapter{result: FulfillmentResult{ - IntegrationKind: "radarr", - ExternalID: "123", - ExternalStatus: "queued", - }} + store.integrations = []Integration{autoApproveRouterInst("router-1", "")} service := newTestService(store) - service.SetFulfillmentAdapters(adapter, nil) + router := &fakeRouterProvider{} + service.SetRouterProvider(router) req, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{ MediaType: MediaTypeMovie, @@ -187,34 +181,75 @@ func TestCreateRequestAutoApprovesWithConfiguredIntegration(t *testing.T) { if err != nil { t.Fatalf("CreateRequest returned error: %v", err) } - // A configured HD default auto-approves and immediately submits, so the - // request lands in the fulfillment pipeline (aggregate of one queued target). + if req.Status != StatusPending { + t.Fatalf("status = %q, want pending (empty-key connection is unconfigured)", req.Status) + } + if router.fulfillCalls != 0 { + t.Fatalf("fulfill calls = %d, want 0 (must not submit to a keyless connection)", router.fulfillCalls) + } +} + +func TestCreateRequestAutoApprovesWithConfiguredIntegration(t *testing.T) { + store := newFakeStore() + store.settings.RequestsEnabled = true + store.settings.GlobalAutoApprovalEnabled = true + // A plugin-driven router connection that sets only the generic + // Enabled/CapabilityID/InstallationID fields (no legacy Kind/IsDefault columns) + // must still satisfy the auto-approve gate. + store.integrations = []Integration{routerInst("router-1")} + service := newTestService(store) + service.SetRouterProvider(&fakeRouterProvider{}) + + req, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{ + MediaType: MediaTypeMovie, + TMDBID: 550, + Title: "Fight Club", + }) + if err != nil { + t.Fatalf("CreateRequest returned error: %v", err) + } + // The configured router connection auto-approves and immediately submits, so the + // request lands in the fulfillment pipeline (one queued target). if req.Status != StatusQueued { t.Fatalf("status = %q, want queued (auto-approved and submitted)", req.Status) } } +func TestCreateRequestAutoApprovalRespectsSupportedMediaTypes(t *testing.T) { + store := newFakeStore() + store.settings.RequestsEnabled = true + store.settings.GlobalAutoApprovalEnabled = true + // A router connection that only serves series must NOT auto-approve a movie + // request; the gate falls back to manual approval (pending). + seriesOnly := routerInst("router-series") + seriesOnly.SupportedMediaTypes = []string{string(MediaTypeSeries)} + store.integrations = []Integration{seriesOnly} + service := newTestService(store) + service.SetRouterProvider(&fakeRouterProvider{}) + + req, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{ + MediaType: MediaTypeMovie, + TMDBID: 550, + Title: "Fight Club", + }) + if err != nil { + t.Fatalf("CreateRequest returned error: %v", err) + } + if req.Status != StatusPending { + t.Fatalf("status = %q, want pending (no router connection supports movie)", req.Status) + } +} + func TestCreateRequestAutoApprovalSubmitsMovie(t *testing.T) { store := newFakeStore() store.settings.RequestsEnabled = true store.settings.GlobalAutoApprovalEnabled = true - qualityProfileID := 1 - store.integrations = []Integration{{ - Kind: "radarr", - Enabled: true, - IsDefault: true, - BaseURL: "http://radarr.local", - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }} - adapter := &fakeMovieAdapter{result: FulfillmentResult{ - IntegrationKind: "radarr", - ExternalID: "123", - ExternalStatus: "queued", - }} + // The repo decrypts api_key_ref on read, so the connection carries the literal + // key here (no host-side secret resolution). + store.integrations = []Integration{autoApproveRouterInst("router-1", "radarr-key")} + router := &fakeRouterProvider{} service := newTestService(store) - service.SetFulfillmentAdapters(adapter, nil) + service.SetRouterProvider(router) req, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{ MediaType: MediaTypeMovie, @@ -224,16 +259,15 @@ func TestCreateRequestAutoApprovalSubmitsMovie(t *testing.T) { if err != nil { t.Fatalf("CreateRequest returned error: %v", err) } - if req.Status != StatusQueued || req.IntegrationKind != "radarr" || req.ExternalID != "123" { - t.Fatalf("request = %+v, want queued radarr external id", req) + if req.Status != StatusQueued || req.ExternalID != "ext-1080p" { + t.Fatalf("request = %+v, want queued with router external id", req) } - if adapter.calls != 1 { - t.Fatalf("adapter calls = %d, want 1", adapter.calls) + if router.fulfillCalls != 1 { + t.Fatalf("fulfill calls = %d, want 1", router.fulfillCalls) } - // The repo decrypts api_key_ref on read, so the adapter receives the literal - // key verbatim — no resolver indirection. - if got := adapter.gotIntegration.APIKeyRef; got != "radarr-key" { - t.Fatalf("adapter api key = %q, want radarr-key", got) + // The plaintext credential is resolved before dispatch and handed to the provider. + if len(router.gotConns) != 1 || router.gotConns[0].APIKey != "radarr-key" { + t.Fatalf("router connections = %+v, want resolved api key", router.gotConns) } } @@ -241,19 +275,11 @@ func TestCreateRequestSubmissionFailureMarksFailed(t *testing.T) { store := newFakeStore() store.settings.RequestsEnabled = true store.settings.GlobalAutoApprovalEnabled = true - qualityProfileID := 1 - store.integrations = []Integration{{ - Kind: "radarr", - Enabled: true, - IsDefault: true, - BaseURL: "http://radarr.local", - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }} - adapter := &fakeMovieAdapter{err: errors.New("radarr unavailable")} + store.integrations = []Integration{autoApproveRouterInst("router-1", "radarr-key")} + // A provider that creates no targets (e.g. no radarr instance) returns its own + // message; the host marks the request failed with it. service := newTestService(store) - service.SetFulfillmentAdapters(adapter, nil) + service.SetRouterProvider(&fakeRouterProvider{noTargets: true, fulfillMsg: "radarr unavailable"}) req, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{ MediaType: MediaTypeMovie, @@ -264,7 +290,7 @@ func TestCreateRequestSubmissionFailureMarksFailed(t *testing.T) { t.Fatalf("CreateRequest returned error: %v", err) } if req.Outcome != OutcomeFailed || req.LastError != "radarr unavailable" { - t.Fatalf("request = %+v, want failed outcome with adapter error", req) + t.Fatalf("request = %+v, want failed outcome with provider message", req) } } @@ -784,43 +810,32 @@ func TestReconcileRequestsCompletesByStoredTVDBID(t *testing.T) { } } -func TestReconcileRequestsMarksDownloadingFromAdapter(t *testing.T) { +func TestReconcileRequestsMarksDownloadingFromProvider(t *testing.T) { store := newFakeStore() - qualityProfileID := 1 - store.integrations = []Integration{{ - ID: "radarr-hd", - Kind: "radarr", - Enabled: true, - IsDefault: true, - BaseURL: "http://radarr.local", - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }} + store.integrations = []Integration{routerInst("router-1")} store.candidates = []*Request{{ - ID: "req-1", - MediaType: MediaTypeMovie, - TMDBID: 550, - Status: StatusQueued, - Outcome: OutcomeActive, - ExternalID: "123", + ID: "req-1", + MediaType: MediaTypeMovie, + TMDBID: 550, + Status: StatusQueued, + Outcome: OutcomeActive, }} - // Reconcile now drives status per-target; seed a queued target for the request. + // Reconcile drives status per-target via the provider; seed a queued target. store.requests["req-1"] = &Request{ID: "req-1", MediaType: MediaTypeMovie, TMDBID: 550, Status: StatusQueued, Outcome: OutcomeActive} if _, err := store.CreateTarget(context.Background(), Target{ - RequestID: "req-1", IntegrationID: "radarr-hd", IntegrationKind: "radarr", + RequestID: "req-1", IntegrationID: "router-1", Quality: Quality1080p, Status: StatusQueued, ExternalID: "123", }); err != nil { t.Fatalf("seed target: %v", err) } - adapter := &fakeMovieAdapter{status: FulfillmentStatus{ - Status: StatusDownloading, - IntegrationKind: "radarr", - ExternalID: "123", - ExternalStatus: "downloading", - }} + router := &fakeRouterProvider{statuses: []RouterTargetStatus{{ + Quality: Quality1080p, + ConnectionID: "router-1", + Status: StatusDownloading, + ExternalStatus: "downloading", + }}} service := newTestService(store) - service.SetFulfillmentAdapters(adapter, nil) + service.SetRouterProvider(router) result, err := service.ReconcileRequests(context.Background(), 100) if err != nil { @@ -829,8 +844,83 @@ func TestReconcileRequestsMarksDownloadingFromAdapter(t *testing.T) { if result.Downloading != 1 || len(store.statusUpdates) != 1 || store.statusUpdates[0] != StatusDownloading { t.Fatalf("result = %+v statusUpdates = %+v, want one downloading update", result, store.statusUpdates) } - if adapter.statusCalls != 1 { - t.Fatalf("status adapter calls = %d, want 1", adapter.statusCalls) + if router.statusCalls != 1 { + t.Fatalf("provider status calls = %d, want 1", router.statusCalls) + } +} + +func TestReconcileRequestsPreservesProviderFailureMessage(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + store.candidates = []*Request{{ + ID: "req-1", + MediaType: MediaTypeMovie, + TMDBID: 550, + Status: StatusQueued, + Outcome: OutcomeActive, + }} + store.requests["req-1"] = &Request{ID: "req-1", MediaType: MediaTypeMovie, TMDBID: 550, Status: StatusQueued, Outcome: OutcomeActive} + if _, err := store.CreateTarget(context.Background(), Target{ + RequestID: "req-1", IntegrationID: "router-1", + Quality: Quality1080p, Status: StatusQueued, ExternalID: "123", + }); err != nil { + t.Fatalf("seed target: %v", err) + } + router := &fakeRouterProvider{statuses: []RouterTargetStatus{{ + Quality: Quality1080p, + ConnectionID: "router-1", + Status: StatusFailed, + ExternalStatus: "failed", + Message: "indexer rejected the request", + }}} + service := newTestService(store) + service.SetRouterProvider(router) + + result, err := service.ReconcileRequests(context.Background(), 100) + if err != nil { + t.Fatalf("ReconcileRequests returned error: %v", err) + } + if result.Failed != 1 { + t.Fatalf("result = %+v, want one failed target update", result) + } + targets, _ := store.ListTargets(context.Background(), "req-1") + if len(targets) != 1 || targets[0].LastError != "indexer rejected the request" { + t.Fatalf("targets = %+v, want provider failure message preserved", targets) + } +} + +func TestReconcileRequestsResolvesGlobalInputsOncePerCycle(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} // APIKeyRef "key-router-1" + + // Three approved candidates that all share the same router connection. Each + // one drives submitApprovedRequest, which resolves the router connection. + // Without per-cycle caching this would fetch integrations/settings once per + // request. + for _, id := range []string{"req-1", "req-2", "req-3"} { + req := &Request{ID: id, MediaType: MediaTypeMovie, TMDBID: 550, Status: StatusApproved, Outcome: OutcomeActive} + store.candidates = append(store.candidates, req) + store.requests[id] = req + } + + service := newTestService(store) + service.SetRouterProvider(&fakeRouterProvider{}) + + result, err := service.ReconcileRequests(context.Background(), 100) + if err != nil { + t.Fatalf("ReconcileRequests returned error: %v", err) + } + if result.Checked != 3 || result.Submitted != 3 { + t.Fatalf("result = %+v, want 3 checked / 3 submitted", result) + } + + // Integrations and settings are fetched once per cycle (the connection's key is + // already decrypted by the repo on read, so there is nothing to re-resolve). + if store.listIntegrationsCalls != 1 { + t.Fatalf("ListIntegrations calls = %d, want 1 per cycle", store.listIntegrationsCalls) + } + if store.getSettingsCalls != 1 { + t.Fatalf("GetSettings calls = %d, want 1 per cycle", store.getSettingsCalls) } } @@ -838,7 +928,6 @@ func TestDeleteIntegrationRejectsLiveTargets(t *testing.T) { store := newFakeStore() store.integrations = []Integration{{ ID: "radarr-hd", - Kind: "radarr", Enabled: true, }} store.targets = map[string][]Target{ @@ -864,6 +953,186 @@ func TestDeleteIntegrationRejectsLiveTargets(t *testing.T) { } } +func TestCreateIntegrationRejectedByPluginValidate(t *testing.T) { + store := newFakeStore() + service := newTestService(store) + service.SetRouterProvider(&fakeRouterProvider{ + validateFieldErrors: map[string]string{"root_folder": "root folder does not exist"}, + validateFormError: "connection invalid", + }) + + install := 1 + _, err := service.CreateIntegration(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + Name: "radarr", + CapabilityID: "arr", + BaseURL: "http://radarr.local", + InstallationID: &install, + }) + if err == nil { + t.Fatal("expected validation error from plugin Validate") + } + var ve *ValidationError + if !errors.As(err, &ve) { + t.Fatalf("err = %v, want *ValidationError", err) + } + if ve.FieldErrors["root_folder"] != "root folder does not exist" { + t.Fatalf("field errors = %+v, want root_folder error", ve.FieldErrors) + } + if ve.FormError != "connection invalid" { + t.Fatalf("form error = %q, want connection invalid", ve.FormError) + } + if len(store.integrations) != 0 { + t.Fatalf("integrations = %d, want 0 (rejected before persist)", len(store.integrations)) + } +} + +// TestValidateInstanceRequiresCapabilitySubID locks the capability_id contract: +// the column carries the capability SUB-ID ("arr"/"seerr"), matching the value +// the host passes to pluginhost.Client.RequestRouter -> requireCapability, which +// keys on (type, id). The capability TYPE ("request_router.v1") must NOT be +// accepted or defaulted in, since requireCapability("request_router.v1", +// "request_router.v1") never matches a plugin whose capability id is "arr". +func TestValidateInstanceRequiresCapabilitySubID(t *testing.T) { + install := 1 + if err := validateInstance(&Integration{Name: "radarr", CapabilityID: "arr", InstallationID: &install}); err != nil { + t.Fatalf("validateInstance(sub-id \"arr\") = %v, want nil", err) + } + if err := validateInstance(&Integration{Name: "radarr", CapabilityID: "", InstallationID: &install}); !errors.Is(err, ErrInvalidInput) { + t.Fatalf("validateInstance(empty capability) = %v, want ErrInvalidInput", err) + } +} + +// TestCreateIntegrationPassesCapabilitySubIDToPlugin guards that the sub-id the +// admin selected reaches the router provider verbatim (it used to be rewritten to +// the capability type, which the plugin runtime could never resolve). +func TestCreateIntegrationPassesCapabilitySubIDToPlugin(t *testing.T) { + store := newFakeStore() + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + + install := 1 + if _, err := service.CreateIntegration(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + Name: "radarr", + CapabilityID: "arr", + BaseURL: "http://radarr.local", + APIKeyRef: "key-radarr", + InstallationID: &install, + }); err != nil { + t.Fatalf("CreateIntegration err = %v, want nil", err) + } + if router.gotValidateCapability != "arr" { + t.Fatalf("plugin Validate capability = %q, want \"arr\"", router.gotValidateCapability) + } +} + +// TestUpdateIntegrationRefusesStoredKeyReuseOnChangedBaseURL covers the security +// hardening: when the caller leaves api_key_ref blank ("keep saved key") but +// changes the base_url, the service must refuse rather than pair the stored, +// API-unreadable key with the new (potentially attacker-controlled) URL. The +// plugin Validate is never called and nothing is persisted. +func TestUpdateIntegrationRefusesStoredKeyReuseOnChangedBaseURL(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} // BaseURL http://router-1.local, APIKeyRef key-router-1 + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + + install := 1 + _, err := service.UpdateIntegration(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + ID: "router-1", + Name: "router-1", + CapabilityID: "arr", + BaseURL: "http://attacker.example", // changed from the stored base URL + APIKeyRef: "", // blank -> "keep saved key" + InstallationID: &install, + }) + var ve *ValidationError + if !errors.As(err, &ve) { + t.Fatalf("err = %v, want *ValidationError", err) + } + if ve.FieldErrors["api_key_ref"] == "" { + t.Fatalf("field errors = %+v, want api_key_ref message", ve.FieldErrors) + } + if router.validateCalls != 0 { + t.Fatalf("plugin Validate calls = %d, want 0 (refused before dispatch)", router.validateCalls) + } + // Stored row must be unchanged (not persisted with the new URL). + if got := store.integrations[0].BaseURL; got != "http://router-1.local" { + t.Fatalf("stored base_url = %q, want unchanged http://router-1.local", got) + } +} + +// TestUpdateIntegrationKeepsKeyWhenBaseURLUnchanged confirms the normal +// edit-keeping-key flow still works: blank api_key_ref with an unchanged (or +// blank) base_url backfills the stored key, calls the plugin Validate, and +// persists. +func TestUpdateIntegrationKeepsKeyWhenBaseURLUnchanged(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + + install := 1 + updated, err := service.UpdateIntegration(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + ID: "router-1", + Name: "router-1-renamed", + CapabilityID: "arr", + BaseURL: "http://router-1.local", // unchanged + APIKeyRef: "", // blank -> keep saved key + InstallationID: &install, + }) + if err != nil { + t.Fatalf("UpdateIntegration err = %v, want nil", err) + } + if router.validateCalls != 1 { + t.Fatalf("plugin Validate calls = %d, want 1", router.validateCalls) + } + if updated == nil || updated.Name != "router-1-renamed" { + t.Fatalf("updated = %+v, want persisted name router-1-renamed", updated) + } +} + +func TestLoadIntegrationOptionsDoesNotBackfillStoredKeyForChangedBaseURL(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + + if _, err := service.LoadIntegrationOptions(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + ID: "router-1", + BaseURL: "http://attacker.example", + }); err != nil { + t.Fatalf("LoadIntegrationOptions: %v", err) + } + if router.gotOptionsConn.APIKey != "" { + t.Fatalf("probe API key = %q, want empty for changed base URL", router.gotOptionsConn.APIKey) + } + if router.gotOptionsConn.BaseURL != "http://attacker.example" { + t.Fatalf("probe base URL = %q, want submitted URL", router.gotOptionsConn.BaseURL) + } +} + +func TestLoadIntegrationOptionsBackfillsStoredKeyForSameBaseURL(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + + if _, err := service.LoadIntegrationOptions(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + ID: "router-1", + BaseURL: "http://router-1.local", + }); err != nil { + t.Fatalf("LoadIntegrationOptions: %v", err) + } + if router.gotOptionsConn.APIKey != "key-router-1" { + t.Fatalf("probe API key = %q, want stored key for unchanged base URL", router.gotOptionsConn.APIKey) + } +} + func TestCancelOwnerCanWithdrawPendingRequest(t *testing.T) { store := newFakeStore() store.requests["req-mine"] = &Request{ @@ -985,16 +1254,7 @@ func TestDeclineRejectsQueuedRequests(t *testing.T) { func TestRetryResubmitsFailedQueuedRequest(t *testing.T) { store := newFakeStore() - qualityProfileID := 1 - store.integrations = []Integration{{ - Kind: "radarr", - Enabled: true, - IsDefault: true, - BaseURL: "http://radarr.local", - APIKeyRef: "radarr-key", - RootFolder: "/movies", - QualityProfileID: &qualityProfileID, - }} + store.integrations = []Integration{routerInst("router-1")} store.requests["req-1"] = &Request{ ID: "req-1", MediaType: MediaTypeMovie, @@ -1002,26 +1262,21 @@ func TestRetryResubmitsFailedQueuedRequest(t *testing.T) { Status: StatusQueued, Outcome: OutcomeFailed, } - adapter := &fakeMovieAdapter{result: FulfillmentResult{ - IntegrationKind: "radarr", - ExternalID: "99", - ExternalStatus: "queued", - }} + router := &fakeRouterProvider{} service := newTestService(store) - service.SetFulfillmentAdapters(adapter, nil) + service.SetRouterProvider(router) req, err := service.Retry(context.Background(), Viewer{UserID: 1, IsAdmin: true}, "req-1") if err != nil { t.Fatalf("Retry returned error: %v", err) } - if adapter.calls != 1 { - t.Fatalf("adapter calls = %d, want 1", adapter.calls) + if router.fulfillCalls != 1 { + t.Fatalf("fulfill calls = %d, want 1", router.fulfillCalls) } - // Retry re-routes the failed request to its default instance and re-submits - // a fresh target; the request aggregate returns to queued with the new - // external id reported by the adapter. - if req.Status != StatusQueued || req.ExternalID != "99" { - t.Fatalf("request = %+v, want re-queued with external id 99", req) + // Retry transitions the failed request back to approved and re-dispatches via + // the router; the request aggregate returns to queued with the new external id. + if req.Status != StatusQueued || req.ExternalID != "ext-1080p" { + t.Fatalf("request = %+v, want re-queued with router external id", req) } } @@ -1053,6 +1308,9 @@ type fakeStore struct { requests map[string]*Request targets map[string][]Target targetSeq int64 + + listIntegrationsCalls int + getSettingsCalls int } func newFakeStore() *fakeStore { @@ -1073,6 +1331,7 @@ func newFakeStore() *fakeStore { func (f *fakeStore) GetSettings(context.Context) (Settings, error) { f.mu.Lock() defer f.mu.Unlock() + f.getSettingsCalls++ return f.settings, nil } @@ -1235,6 +1494,7 @@ func (f *fakeStore) SetOutcome(_ context.Context, id string, outcome Outcome, _ func (f *fakeStore) ListIntegrations(context.Context) ([]Integration, error) { f.mu.Lock() defer f.mu.Unlock() + f.listIntegrationsCalls++ return f.integrations, nil } @@ -1274,17 +1534,6 @@ func (f *fakeStore) UpdateIntegration(_ context.Context, in Integration) (*Integ func (f *fakeStore) SaveIntegrationWithDefaults(_ context.Context, in Integration, isCreate bool) (*Integration, error) { f.mu.Lock() defer f.mu.Unlock() - for i := range f.integrations { - if f.integrations[i].Kind != in.Kind || f.integrations[i].ID == in.ID { - continue - } - if in.IsDefault { - f.integrations[i].IsDefault = false - } - if in.IsDefault4K { - f.integrations[i].IsDefault4K = false - } - } if isCreate { f.integrations = append(f.integrations, in) cp := in @@ -1676,66 +1925,744 @@ func (f *fakeTMDBClient) GetMediaDetail(context.Context, string, int) (*tmdb.Med return f.detail, nil } -type fakeMovieAdapter struct { - result FulfillmentResult - status FulfillmentStatus - err error - statusErr error - calls int - statusCalls int - gotReq Request - gotIntegration Integration -} - -func (f *fakeMovieAdapter) SubmitMovie(_ context.Context, req Request, integration Integration) (FulfillmentResult, error) { - f.calls++ - f.gotReq = req - f.gotIntegration = integration - return f.result, f.err -} - -func (f *fakeMovieAdapter) CheckMovieStatus(_ context.Context, req Request, integration Integration) (FulfillmentStatus, error) { - f.statusCalls++ - f.gotReq = req - f.gotIntegration = integration - return f.status, f.statusErr -} - type fixedCeiling struct{ q string } func (f fixedCeiling) MaxPlaybackQuality(context.Context, int, string) (string, error) { return f.q, nil } -type recordingMovieAdapter struct { - mu sync.Mutex - ids []string +// fakeRouterProvider is a canned RequestRouterProvider standing in for a +// request_router.v1 plugin. Fulfill emits one target per requested quality +// (unless noTargets is set), recording the qualities and connections it saw. +type fakeRouterProvider struct { + mu sync.Mutex + + // Fulfill behavior. + noTargets bool + fulfillMsg string + fulfillErr error + targetsOverride []RouterTarget // when non-nil, Fulfill returns this verbatim + gotQualities []Quality + gotConns []ResolvedRouterConnection + gotInstallationID int + fulfillCalls int + + gotRequesterEmail string + gotRequesterUsername string + + // CheckStatus behavior. + statuses []RouterTargetStatus + statusErr error + statusCalls int + + // ListConfigOptions behavior. + options map[string][]RouterOption + gotOptionsConn ResolvedRouterConnection + + // Validate behavior (default empty = valid). + validateFieldErrors map[string]string + validateFormError string + validateErr error + validateCalls int + gotValidateCapability string + gotValidateSiblings []ResolvedRouterConnection } -func (a *recordingMovieAdapter) SubmitMovie(_ context.Context, _ Request, integration Integration) (FulfillmentResult, error) { - a.mu.Lock() - defer a.mu.Unlock() - a.ids = append(a.ids, integration.ID) - return FulfillmentResult{IntegrationKind: integration.Kind, ExternalID: "ext-" + integration.ID, ExternalStatus: "queued"}, nil +func (f *fakeRouterProvider) Fulfill(_ context.Context, installationID int, _ string, req Request, qualities []Quality, conns []ResolvedRouterConnection) ([]RouterTarget, string, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.gotRequesterEmail = req.RequesterEmail + f.gotRequesterUsername = req.RequesterUsername + f.fulfillCalls++ + f.gotQualities = append(f.gotQualities, qualities...) + f.gotConns = conns + f.gotInstallationID = installationID + if f.fulfillErr != nil { + return nil, "", f.fulfillErr + } + if f.targetsOverride != nil { + return f.targetsOverride, f.fulfillMsg, nil + } + if f.noTargets { + return nil, f.fulfillMsg, nil + } + connID := "" + if len(conns) > 0 { + connID = conns[0].ID + } + out := make([]RouterTarget, 0, len(qualities)) + for _, q := range qualities { + out = append(out, RouterTarget{ + Quality: q, + ConnectionID: connID, + ExternalID: "ext-" + string(q), + ExternalStatus: "queued", + Status: StatusQueued, + }) + } + return out, f.fulfillMsg, nil +} + +func (f *fakeRouterProvider) CheckStatus(_ context.Context, _ int, _ string, _ Request, _ []RouterTargetRef, _ []ResolvedRouterConnection) ([]RouterTargetStatus, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.statusCalls++ + return f.statuses, f.statusErr +} + +func (f *fakeRouterProvider) ListConfigOptions(_ context.Context, _ int, _ string, conn ResolvedRouterConnection) (map[string][]RouterOption, error) { + f.mu.Lock() + f.gotOptionsConn = conn + f.mu.Unlock() + return f.options, nil +} + +func (f *fakeRouterProvider) TestConnection(_ context.Context, _ int, _ string, _ ResolvedRouterConnection) (bool, string, error) { + return true, "", nil +} + +func (f *fakeRouterProvider) Validate(_ context.Context, _ int, capabilityID string, _ ResolvedRouterConnection, siblings []ResolvedRouterConnection) (map[string]string, string, error) { + f.mu.Lock() + f.validateCalls++ + f.gotValidateCapability = capabilityID + f.gotValidateSiblings = siblings + f.mu.Unlock() + return f.validateFieldErrors, f.validateFormError, f.validateErr +} + +// routerInst builds an enabled request_router integration connection for tests. +func routerInst(id string) Integration { + return routerInstOn(id, 1) +} + +// routerInstOn builds an enabled request_router connection bound to a specific +// installation id (for multi-installation isolation tests). +func routerInstOn(id string, installID int) Integration { + install := installID + return Integration{ + ID: id, + Name: id, + Enabled: true, + BaseURL: "http://" + id + ".local", + APIKeyRef: "key-" + id, + CapabilityID: "arr", + InstallationID: &install, + } +} + +// autoApproveRouterInst is a router connection that satisfies the auto-approval +// gate (integrationConfigured: an enabled request_router connection bound to an +// installation with a base URL and api key). +func autoApproveRouterInst(id, apiKeyRef string) Integration { + in := routerInst(id) + in.APIKeyRef = apiKeyRef + return in +} + +func TestUpdateIntegrationPassesSiblingsToValidate(t *testing.T) { + store := newFakeStore() + inst := 1 + a := routerInstOn("conn-a", inst) + b := routerInstOn("conn-b", inst) + b.PluginConfig = map[string]any{"service_kind": "radarr"} + store.integrations = []Integration{a, b} + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + + if _, err := service.UpdateIntegration(context.Background(), Viewer{UserID: 1, IsAdmin: true}, Integration{ + ID: "conn-a", + Name: "conn-a", + CapabilityID: "arr", + BaseURL: "http://conn-a.local", + APIKeyRef: "key-conn-a", + InstallationID: &inst, + }); err != nil { + t.Fatalf("UpdateIntegration: %v", err) + } + if len(router.gotValidateSiblings) != 1 { + t.Fatalf("siblings = %d, want 1 (the other installation-1 connection)", len(router.gotValidateSiblings)) + } + sib := router.gotValidateSiblings[0] + if sib.ID != "conn-b" { + t.Fatalf("sibling id = %q, want conn-b (self excluded)", sib.ID) + } + if sib.APIKey != "" || sib.BaseURL != "" { + t.Fatalf("sibling must carry no credentials, got APIKey=%q BaseURL=%q", sib.APIKey, sib.BaseURL) + } + if sib.Config["service_kind"] != "radarr" { + t.Fatalf("sibling config not passed: %+v", sib.Config) + } +} + +func TestAllowedQualities(t *testing.T) { + svc := newTestService(newFakeStore()) + + t.Run("hd ceiling stays 1080p only", func(t *testing.T) { + svcHD := newTestService(newFakeStore()) + svcHD.SetEntitlementResolver(fixedCeiling{q: "1080p"}) + got := svcHD.allowedQualities(context.Background(), Request{}, Settings{}) + if len(got) != 1 || got[0] != Quality1080p { + t.Fatalf("qualities = %v, want [1080p]", got) + } + }) + + t.Run("any/no-cap ceiling adds 2160p", func(t *testing.T) { + // A requester whose max playback quality is "Any" resolves to an empty + // (no-cap) ceiling. Empty means UNLIMITED, so 4K must be requested + // alongside 1080p — it must not be read as "below 4K". + svcAny := newTestService(newFakeStore()) + svcAny.SetEntitlementResolver(fixedCeiling{q: ""}) + got := svcAny.allowedQualities(context.Background(), Request{}, Settings{}) + if len(got) != 2 || got[1] != Quality2160p { + t.Fatalf("qualities = %v, want [1080p 2160p]", got) + } + }) + + t.Run("force dual adds 2160p", func(t *testing.T) { + got := svc.allowedQualities(context.Background(), Request{}, Settings{ForceDualQuality: true}) + if len(got) != 2 || got[1] != Quality2160p { + t.Fatalf("qualities = %v, want [1080p 2160p]", got) + } + }) + + t.Run("4k ceiling adds 2160p", func(t *testing.T) { + svc4k := newTestService(newFakeStore()) + svc4k.SetEntitlementResolver(fixedCeiling{q: "2160p"}) + got := svc4k.allowedQualities(context.Background(), Request{}, Settings{}) + if len(got) != 2 || got[1] != Quality2160p { + t.Fatalf("qualities = %v, want [1080p 2160p]", got) + } + }) } func TestSubmitApprovedFansOutDualQuality(t *testing.T) { store := newFakeStore() - store.integrations = []Integration{ - inst("radarr", "hd", true, false, false), - inst("radarr", "uhd", false, true, false), - } - rec := &recordingMovieAdapter{} + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{} svc := NewService(store, &fakeTMDBClient{}, &fakePresence{}) - svc.SetFulfillmentAdapters(rec, nil) + svc.SetRouterProvider(router) svc.SetEntitlementResolver(fixedCeiling{q: "2160p"}) req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} store.requests["r1"] = &req - if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}); err != nil { + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { t.Fatalf("submit: %v", err) } - if len(rec.ids) != 2 { - t.Fatalf("expected 2 submissions (hd+uhd), got %d: %v", len(rec.ids), rec.ids) + if len(router.gotQualities) != 2 { + t.Fatalf("expected 2 qualities (hd+uhd), got %d: %v", len(router.gotQualities), router.gotQualities) + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 2 { + t.Fatalf("expected 2 persisted targets, got %d", len(targets)) + } +} + +func TestSubmitApprovedSkipsUnconfiguredOptional4KTarget(t *testing.T) { + store := newFakeStore() + hd := routerInst("router-1") + hd.PluginConfig = map[string]any{ + "service_kind": "radarr", + "is_default": true, + "is_4k": false, + } + store.integrations = []Integration{hd} + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + svc.SetEntitlementResolver(fixedCeiling{q: ""}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + if len(router.gotQualities) != 1 || router.gotQualities[0] != Quality1080p { + t.Fatalf("qualities = %v, want only [1080p] when no 4K default is configured", router.gotQualities) + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 1 || targets[0].Quality != Quality1080p || targets[0].Status == StatusFailed { + t.Fatalf("targets = %+v, want one healthy 1080p target", targets) + } +} + +func TestSubmitApprovedUsesConfiguredOptional4KDefault(t *testing.T) { + store := newFakeStore() + hd := routerInst("router-hd") + hd.PluginConfig = map[string]any{ + "service_kind": "radarr", + "is_default": true, + "is_4k": false, + } + uhd := routerInst("router-uhd") + uhd.PluginConfig = map[string]any{ + "service_kind": "radarr", + "is_4k": true, + "is_default_4k": true, + } + store.integrations = []Integration{hd, uhd} + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + svc.SetEntitlementResolver(fixedCeiling{q: "2160p"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + if len(router.gotQualities) != 2 || router.gotQualities[0] != Quality1080p || router.gotQualities[1] != Quality2160p { + t.Fatalf("qualities = %v, want [1080p 2160p]", router.gotQualities) + } +} + +func TestSubmitApprovedNoRouterFails(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + svc := newTestService(store) // no router provider set + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit: %v", err) + } + if got.Outcome != OutcomeFailed { + t.Fatalf("outcome = %q, want failed (no router configured)", got.Outcome) + } +} + +func TestSubmitApprovedNoConnectionsFails(t *testing.T) { + store := newFakeStore() // no integrations + svc := newTestService(store) + svc.SetRouterProvider(&fakeRouterProvider{}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit: %v", err) + } + if got.Outcome != OutcomeFailed { + t.Fatalf("outcome = %q, want failed (no enabled router connections)", got.Outcome) + } +} + +func TestSubmitApprovedZeroTargetsUsesProviderMessage(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + svc := newTestService(store) + svc.SetRouterProvider(&fakeRouterProvider{noTargets: true, fulfillMsg: "no radarr instance configured for 1080p"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit: %v", err) + } + if got.Outcome != OutcomeFailed || got.LastError != "no radarr instance configured for 1080p" { + t.Fatalf("request = %+v, want failed with provider message", got) + } +} + +func TestSubmitApprovedIsIdempotentPerQuality(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + svc.SetEntitlementResolver(fixedCeiling{q: "1080p"}) // HD ceiling -> only 1080p allowed + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + // Seed a healthy 1080p target so the re-run should not re-submit that quality. + if _, err := store.CreateTarget(context.Background(), Target{ + RequestID: "r1", IntegrationID: "router-1", Quality: Quality1080p, Status: StatusQueued, ExternalID: "ext-existing", + }); err != nil { + t.Fatalf("seed: %v", err) + } + + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + // HD ceiling only -> only 1080p is allowed, and it already has a healthy target, + // so Fulfill is never called. + if router.fulfillCalls != 0 { + t.Fatalf("fulfill calls = %d, want 0 (healthy 1080p target already exists)", router.fulfillCalls) + } +} + +func TestSubmitApprovedRecordsDroppedQualityAsFailed(t *testing.T) { + store := newFakeStore() + store.settings.ForceDualQuality = true // want both 1080p and 2160p + store.integrations = []Integration{routerInst("router-1")} + // Plugin fulfills only 1080p, dropping the wanted 2160p. + router := &fakeRouterProvider{targetsOverride: []RouterTarget{{ + Quality: Quality1080p, ConnectionID: "router-1", ExternalID: "ext-hd", ExternalStatus: "queued", Status: StatusQueued, + }}} + svc := newTestService(store) + svc.SetRouterProvider(router) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 2 { + t.Fatalf("targets = %d, want 2 (1080p queued + 2160p failed)", len(targets)) + } + var failed2160 *Target + for i := range targets { + if targets[i].Quality == Quality2160p { + failed2160 = &targets[i] + } + } + if failed2160 == nil || failed2160.Status != StatusFailed { + t.Fatalf("2160p target = %+v, want a failed target", failed2160) + } + if failed2160.LastError != "fulfillment backend returned no target for this quality" { + t.Fatalf("2160p last error = %q, want the no-target message", failed2160.LastError) + } + + // The failed 2160p target is not "healthy", so a re-run (Retry / reconcile) + // re-attempts only that quality. Provide a normal provider for the re-run. + retryRouter := &fakeRouterProvider{} + svc.SetRouterProvider(retryRouter) + cur := *store.requests["r1"] + cur.Status = StatusApproved + cur.Outcome = OutcomeActive + if _, err := svc.submitApprovedRequest(context.Background(), cur, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("retry submit: %v", err) + } + if len(retryRouter.gotQualities) != 1 || retryRouter.gotQualities[0] != Quality2160p { + t.Fatalf("retry qualities = %v, want only [2160p] (1080p is healthy)", retryRouter.gotQualities) + } +} + +func TestSubmitApprovedContainsToSingleInstallation(t *testing.T) { + store := newFakeStore() + // Two enabled router connections on DIFFERENT installations. Only the first + // installation's connections may be sent to that plugin. + store.integrations = []Integration{ + routerInstOn("router-a", 1), + routerInstOn("router-b", 2), + } + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + if router.gotInstallationID != 1 { + t.Fatalf("installation id = %d, want 1 (first eligible)", router.gotInstallationID) + } + if len(router.gotConns) != 1 || router.gotConns[0].ID != "router-a" { + t.Fatalf("connections = %+v, want only installation 1's router-a", router.gotConns) + } +} + +// TestSubmitApprovedContainsToChosenCapability guards that when one installation +// exposes connections for more than one request_router capability sub-id, the +// host hands the plugin only the connections for the FIRST chosen capability — +// never a connection belonging to a different capability of the same installation. +func TestSubmitApprovedContainsToChosenCapability(t *testing.T) { + store := newFakeStore() + arrConn := routerInstOn("arr-conn", 1) // CapabilityID "arr" + seerrConn := routerInstOn("seerr-conn", 1) + seerrConn.CapabilityID = "seerr" + store.integrations = []Integration{arrConn, seerrConn} + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + service.SetEntitlementResolver(fixedCeiling{q: "1080p"}) // single quality, keep it simple + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := service.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + if len(router.gotConns) != 1 { + t.Fatalf("fulfill conns = %d, want 1 (contained to the first chosen capability, not mixed across arr+seerr)", len(router.gotConns)) + } + if router.gotConns[0].ID != "arr-conn" { + t.Fatalf("fulfilled conn = %q, want arr-conn (first chosen)", router.gotConns[0].ID) + } +} + +func TestSubmitApprovedDedupesDuplicateQualityTargets(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + // Misbehaving plugin returns two targets for the same quality. + router := &fakeRouterProvider{targetsOverride: []RouterTarget{ + {Quality: Quality1080p, ConnectionID: "router-1", ExternalID: "ext-1", Status: StatusQueued}, + {Quality: Quality1080p, ConnectionID: "router-1", ExternalID: "ext-2", Status: StatusQueued}, + }} + svc := newTestService(store) + svc.SetRouterProvider(router) + // Pin an HD ceiling: this test is about deduping a duplicate quality, not 4K + // entitlement, so keep it to a single requested quality (1080p). + svc.SetEntitlementResolver(fixedCeiling{q: "1080p"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit returned error: %v", err) + } + if got.Outcome == OutcomeFailed { + t.Fatalf("outcome = failed, want a clean queued aggregate") + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 1 { + t.Fatalf("targets = %d, want 1 (duplicate quality deduped)", len(targets)) + } +} + +func TestSubmitApprovedSkipsMismatchedMediaType(t *testing.T) { + store := newFakeStore() + // Only a series-serving router connection exists; a movie request must not use it. + seriesOnly := routerInst("router-series") + seriesOnly.SupportedMediaTypes = []string{string(MediaTypeSeries)} + store.integrations = []Integration{seriesOnly} + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit: %v", err) + } + if got.Outcome != OutcomeFailed || got.LastError != "no fulfillment backend configured" { + t.Fatalf("request = %+v, want failed with no-backend message", got) + } + if router.fulfillCalls != 0 { + t.Fatalf("fulfill calls = %d, want 0 (series connection filtered out for a movie)", router.fulfillCalls) + } +} + +func TestSubmitApprovedSkipsBadConnectionUsesSibling(t *testing.T) { + store := newFakeStore() + // Two connections on the same installation: one has no api key (unconfigured), + // the other carries a literal key. The healthy sibling must still fulfill the + // request, and the no-key connection must never pin the installation. + bad := routerInstOn("router-bad", 1) + bad.APIKeyRef = "" + good := routerInstOn("router-good", 1) + good.APIKeyRef = "good-key" + store.integrations = []Integration{bad, good} + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit must not abort on a single bad connection: %v", err) + } + if got.Outcome == OutcomeFailed { + t.Fatalf("outcome = failed, want submitted via the healthy sibling") + } + if router.fulfillCalls != 1 { + t.Fatalf("fulfill calls = %d, want 1", router.fulfillCalls) + } + if len(router.gotConns) != 1 || router.gotConns[0].ID != "router-good" || router.gotConns[0].APIKey != "good-key" { + t.Fatalf("router connections = %+v, want only the healthy router-good with resolved key", router.gotConns) + } +} + +func TestSubmitApprovedSkipsConnectionWithEmptyKey(t *testing.T) { + store := newFakeStore() + noKey := routerInstOn("router-nokey", 1) + noKey.APIKeyRef = "" // resolves empty -> must be skipped (never send unauthenticated) + store.integrations = []Integration{noKey} + router := &fakeRouterProvider{} + svc := newTestService(store) + svc.SetRouterProvider(router) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit: %v", err) + } + if got.Outcome != OutcomeFailed { + t.Fatalf("outcome = %q, want failed (no usable connection)", got.Outcome) + } + if router.fulfillCalls != 0 { + t.Fatalf("fulfill calls = %d, want 0 (empty-key connection skipped)", router.fulfillCalls) + } +} + +func TestSubmitApprovedSkipsUnknownQuality(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + // Plugin returns a bogus quality alongside a valid one. + router := &fakeRouterProvider{targetsOverride: []RouterTarget{ + {Quality: Quality("720p"), ConnectionID: "router-1", ExternalID: "ext-bad", Status: StatusQueued}, + {Quality: Quality1080p, ConnectionID: "router-1", ExternalID: "ext-hd", Status: StatusQueued}, + }} + svc := newTestService(store) + svc.SetRouterProvider(router) + // Pin an HD ceiling: this test is about skipping an unknown quality, not 4K + // entitlement, so keep it to a single requested quality (1080p). + svc.SetEntitlementResolver(fixedCeiling{q: "1080p"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 1 { + t.Fatalf("targets = %d, want 1 (720p skipped, 1080p persisted)", len(targets)) + } + for _, tg := range targets { + if tg.Quality == Quality("720p") { + t.Fatalf("a 720p target was persisted: %+v", tg) + } + } +} + +func TestSubmitApprovedSkipsUnknownConnectionTarget(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{targetsOverride: []RouterTarget{ + {Quality: Quality1080p, ConnectionID: "missing-router", ExternalID: "ext-hd", Status: StatusQueued}, + }} + svc := newTestService(store) + svc.SetRouterProvider(router) + svc.SetEntitlementResolver(fixedCeiling{q: "1080p"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit must not abort on an unknown plugin connection: %v", err) + } + if got.Outcome != OutcomeFailed { + t.Fatalf("outcome = %q, want failed missing-quality target", got.Outcome) + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 1 || targets[0].IntegrationID != "" || targets[0].Status != StatusFailed { + t.Fatalf("targets = %+v, want one failed target without unknown integration id", targets) + } +} + +func TestSubmitApprovedCoercesUnknownStatusToQueued(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInst("router-1")} + router := &fakeRouterProvider{targetsOverride: []RouterTarget{ + {Quality: Quality1080p, ConnectionID: "router-1", ExternalID: "ext-hd", Status: Status("bogus")}, + }} + svc := newTestService(store) + svc.SetRouterProvider(router) + // Pin an HD ceiling: this test is about status coercion, not 4K entitlement, + // so keep it to a single requested quality (1080p). + svc.SetEntitlementResolver(fixedCeiling{q: "1080p"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 1 || targets[0].Status != StatusQueued { + t.Fatalf("targets = %+v, want one StatusQueued target (unknown status coerced)", targets) + } +} + +func TestSubmitApprovedSkipsTargetForHealthyQuality(t *testing.T) { + store := newFakeStore() + store.settings.ForceDualQuality = true // want 1080p + 2160p + store.integrations = []Integration{routerInst("router-1")} + // Seed a healthy 1080p target; the plugin (misbehaving) returns one anyway. + if _, err := store.CreateTarget(context.Background(), Target{ + RequestID: "r1", IntegrationID: "router-1", Quality: Quality1080p, Status: StatusQueued, ExternalID: "ext-existing", + }); err != nil { + t.Fatalf("seed: %v", err) + } + router := &fakeRouterProvider{targetsOverride: []RouterTarget{ + {Quality: Quality1080p, ConnectionID: "router-1", ExternalID: "ext-dupe", Status: StatusQueued}, + {Quality: Quality2160p, ConnectionID: "router-1", ExternalID: "ext-uhd", Status: StatusQueued}, + }} + svc := newTestService(store) + svc.SetRouterProvider(router) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit must not error on a duplicate of a healthy quality: %v", err) + } + targets, _ := store.ListTargets(context.Background(), "r1") + if len(targets) != 2 { + t.Fatalf("targets = %d, want 2 (existing 1080p kept + new 2160p; dup 1080p skipped)", len(targets)) + } + count1080 := 0 + for _, tg := range targets { + if tg.Quality == Quality1080p { + count1080++ + } + } + if count1080 != 1 { + t.Fatalf("1080p targets = %d, want 1 (no duplicate persisted)", count1080) + } +} + +func TestSubmitApprovedUnboundInstallationFailsWithGuidance(t *testing.T) { + store := newFakeStore() + // A router connection that exists but is not bound to a plugin installation + // (the migration leaves installation_id NULL for pre-existing rows). + unbound := routerInst("router-unbound") + unbound.InstallationID = nil + store.integrations = []Integration{unbound} + svc := newTestService(store) + svc.SetRouterProvider(&fakeRouterProvider{}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + got, err := svc.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil) + if err != nil { + t.Fatalf("submit: %v", err) + } + if got.Outcome != OutcomeFailed || + got.LastError != "request backend connection is not bound to a plugin installation; re-save it in admin" { + t.Fatalf("request = %+v, want failed with unbound-installation guidance", got) + } +} + +type fakeRequesterIdentity struct { + email, username string + err error + gotUserID int +} + +func (f *fakeRequesterIdentity) ResolveRequester(_ context.Context, userID int) (string, string, error) { + f.gotUserID = userID + return f.email, f.username, f.err +} + +func TestSubmitApprovedPopulatesRequesterIdentity(t *testing.T) { + store := newFakeStore() + store.integrations = []Integration{routerInstOn("router-1", 1)} + router := &fakeRouterProvider{} + service := newTestService(store) + service.SetRouterProvider(router) + service.SetRequesterIdentityResolver(&fakeRequesterIdentity{email: "u@example.com", username: "bob"}) + + req := Request{ID: "r1", MediaType: MediaTypeMovie, Status: StatusApproved, Outcome: OutcomeActive, RequestedByUserID: 7} + store.requests["r1"] = &req + if _, err := service.submitApprovedRequest(context.Background(), req, Viewer{UserID: 7, IsAdmin: true}, nil); err != nil { + t.Fatalf("submit: %v", err) + } + if router.gotRequesterEmail != "u@example.com" || router.gotRequesterUsername != "bob" { + t.Fatalf("descriptor identity = %q/%q, want u@example.com/bob", router.gotRequesterEmail, router.gotRequesterUsername) } } diff --git a/internal/requests/sonarr/client.go b/internal/requests/sonarr/client.go deleted file mode 100644 index af5611b8..00000000 --- a/internal/requests/sonarr/client.go +++ /dev/null @@ -1,183 +0,0 @@ -package sonarr - -import ( - "context" - "fmt" - "net/http" - "net/url" - "strconv" - - mediarequests "github.com/Silo-Server/silo-server/internal/requests" - "github.com/Silo-Server/silo-server/internal/requests/arrclient" -) - -type Client struct { - httpClient *http.Client -} - -type seriesResource struct { - ID int `json:"id,omitempty"` - Title string `json:"title,omitempty"` - TVDBID int `json:"tvdbId,omitempty"` - TMDBID int `json:"tmdbId,omitempty"` - TitleSlug string `json:"titleSlug,omitempty"` - QualityProfileID int `json:"qualityProfileId,omitempty"` - RootFolderPath string `json:"rootFolderPath,omitempty"` - SeasonFolder bool `json:"seasonFolder"` - Monitored bool `json:"monitored"` - SeriesType string `json:"seriesType,omitempty"` - Tags []int `json:"tags,omitempty"` - AddOptions addSeriesOptions `json:"addOptions,omitempty"` -} - -type addSeriesOptions struct { - Monitor string `json:"monitor,omitempty"` - SearchForMissingEpisodes bool `json:"searchForMissingEpisodes"` - SearchForCutoffUnmetEpisodes bool `json:"searchForCutoffUnmetEpisodes,omitempty"` -} - -func NewClient(httpClient *http.Client) *Client { - return &Client{httpClient: httpClient} -} - -func (c *Client) ListSeriesIntegrationOptions(ctx context.Context, integration mediarequests.Integration) (*mediarequests.IntegrationOptions, error) { - client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient) - rootFolders, err := arrclient.ListRootFolders(ctx, client) - if err != nil { - return nil, err - } - qualityProfiles, err := arrclient.ListQualityProfiles(ctx, client) - if err != nil { - return nil, err - } - tags, err := arrclient.ListTags(ctx, client) - if err != nil { - return nil, err - } - return &mediarequests.IntegrationOptions{ - Kind: "sonarr", - RootFolders: rootFolders, - QualityProfiles: qualityProfiles, - Tags: tags, - }, nil -} - -func (c *Client) SubmitSeries(ctx context.Context, req mediarequests.Request, integration mediarequests.Integration) (mediarequests.FulfillmentResult, error) { - if req.MediaType != mediarequests.MediaTypeSeries { - return mediarequests.FulfillmentResult{}, fmt.Errorf("sonarr: request is not a series") - } - if integration.QualityProfileID == nil { - return mediarequests.FulfillmentResult{}, fmt.Errorf("sonarr: quality profile is required") - } - if req.TVDBID == nil || *req.TVDBID <= 0 { - return mediarequests.FulfillmentResult{}, fmt.Errorf("sonarr: tvdb_id is required") - } - - client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient) - series, err := c.lookupSeries(ctx, client, *req.TVDBID) - if err != nil { - return mediarequests.FulfillmentResult{}, err - } - series.RootFolderPath = integration.RootFolder - series.QualityProfileID = *integration.QualityProfileID - series.SeasonFolder = arrclient.BoolOption(integration.Options, "season_folder", true) - series.Monitored = arrclient.BoolOption(integration.Options, "monitored", true) - series.SeriesType = arrclient.StringOption(integration.Options, "series_type", "standard") - series.Tags = integration.Tags - series.AddOptions = addSeriesOptions{ - Monitor: arrclient.StringOption(integration.Options, "monitor", "all"), - SearchForMissingEpisodes: arrclient.BoolOption( - integration.Options, - "search_for_missing_episodes", - arrclient.BoolOption(integration.Options, "search_on_add", true), - ), - SearchForCutoffUnmetEpisodes: arrclient.BoolOption(integration.Options, "search_for_cutoff_unmet", false), - } - - var created seriesResource - if err := client.PostJSON(ctx, "/api/v3/series", series, &created); err != nil { - if !arrclient.IsEmptyOrTruncatedDecodeError(err) { - return mediarequests.FulfillmentResult{}, err - } - // POST accepted but Sonarr returned an empty body. Recover the - // new series' Sonarr ID by listing series filtered by TVDB ID; - // without the ID the reconcile loop cannot advance the request. - if found, lookErr := c.findSeriesByTVDBID(ctx, client, *req.TVDBID); lookErr == nil && found.ID > 0 { - return resultFromSeries(found), nil - } - return arrclient.AcceptedWithoutResponse("sonarr"), nil - } - return resultFromSeries(created), nil -} - -func (c *Client) findSeriesByTVDBID(ctx context.Context, client *arrclient.Client, tvdbID int) (seriesResource, error) { - values := url.Values{} - values.Set("tvdbId", strconv.Itoa(tvdbID)) - var matches []seriesResource - if err := client.GetJSON(ctx, "/api/v3/series?"+values.Encode(), &matches); err != nil { - return seriesResource{}, err - } - for _, s := range matches { - if s.ID > 0 && s.TVDBID == tvdbID { - return s, nil - } - } - return seriesResource{}, fmt.Errorf("sonarr: series not found after add for tvdb_id %d", tvdbID) -} - -func (c *Client) CheckSeriesStatus(ctx context.Context, req mediarequests.Request, integration mediarequests.Integration) (mediarequests.FulfillmentStatus, error) { - client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient) - seriesID, _ := strconv.Atoi(req.ExternalID) - if seriesID <= 0 { - return mediarequests.FulfillmentStatus{ - Status: mediarequests.StatusQueued, - IntegrationKind: "sonarr", - ExternalStatus: "external_id_unavailable", - }, nil - } - - queues, err := c.queueDetails(ctx, client, seriesID) - if err != nil { - return mediarequests.FulfillmentStatus{}, err - } - evaluation := arrclient.EvaluateQueue(queues) - return arrclient.StatusFromQueueEvaluation("sonarr", seriesID, evaluation), nil -} - -func (c *Client) lookupSeries(ctx context.Context, client *arrclient.Client, tvdbID int) (seriesResource, error) { - values := url.Values{} - values.Set("term", "tvdb:"+strconv.Itoa(tvdbID)) - var matches []seriesResource - if err := client.GetJSON(ctx, "/api/v3/series/lookup?"+values.Encode(), &matches); err != nil { - return seriesResource{}, err - } - for _, match := range matches { - if match.TVDBID == tvdbID { - return match, nil - } - } - return seriesResource{}, fmt.Errorf("sonarr: no series found for tvdb_id %d", tvdbID) -} - -func (c *Client) queueDetails(ctx context.Context, client *arrclient.Client, seriesID int) ([]arrclient.QueueResource, error) { - values := url.Values{} - values.Set("seriesId", strconv.Itoa(seriesID)) - var queues []arrclient.QueueResource - if err := client.GetJSON(ctx, "/api/v3/queue/details?"+values.Encode(), &queues); err != nil { - return nil, err - } - return queues, nil -} - -func resultFromSeries(series seriesResource) mediarequests.FulfillmentResult { - externalID := "" - if series.ID > 0 { - externalID = strconv.Itoa(series.ID) - } - return mediarequests.FulfillmentResult{ - IntegrationKind: "sonarr", - ExternalID: externalID, - ExternalStatus: "queued", - } -} - diff --git a/internal/requests/sonarr/client_test.go b/internal/requests/sonarr/client_test.go deleted file mode 100644 index 07ac24ad..00000000 --- a/internal/requests/sonarr/client_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package sonarr - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - mediarequests "github.com/Silo-Server/silo-server/internal/requests" -) - -func TestSubmitSeriesAddsLookupResult(t *testing.T) { - qualityProfileID := 3 - tvdbID := 121361 - var posted seriesResource - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Api-Key"); got != "sonarr-key" { - t.Fatalf("X-Api-Key = %q, want sonarr-key", got) - } - switch r.URL.Path { - case "/api/v3/series": - if r.Method == http.MethodGet { - if got := r.URL.Query().Get("tvdbId"); got != "121361" { - t.Fatalf("tvdbId = %q, want 121361", got) - } - w.Write([]byte(`[]`)) - return - } - if r.Method == http.MethodPost { - if err := json.NewDecoder(r.Body).Decode(&posted); err != nil { - t.Fatalf("decode posted series: %v", err) - } - w.Write([]byte(`{"id":24,"tvdbId":121361}`)) - return - } - case "/api/v3/series/lookup": - if got := r.URL.Query().Get("term"); got != "tvdb:121361" { - t.Fatalf("lookup term = %q, want tvdb:121361", got) - } - w.Write([]byte(`[{"title":"Game of Thrones","tvdbId":121361,"titleSlug":"game-of-thrones"}]`)) - return - } - t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) - })) - defer server.Close() - - client := NewClient(server.Client()) - result, err := client.SubmitSeries(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeSeries, - TMDBID: 1399, - TVDBID: &tvdbID, - Title: "Game of Thrones", - }, mediarequests.Integration{ - Kind: "sonarr", - BaseURL: server.URL, - APIKeyRef: "sonarr-key", - RootFolder: "/series", - QualityProfileID: &qualityProfileID, - Options: map[string]any{ - "monitor": "future", - }, - }) - if err != nil { - t.Fatalf("SubmitSeries returned error: %v", err) - } - if result.ExternalID != "24" || result.IntegrationKind != "sonarr" { - t.Fatalf("result = %+v, want sonarr external id 24", result) - } - if posted.RootFolderPath != "/series" || posted.QualityProfileID != qualityProfileID { - t.Fatalf("posted series = %+v, missing root folder/quality profile", posted) - } - if posted.AddOptions.Monitor != "future" { - t.Fatalf("monitor = %q, want future", posted.AddOptions.Monitor) - } -} - -func TestSubmitSeriesRecoversFromEmptyAddResponse(t *testing.T) { - qualityProfileID := 3 - tvdbID := 121361 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/v3/series/lookup": - w.Write([]byte(`[{"title":"Game of Thrones","tvdbId":121361,"titleSlug":"game-of-thrones"}]`)) - case "/api/v3/series": - if r.Method == http.MethodPost { - w.WriteHeader(http.StatusCreated) - return - } - if r.Method == http.MethodGet { - if got := r.URL.Query().Get("tvdbId"); got != "121361" { - t.Fatalf("tvdbId = %q, want 121361", got) - } - w.Write([]byte(`[{"id":77,"tvdbId":121361,"title":"Game of Thrones"}]`)) - return - } - default: - t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) - } - })) - defer server.Close() - - client := NewClient(server.Client()) - result, err := client.SubmitSeries(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeSeries, - TMDBID: 1399, - TVDBID: &tvdbID, - Title: "Game of Thrones", - }, mediarequests.Integration{ - Kind: "sonarr", - BaseURL: server.URL, - APIKeyRef: "sonarr-key", - RootFolder: "/series", - QualityProfileID: &qualityProfileID, - }) - if err != nil { - t.Fatalf("SubmitSeries returned error: %v", err) - } - if result.ExternalID != "77" { - t.Fatalf("ExternalID = %q, want 77 (recovered after empty 201)", result.ExternalID) - } - if result.ExternalStatus != "queued" { - t.Fatalf("ExternalStatus = %q, want queued", result.ExternalStatus) - } -} - -func TestSubmitSeriesRejectsNonExactTVDBLookupMatch(t *testing.T) { - qualityProfileID := 3 - tvdbID := 121361 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/v3/series/lookup" { - t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) - } - w.Write([]byte(`[{"title":"Wrong Show","tvdbId":999,"titleSlug":"wrong-show"}]`)) - })) - defer server.Close() - - client := NewClient(server.Client()) - _, err := client.SubmitSeries(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeSeries, - TMDBID: 1399, - TVDBID: &tvdbID, - Title: "Game of Thrones", - }, mediarequests.Integration{ - Kind: "sonarr", - BaseURL: server.URL, - APIKeyRef: "sonarr-key", - RootFolder: "/series", - QualityProfileID: &qualityProfileID, - }) - if err == nil { - t.Fatal("expected error for non-exact TVDB lookup match") - } -} - -func TestCheckSeriesStatusReadsQueueDetails(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Api-Key"); got != "sonarr-key" { - t.Fatalf("X-Api-Key = %q, want sonarr-key", got) - } - if r.URL.Path != "/api/v3/queue/details" { - t.Fatalf("unexpected path %s", r.URL.Path) - } - if got := r.URL.Query().Get("seriesId"); got != "24" { - t.Fatalf("seriesId = %q, want 24", got) - } - w.Write([]byte(`[{"seriesId":24,"status":"downloading","trackedDownloadState":"importing"}]`)) - })) - defer server.Close() - - client := NewClient(server.Client()) - status, err := client.CheckSeriesStatus(context.Background(), mediarequests.Request{ - MediaType: mediarequests.MediaTypeSeries, - TMDBID: 1399, - ExternalID: "24", - }, mediarequests.Integration{ - Kind: "sonarr", - BaseURL: server.URL, - APIKeyRef: "sonarr-key", - }) - if err != nil { - t.Fatalf("CheckSeriesStatus returned error: %v", err) - } - if status.Status != mediarequests.StatusDownloading || status.ExternalStatus != "downloading/importing" { - t.Fatalf("status = %+v, want downloading", status) - } -} - -func TestListSeriesIntegrationOptionsLoadsChoices(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Api-Key"); got != "sonarr-key" { - t.Fatalf("X-Api-Key = %q, want sonarr-key", got) - } - switch r.URL.Path { - case "/api/v3/rootfolder": - w.Write([]byte(`[{"path":"/series","freeSpace":123,"totalSpace":456,"accessible":true}]`)) - case "/api/v3/qualityprofile": - w.Write([]byte(`[{"id":3,"name":"HD-1080p"}]`)) - case "/api/v3/tag": - w.Write([]byte(`[{"id":4,"label":"requests"}]`)) - default: - t.Fatalf("unexpected path %s", r.URL.Path) - } - })) - defer server.Close() - - client := NewClient(server.Client()) - options, err := client.ListSeriesIntegrationOptions(context.Background(), mediarequests.Integration{ - Kind: "sonarr", - BaseURL: server.URL, - APIKeyRef: "sonarr-key", - }) - if err != nil { - t.Fatalf("ListSeriesIntegrationOptions returned error: %v", err) - } - if options.Kind != "sonarr" { - t.Fatalf("kind = %q, want sonarr", options.Kind) - } - if len(options.RootFolders) != 1 || options.RootFolders[0].Path != "/series" { - t.Fatalf("root folders = %+v, want /series", options.RootFolders) - } - if len(options.QualityProfiles) != 1 || options.QualityProfiles[0].ID != 3 { - t.Fatalf("quality profiles = %+v, want id 3", options.QualityProfiles) - } - if len(options.Tags) != 1 || options.Tags[0].ID != 4 { - t.Fatalf("tags = %+v, want id 4", options.Tags) - } -} diff --git a/internal/requests/types.go b/internal/requests/types.go index 6e0d6676..e032a72d 100644 --- a/internal/requests/types.go +++ b/internal/requests/types.go @@ -139,6 +139,8 @@ type Request struct { Outcome Outcome `json:"outcome"` RequestedByUserID int `json:"requested_by_user_id,omitempty"` RequestedByProfileID string `json:"requested_by_profile_id,omitempty"` + RequesterEmail string `json:"-"` + RequesterUsername string `json:"-"` IntegrationKind string `json:"integration_kind,omitempty"` IsAnime bool `json:"is_anime"` Targets []Target `json:"targets,omitempty"` @@ -254,51 +256,19 @@ type ListFilter struct { } type Integration struct { - ID string `json:"id"` - Name string `json:"name"` - Kind string `json:"kind"` - Enabled bool `json:"enabled"` - Is4K bool `json:"is_4k"` - IsDefault bool `json:"is_default"` - IsDefault4K bool `json:"is_default_4k"` - AnimeEnabled bool `json:"anime_enabled"` - AnimeQualityProfileID *int `json:"anime_quality_profile_id,omitempty"` - AnimeRootFolder string `json:"anime_root_folder,omitempty"` - AnimeTags []int `json:"anime_tags"` - BaseURL string `json:"base_url"` - APIKeyRef string `json:"api_key_ref,omitempty"` - RootFolder string `json:"root_folder"` - QualityProfileID *int `json:"quality_profile_id,omitempty"` - Tags []int `json:"tags"` - Options map[string]any `json:"options"` - LastCheckAt *time.Time `json:"last_check_at,omitempty"` - LastCheckStatus string `json:"last_check_status,omitempty"` - LastCheckError string `json:"last_check_error,omitempty"` - UpdatedAt time.Time `json:"updated_at"` -} - -type IntegrationRootFolder struct { - Path string `json:"path"` - FreeSpace int64 `json:"free_space,omitempty"` - TotalSpace int64 `json:"total_space,omitempty"` - Accessible bool `json:"accessible"` -} - -type IntegrationQualityProfile struct { - ID int `json:"id"` - Name string `json:"name"` -} - -type IntegrationTag struct { - ID int `json:"id"` - Label string `json:"label"` -} - -type IntegrationOptions struct { - Kind string `json:"kind"` - RootFolders []IntegrationRootFolder `json:"root_folders"` - QualityProfiles []IntegrationQualityProfile `json:"quality_profiles"` - Tags []IntegrationTag `json:"tags"` + ID string `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + BaseURL string `json:"base_url"` + APIKeyRef string `json:"api_key_ref,omitempty"` + LastCheckAt *time.Time `json:"last_check_at,omitempty"` + LastCheckStatus string `json:"last_check_status,omitempty"` + LastCheckError string `json:"last_check_error,omitempty"` + UpdatedAt time.Time `json:"updated_at"` + CapabilityID string `json:"capability_id"` + InstallationID *int `json:"installation_id,omitempty"` + SupportedMediaTypes []string `json:"supported_media_types"` + PluginConfig map[string]any `json:"plugin_config"` } type FulfillmentResult struct { diff --git a/internal/scanner/audiobook.go b/internal/scanner/audiobook.go index a9bbe4f9..2a59cf09 100644 --- a/internal/scanner/audiobook.go +++ b/internal/scanner/audiobook.go @@ -48,7 +48,7 @@ func stripNarratorSuffix(title string) string { // The scanner write path (Task 8) converts this into media_items + // media_files + item_people rows. type parsedAudiobook struct { - Title string + Title string Author string Narrator string Series string diff --git a/internal/scanner/audiobook_cover.go b/internal/scanner/audiobook_cover.go index f1d6d797..6a1440e6 100644 --- a/internal/scanner/audiobook_cover.go +++ b/internal/scanner/audiobook_cover.go @@ -4,11 +4,18 @@ import ( "bytes" "context" "fmt" + "io" "log/slog" + "os" "os/exec" + "path/filepath" "strings" + + "github.com/Silo-Server/silo-server/internal/catalog" ) +const maxAudiobookSidecarCoverSize = 8 * 1024 * 1024 + // audiobookCoverCacher is the narrow slice of imagecache.Cacher the // audiobook scanner uses. Defined with scalar args (not the imagecache // request struct) to avoid an import cycle: imagecache imports metadata @@ -17,6 +24,15 @@ type audiobookCoverCacher interface { CacheAudiobookCover(ctx context.Context, data []byte, contentID string) (basePath string, ext string, thumbhash string, err error) } +type ebookCoverCacher interface { + CacheEbookCover(ctx context.Context, data []byte, contentID string) (basePath string, ext string, thumbhash string, err error) +} + +type audiobookCoverMetadataStore interface { + GetPosterPath(ctx context.Context, contentID string) (string, error) + UpdateMetadata(ctx context.Context, contentID string, upd *catalog.MetadataUpdate) error +} + // FFmpegPathFromFFprobe derives the ffmpeg binary path from a configured // ffprobe path. They live side by side in every silo deployment. func FFmpegPathFromFFprobe(ffprobePath string) string { @@ -76,3 +92,101 @@ func ExtractAndUploadAudiobookCover( } return fmt.Sprintf("%s/original%s", basePath, ext), thumbhash } + +func applyAudiobookSidecarCover(ctx context.Context, store audiobookCoverMetadataStore, cacher audiobookCoverCacher, contentID string, folderPath string) error { + if store == nil || cacher == nil || contentID == "" || folderPath == "" { + return nil + } + cover, _, err := findSidecarAudiobookCover(folderPath) + if err != nil || len(cover) == 0 { + return err + } + existingPosterPath, err := store.GetPosterPath(ctx, contentID) + if err != nil { + return fmt.Errorf("get audiobook poster path for cover: %w", err) + } + if strings.TrimSpace(existingPosterPath) != "" { + return nil + } + basePath, ext, thumbhash, err := cacher.CacheAudiobookCover(ctx, cover, contentID) + if err != nil { + return err + } + posterPath := strings.TrimRight(basePath, "/") + "/original" + ext + update := &catalog.MetadataUpdate{PosterPath: &posterPath} + if thumbhash != "" { + update.PosterThumbhash = &thumbhash + } + return store.UpdateMetadata(ctx, contentID, update) +} + +var sidecarAudiobookCoverNames = []string{"cover", "folder", "front", "poster", "thumbnail"} +var sidecarAudiobookCoverExtensions = []string{".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".bmp"} + +func findSidecarAudiobookCover(dir string) ([]byte, string, error) { + if dir == "" { + return nil, "", nil + } + entries, err := os.ReadDir(dir) + if err != nil { + return nil, "", err + } + byName := make(map[string]string, len(entries)) + for _, entry := range entries { + if !isRegularDirEntry(entry) { + continue + } + byName[strings.ToLower(entry.Name())] = filepath.Join(dir, entry.Name()) + } + for _, name := range sidecarAudiobookCoverNames { + for _, ext := range sidecarAudiobookCoverExtensions { + path := byName[name+ext] + if path == "" { + continue + } + data, err := readSidecarAudiobookCover(path) + if err != nil { + return nil, path, err + } + return data, path, nil + } + } + return nil, "", nil +} + +func isRegularDirEntry(entry os.DirEntry) bool { + if entry == nil || entry.IsDir() || entry.Type()&os.ModeSymlink != 0 { + return false + } + info, err := entry.Info() + if err != nil { + return false + } + return info.Mode().IsRegular() +} + +func readSidecarAudiobookCover(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return nil, err + } + if info.Size() > maxAudiobookSidecarCoverSize { + return nil, fmt.Errorf("audiobook sidecar cover too large: %s", path) + } + data, err := io.ReadAll(io.LimitReader(file, maxAudiobookSidecarCoverSize+1)) + if err != nil { + return nil, err + } + if len(data) > maxAudiobookSidecarCoverSize { + return nil, fmt.Errorf("audiobook sidecar cover too large: %s", path) + } + if len(data) == 0 { + return nil, fmt.Errorf("audiobook sidecar cover empty: %s", path) + } + return data, nil +} diff --git a/internal/scanner/audiobook_cover_test.go b/internal/scanner/audiobook_cover_test.go new file mode 100644 index 00000000..5fea0788 --- /dev/null +++ b/internal/scanner/audiobook_cover_test.go @@ -0,0 +1,111 @@ +package scanner + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/Silo-Server/silo-server/internal/catalog" +) + +type fakeAudiobookCoverCacher struct { + calls int + data []byte + contentID string + err error +} + +func (f *fakeAudiobookCoverCacher) CacheAudiobookCover(_ context.Context, data []byte, contentID string) (string, string, string, error) { + f.calls++ + f.data = append([]byte(nil), data...) + f.contentID = contentID + if f.err != nil { + return "", "", "", f.err + } + return "local/audiobooks/" + contentID + "/poster", ".webp", "thumb", nil +} + +type fakeAudiobookCoverStore struct { + posterPath string + getErr error + contentID string + update *catalog.MetadataUpdate + err error +} + +func (f *fakeAudiobookCoverStore) GetPosterPath(_ context.Context, _ string) (string, error) { + return f.posterPath, f.getErr +} + +func (f *fakeAudiobookCoverStore) UpdateMetadata(_ context.Context, contentID string, update *catalog.MetadataUpdate) error { + f.contentID = contentID + f.update = update + return f.err +} + +func TestApplyAudiobookSidecarCoverCachesFolderCover(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "folder.png"), []byte("audiobook-sidecar-cover"), 0o644); err != nil { + t.Fatalf("write cover: %v", err) + } + cacher := &fakeAudiobookCoverCacher{} + store := &fakeAudiobookCoverStore{} + + err := applyAudiobookSidecarCover(context.Background(), store, cacher, "content-1", dir) + if err != nil { + t.Fatalf("applyAudiobookSidecarCover: %v", err) + } + + if cacher.calls != 1 || string(cacher.data) != "audiobook-sidecar-cover" { + t.Fatalf("cache call = calls %d data %q", cacher.calls, string(cacher.data)) + } + if store.update == nil || store.update.PosterPath == nil || *store.update.PosterPath != "local/audiobooks/content-1/poster/original.webp" { + t.Fatalf("poster update = %#v", store.update) + } + if store.update.PosterThumbhash == nil || *store.update.PosterThumbhash != "thumb" { + t.Fatalf("poster thumbhash = %#v", store.update.PosterThumbhash) + } +} + +func TestApplyAudiobookSidecarCoverPreservesExistingPoster(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), []byte("audiobook-sidecar-cover"), 0o644); err != nil { + t.Fatalf("write cover: %v", err) + } + cacher := &fakeAudiobookCoverCacher{} + store := &fakeAudiobookCoverStore{ + posterPath: "provider/poster.webp", + } + + err := applyAudiobookSidecarCover(context.Background(), store, cacher, "content-1", dir) + if err != nil { + t.Fatalf("applyAudiobookSidecarCover: %v", err) + } + + if cacher.calls != 0 { + t.Fatalf("cache calls = %d, want 0", cacher.calls) + } + if store.update != nil { + t.Fatalf("unexpected update = %#v", store.update) + } +} + +func TestFindSidecarAudiobookCoverSkipsSymlinks(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "secret.jpg") + if err := os.WriteFile(target, []byte("not-a-cover"), 0o644); err != nil { + t.Fatalf("write target: %v", err) + } + if err := os.Symlink(target, filepath.Join(dir, "cover.jpg")); err != nil { + t.Fatalf("symlink cover: %v", err) + } + + data, path, err := findSidecarAudiobookCover(dir) + if err != nil { + t.Fatalf("findSidecarAudiobookCover: %v", err) + } + if data != nil || path != "" { + t.Fatalf("findSidecarAudiobookCover returned data %q path %q, want no cover", string(data), path) + } +} diff --git a/internal/scanner/audiobook_scan.go b/internal/scanner/audiobook_scan.go index 8000d153..68fe66e0 100644 --- a/internal/scanner/audiobook_scan.go +++ b/internal/scanner/audiobook_scan.go @@ -316,6 +316,14 @@ func (s *Scanner) reconcileAudiobookFolder(ctx context.Context, folder *models.M if err := s.upsertAudiobookMediaFiles(ctx, folder, contentID, folderPath, parsed); err != nil { return fmt.Errorf("upsert audiobook files: %w", err) } + if err := applyAudiobookSidecarCover(ctx, s.itemRepo, s.imageCacher, contentID, folderPath); err != nil { + slog.Warn("audiobook scan: sidecar cover upload failed", + "folder_id", folder.ID, + "content_id", contentID, + "path", folderPath, + "error", err, + ) + } if err := s.upsertAudiobookPeople(ctx, contentID, parsed); err != nil { return fmt.Errorf("upsert audiobook people: %w", err) } diff --git a/internal/scanner/audiobook_test.go b/internal/scanner/audiobook_test.go index 36459b9c..a438e6c1 100644 --- a/internal/scanner/audiobook_test.go +++ b/internal/scanner/audiobook_test.go @@ -206,6 +206,77 @@ func TestScanAudiobookFolderReturnsCanceledContext(t *testing.T) { } } +func TestScanSubtreeAudiobookLibraryUsesAudiobookPipeline(t *testing.T) { + root := t.TempDir() + bookDir := filepath.Join(root, "bad-book") + if err := os.Mkdir(bookDir, 0o755); err != nil { + t.Fatalf("mkdir book dir: %v", err) + } + if err := os.WriteFile(filepath.Join(bookDir, "chapter.mp3"), []byte("not real audio"), 0o644); err != nil { + t.Fatalf("write fake audio: %v", err) + } + + s := &Scanner{ffprobePath: "definitely-missing-ffprobe"} + _, err := s.ScanSubtree(context.Background(), &models.MediaFolder{ID: 42, Type: "audiobooks"}, bookDir) + if err == nil { + t.Fatal("ScanSubtree returned nil, want audiobook parse failure") + } + if strings.Contains(err.Error(), "getting existing files") { + t.Fatalf("ScanSubtree used generic file pipeline: %v", err) + } + if !strings.Contains(err.Error(), "folder_id=42") { + t.Fatalf("error = %q, want audiobook scanner aggregate failure", err) + } +} + +func TestScanSubtreeAudiobookLibraryRejectsInvalidScopedRoot(t *testing.T) { + s := &Scanner{} + + _, err := s.ScanSubtree(context.Background(), &models.MediaFolder{ID: 42, Type: "audiobooks"}, "") + if err == nil { + t.Fatal("ScanSubtree returned nil, want invalid root error") + } + if !strings.Contains(err.Error(), "invalid audiobook scan root") { + t.Fatalf("error = %q, want invalid audiobook scan root", err) + } +} + +func TestScanFileAudiobookLibraryUsesAudiobookPipeline(t *testing.T) { + root := t.TempDir() + bookDir := filepath.Join(root, "bad-book") + if err := os.Mkdir(bookDir, 0o755); err != nil { + t.Fatalf("mkdir book dir: %v", err) + } + filePath := filepath.Join(bookDir, "chapter.mp3") + if err := os.WriteFile(filePath, []byte("not real audio"), 0o644); err != nil { + t.Fatalf("write fake audio: %v", err) + } + + s := &Scanner{ffprobePath: "definitely-missing-ffprobe"} + err := s.ScanFile(context.Background(), filePath, &models.MediaFolder{ID: 42, Type: "audiobooks"}) + if err == nil { + t.Fatal("ScanFile returned nil, want audiobook parse failure") + } + if strings.Contains(err.Error(), "unrecognized video extension") { + t.Fatalf("ScanFile used video extension gate: %v", err) + } + if !strings.Contains(err.Error(), "folder_id=42") { + t.Fatalf("error = %q, want audiobook scanner aggregate failure", err) + } +} + +func TestScanFileAudiobookLibraryRejectsRelativeScopedRoot(t *testing.T) { + s := &Scanner{} + + err := s.ScanFile(context.Background(), "chapter.mp3", &models.MediaFolder{ID: 42, Type: "audiobooks"}) + if err == nil { + t.Fatal("ScanFile returned nil, want invalid root error") + } + if !strings.Contains(err.Error(), "invalid audiobook scan root") { + t.Fatalf("error = %q, want invalid audiobook scan root", err) + } +} + func TestResolveAudiobookMediaItemReusesRootScopedContentID(t *testing.T) { finder := &fakeRootContentFinder{contentID: "book-root-id"} writer := &fakeFilesystemItemWriter{} diff --git a/internal/scanner/ebook.go b/internal/scanner/ebook.go new file mode 100644 index 00000000..96c93827 --- /dev/null +++ b/internal/scanner/ebook.go @@ -0,0 +1,1110 @@ +package scanner + +import ( + "archive/zip" + "bytes" + "encoding/hex" + "encoding/xml" + "fmt" + "html" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + "unicode" + "unicode/utf8" + + xhtml "golang.org/x/net/html" + "golang.org/x/net/html/charset" + "golang.org/x/text/encoding/charmap" + textunicode "golang.org/x/text/encoding/unicode" +) + +const maxEPUBMetadataEntrySize = 8 * 1024 * 1024 +const maxPDFMetadataScanSize = 2 * 1024 * 1024 + +var ebookExtensions = map[string]bool{ + ".epub": true, + ".pdf": true, + ".mobi": true, + ".azw": true, + ".azw3": true, + ".fb2": true, + ".fbz": true, + ".cbz": true, + ".cbr": true, +} + +type parsedEbook struct { + Format string + Title string + Authors []string + Description string + Publisher string + PublishedAt time.Time + Year int + Language string + ISBN string + Series string + SeriesIndex string + Genres []string + PageCount int + Cover *parsedEbookCover +} + +type parsedEbookCover struct { + ContentType string + Bytes []byte +} + +func SupportsEbookFile(filePath string) bool { + if strings.HasSuffix(strings.ToLower(filePath), ".fb2.zip") { + return true + } + return ebookExtensions[strings.ToLower(filepath.Ext(filePath))] +} + +func parseEbookFile(path string) (book parsedEbook, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic parsing ebook %s: %v", path, r) + } + }() + format := ebookFileFormat(path) + switch format { + case ".epub": + book, err = parseEbookEPUB(path) + case ".fb2": + book, err = parseEbookFB2(path) + case ".fbz": + book, err = parseEbookFBZ(path) + case ".cbz": + book, err = parseEbookCBZ(path) + case ".pdf": + book, err = parseEbookPDF(path) + case ".mobi", ".azw", ".azw3", ".cbr": + book = parsedEbook{Format: strings.TrimPrefix(format, ".")} + default: + err = fmt.Errorf("unsupported ebook format: %s", filepath.Ext(path)) + } + book.sanitize() + return book, err +} + +func parseEbookPDF(path string) (parsedEbook, error) { + book := parsedEbook{Format: "pdf"} + file, err := os.Open(path) + if err != nil { + return book, err + } + defer file.Close() + + head, tail, err := readPDFMetadataWindows(file) + if err != nil { + return book, err + } + info := parsePDFInfoFields(head) + // A head match comes from a linearized PDF whose Info dictionary sits at + // the start of the file and is authoritative; non-linearized PDFs (the + // common case) store the Info dictionary near the end and usually have no + // head match at all. The tail therefore only fills keys the head did not + // produce, so key-shaped byte noise inside trailing compressed streams can + // never replace a good head value. + for key, value := range parsePDFInfoFields(tail) { + if value != "" && info[key] == "" { + info[key] = value + } + } + book.Title = info["Title"] + book.Authors = splitEbookAuthors(info["Author"]) + book.Description = info["Subject"] + book.Genres = splitPDFKeywords(info["Keywords"]) + for _, value := range []string{ + info["ISBN"], + info["Subject"], + info["Keywords"], + info["Title"], + } { + if isbn := normalizeEbookISBN(value); isbn != "" { + book.ISBN = isbn + break + } + } + if t, ok := parsePDFDate(info["CreationDate"]); ok { + book.PublishedAt = t + book.Year = t.Year() + } + return book, nil +} + +// readPDFMetadataWindows reads the first and last maxPDFMetadataScanSize bytes +// of the file. The windows never overlap: for files at most one window long +// the tail is nil, and for files shorter than two windows the tail starts +// where the head ends. +func readPDFMetadataWindows(file *os.File) (head []byte, tail []byte, err error) { + head, err = io.ReadAll(io.LimitReader(file, maxPDFMetadataScanSize)) + if err != nil { + return nil, nil, err + } + info, err := file.Stat() + if err != nil { + return nil, nil, err + } + size := info.Size() + if size <= int64(len(head)) { + return head, nil, nil + } + tailStart := size - maxPDFMetadataScanSize + if tailStart < int64(len(head)) { + tailStart = int64(len(head)) + } + tail = make([]byte, size-tailStart) + if _, err := file.ReadAt(tail, tailStart); err != nil { + return nil, nil, err + } + return head, tail, nil +} + +func ebookFileFormat(path string) string { + if strings.HasSuffix(strings.ToLower(path), ".fb2.zip") { + return ".fbz" + } + return strings.ToLower(filepath.Ext(path)) +} + +func (b *parsedEbook) sanitize() { + b.Format = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(b.Format)), ".") + b.Title = strings.TrimSpace(b.Title) + b.Description = cleanEbookDescription(b.Description) + b.Publisher = strings.TrimSpace(b.Publisher) + b.Language = strings.TrimSpace(b.Language) + b.ISBN = normalizeEbookISBN(b.ISBN) + b.Series = strings.TrimSpace(b.Series) + b.SeriesIndex = strings.TrimSpace(b.SeriesIndex) + b.Authors = uniqueTrimmedStrings(b.Authors) + b.Genres = uniqueTrimmedStrings(b.Genres) + if b.PageCount < 0 { + b.PageCount = 0 + } + if b.Year == 0 && !b.PublishedAt.IsZero() { + b.Year = b.PublishedAt.Year() + } +} + +func cleanEbookDescription(value string) string { + value = strings.TrimSpace(html.UnescapeString(value)) + if value == "" { + return "" + } + if !strings.Contains(value, "<") || !strings.Contains(value, ">") { + return strings.Join(strings.Fields(value), " ") + } + + tokenizer := xhtml.NewTokenizer(strings.NewReader(value)) + var out strings.Builder + needsSpace := false + writeSpace := func() { + if out.Len() > 0 && !needsSpace { + needsSpace = true + } + } + for { + switch tokenizer.Next() { + case xhtml.ErrorToken: + return strings.Join(strings.Fields(out.String()), " ") + case xhtml.TextToken: + text := strings.TrimSpace(html.UnescapeString(string(tokenizer.Text()))) + if text == "" { + continue + } + if out.Len() > 0 && (needsSpace || !startsWithClosingPunctuation(text)) { + out.WriteByte(' ') + } + out.WriteString(text) + needsSpace = false + case xhtml.StartTagToken, xhtml.SelfClosingTagToken, xhtml.EndTagToken: + name, _ := tokenizer.TagName() + switch strings.ToLower(string(name)) { + case "br", "p", "div", "section", "article", "li", "ul", "ol", "blockquote", "tr": + writeSpace() + } + } + } +} + +func startsWithClosingPunctuation(value string) bool { + for _, r := range value { + switch r { + case '.', ',', ';', ':', '!', '?', ')', ']', '}': + return true + default: + return false + } + } + return false +} + +func looksLikeHTML(value string) bool { + value = strings.TrimSpace(value) + return strings.Contains(value, "<") && strings.Contains(value, ">") +} + +func uniqueTrimmedStrings(values []string) []string { + seen := map[string]struct{}{} + var out []string + for _, value := range values { + trimmed := strings.TrimSpace(value) + key := strings.ToLower(trimmed) + if key == "" { + continue + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + out = append(out, trimmed) + } + return out +} + +func normalizeEbookISBN(value string) string { + value = strings.ToUpper(strings.TrimSpace(value)) + for _, prefix := range []string{"ISBN-13", "ISBN-10", "ISBN"} { + if strings.HasPrefix(value, prefix) { + value = strings.TrimSpace(strings.TrimPrefix(value, prefix)) + value = strings.TrimLeft(value, ": ") + break + } + } + var out strings.Builder + for _, r := range value { + if r >= '0' && r <= '9' { + out.WriteRune(r) + continue + } + if r == 'X' { + out.WriteRune(r) + } + } + candidate := out.String() + switch len(candidate) { + case 10: + if validISBN10(candidate) { + return candidate + } + case 13: + if validISBN13(candidate) { + return candidate + } + } + return "" +} + +func validISBN10(candidate string) bool { + if len(candidate) != 10 { + return false + } + sum := 0 + for i, r := range candidate { + value := 0 + switch { + case r >= '0' && r <= '9': + value = int(r - '0') + case r == 'X' && i == 9: + value = 10 + default: + return false + } + sum += value * (10 - i) + } + return sum%11 == 0 +} + +func validISBN13(candidate string) bool { + if len(candidate) != 13 { + return false + } + sum := 0 + for i, r := range candidate { + if r < '0' || r > '9' { + return false + } + value := int(r - '0') + if i%2 == 1 { + value *= 3 + } + sum += value + } + return sum%10 == 0 +} + +func parseEbookEPUB(path string) (parsedEbook, error) { + book := parsedEbook{Format: "epub"} + reader, err := zip.OpenReader(path) + if err != nil { + return book, err + } + defer reader.Close() + + container, err := readEPUBZipEntry(&reader.Reader, "META-INF/container.xml") + if err != nil { + return book, err + } + opfPath, err := epubOPFPath(container) + if err != nil { + return book, err + } + opf, err := readEPUBZipEntry(&reader.Reader, opfPath) + if err != nil { + return book, err + } + if err := parseEPUBOPFMetadata(opf, &book); err != nil { + return book, err + } + if cover, err := extractEPUBCover(&reader.Reader, opfPath, opf); err == nil { + book.Cover = cover + } + return book, nil +} + +func parseEbookFB2(path string) (parsedEbook, error) { + book := parsedEbook{Format: "fb2"} + file, err := os.Open(path) + if err != nil { + return book, err + } + defer file.Close() + // Mirror the .fbz entry cap so a plain .fb2 cannot stream unbounded + // bytes through the XML decoder. + info, err := file.Stat() + if err != nil { + return book, err + } + if info.Size() > maxEPUBMetadataEntrySize { + return book, fmt.Errorf("fb2 file too large: %s", path) + } + return parseEbookFB2Reader(io.LimitReader(file, maxEPUBMetadataEntrySize+1), "fb2") +} + +func parseEbookFBZ(path string) (parsedEbook, error) { + book := parsedEbook{Format: "fbz"} + reader, err := zip.OpenReader(path) + if err != nil { + return book, err + } + defer reader.Close() + + for _, file := range reader.File { + if !strings.HasSuffix(strings.ToLower(file.Name), ".fb2") { + continue + } + if file.UncompressedSize64 > maxEPUBMetadataEntrySize { + return book, fmt.Errorf("fbz entry too large: %s", file.Name) + } + entry, err := file.Open() + if err != nil { + return book, err + } + defer entry.Close() + return parseEbookFB2Reader(io.LimitReader(entry, maxEPUBMetadataEntrySize+1), "fbz") + } + return book, fmt.Errorf("fbz archive has no fb2 entry") +} + +func parseEbookCBZ(path string) (parsedEbook, error) { + book := parsedEbook{Format: "cbz"} + reader, err := zip.OpenReader(path) + if err != nil { + return book, err + } + defer reader.Close() + + var coverPage *zip.File + var coverKey string + for _, file := range reader.File { + if !isComicArchivePage(file.Name) { + continue + } + book.PageCount++ + key := normalizedArchivePath(file.Name) + if coverPage == nil || naturalPathLess(key, coverKey) { + coverPage, coverKey = file, key + } + } + if coverPage != nil { + if cover, err := readArchiveImageCover(coverPage); err == nil { + book.Cover = cover + } + } + return book, nil +} + +// naturalPathLess orders archive entry names case-insensitively with digit +// runs compared numerically, so unpadded page numbers ("2.jpg" before +// "10.jpg") and chapter directories ("ch2/" before "ch10/") sort in reading +// order instead of byte order. +func naturalPathLess(a, b string) bool { + for len(a) > 0 && len(b) > 0 { + if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) { + aRun, aRest := splitDigitRun(a) + bRun, bRest := splitDigitRun(b) + aNum := strings.TrimLeft(aRun, "0") + bNum := strings.TrimLeft(bRun, "0") + if len(aNum) != len(bNum) { + return len(aNum) < len(bNum) + } + if aNum != bNum { + return aNum < bNum + } + a, b = aRest, bRest + continue + } + ar, aSize := utf8.DecodeRuneInString(a) + br, bSize := utf8.DecodeRuneInString(b) + al, bl := unicode.ToLower(ar), unicode.ToLower(br) + if al != bl { + return al < bl + } + a, b = a[aSize:], b[bSize:] + } + return len(a) < len(b) +} + +func isASCIIDigit(b byte) bool { + return b >= '0' && b <= '9' +} + +func splitDigitRun(s string) (run string, rest string) { + i := 0 + for i < len(s) && isASCIIDigit(s[i]) { + i++ + } + return s[:i], s[i:] +} + +func isComicArchivePage(name string) bool { + clean := normalizedArchivePath(name) + if clean == "" || strings.HasSuffix(clean, "/") { + return false + } + base := strings.ToLower(filepath.Base(clean)) + if strings.HasPrefix(base, "._") { + return false + } + parts := strings.Split(strings.ToLower(clean), "/") + for _, part := range parts { + if part == "__macosx" { + return false + } + } + switch filepath.Ext(base) { + case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif", ".bmp": + return true + default: + return false + } +} + +func normalizedArchivePath(name string) string { + return strings.TrimSpace(strings.ReplaceAll(name, "\\", "/")) +} + +func readArchiveImageCover(file *zip.File) (*parsedEbookCover, error) { + if file == nil { + return nil, fmt.Errorf("nil archive image") + } + if file.UncompressedSize64 > maxEPUBMetadataEntrySize { + return nil, fmt.Errorf("archive cover entry too large: %s", file.Name) + } + entry, err := file.Open() + if err != nil { + return nil, err + } + defer entry.Close() + data, err := io.ReadAll(io.LimitReader(entry, maxEPUBMetadataEntrySize+1)) + if err != nil { + return nil, err + } + if len(data) > maxEPUBMetadataEntrySize { + return nil, fmt.Errorf("archive cover entry too large: %s", file.Name) + } + if len(data) == 0 { + return nil, fmt.Errorf("archive cover entry empty: %s", file.Name) + } + return &parsedEbookCover{ + ContentType: ebookImageContentType(file.Name), + Bytes: data, + }, nil +} + +func parseEbookFB2Reader(reader io.Reader, format string) (parsedEbook, error) { + book := parsedEbook{Format: format} + var fb2 struct { + Description struct { + TitleInfo struct { + Genres []string `xml:"genre"` + Authors []struct { + FirstName string `xml:"first-name"` + MiddleName string `xml:"middle-name"` + LastName string `xml:"last-name"` + Nickname string `xml:"nickname"` + } `xml:"author"` + BookTitle string `xml:"book-title"` + Lang string `xml:"lang"` + Date struct { + Value string `xml:"value,attr"` + Text string `xml:",chardata"` + } `xml:"date"` + Sequences []struct { + Name string `xml:"name,attr"` + Number string `xml:"number,attr"` + } `xml:"sequence"` + } `xml:"title-info"` + PublishInfo struct { + ISBN string `xml:"isbn"` + Publisher string `xml:"publisher"` + Year string `xml:"year"` + } `xml:"publish-info"` + } `xml:"description"` + } + decoder := xml.NewDecoder(reader) + decoder.CharsetReader = ebookXMLCharsetReader + if err := decoder.Decode(&fb2); err != nil { + return book, err + } + + book.Title = fb2.Description.TitleInfo.BookTitle + book.Language = fb2.Description.TitleInfo.Lang + book.Genres = fb2.Description.TitleInfo.Genres + if t, ok := parseEbookDate(firstNonEmpty(fb2.Description.TitleInfo.Date.Value, fb2.Description.TitleInfo.Date.Text)); ok { + book.PublishedAt = t + } + for _, author := range fb2.Description.TitleInfo.Authors { + name := strings.Join(uniqueTrimmedStrings([]string{ + author.FirstName, + author.MiddleName, + author.LastName, + }), " ") + if name == "" { + name = author.Nickname + } + book.Authors = append(book.Authors, name) + } + if len(fb2.Description.TitleInfo.Sequences) > 0 { + book.Series = fb2.Description.TitleInfo.Sequences[0].Name + book.SeriesIndex = fb2.Description.TitleInfo.Sequences[0].Number + } + book.ISBN = fb2.Description.PublishInfo.ISBN + book.Publisher = fb2.Description.PublishInfo.Publisher + if year, err := strconv.Atoi(strings.TrimSpace(fb2.Description.PublishInfo.Year)); err == nil { + book.Year = year + } + return book, nil +} + +func parsePDFInfoFields(data []byte) map[string]string { + fields := map[string]string{} + for _, key := range []string{"Title", "Author", "Subject", "Keywords", "CreationDate", "ISBN"} { + if value, ok := findPDFInfoValue(data, key); ok { + fields[key] = value + } + } + return fields +} + +// pdfWhitespace is the PDF whitespace character set (ISO 32000-1, table 1). +const pdfWhitespace = "\x00\t\n\f\r " + +// isPDFTokenDelimiter reports whether b legally terminates a PDF name token. +// Without this check a key with a shared prefix (e.g. "/TitleSort") would be +// mistaken for the key itself (e.g. "/Title"). +func isPDFTokenDelimiter(b byte) bool { + switch b { + case '\x00', '\t', '\n', '\f', '\r', ' ', '(', ')', '<', '>', '[', ']', '{', '}', '/', '%': + return true + default: + return false + } +} + +// findPDFInfoValue scans every occurrence of "/" in the window and +// returns the first whose token is properly delimited and whose value parses +// as a PDF string. Raw byte search can match key-shaped noise inside +// compressed streams, so a failed parse moves on to the next occurrence +// instead of giving up. +func findPDFInfoValue(data []byte, key string) (string, bool) { + token := []byte("/" + key) + for offset := 0; offset < len(data); { + idx := bytes.Index(data[offset:], token) + if idx < 0 { + return "", false + } + idx += offset + offset = idx + len(token) + rest := data[idx+len(token):] + if len(rest) == 0 { + return "", false + } + if !isPDFTokenDelimiter(rest[0]) { + continue + } + trimmed := bytes.TrimLeft(rest, pdfWhitespace) + if len(trimmed) == 0 { + return "", false + } + if value, ok := readPDFString(trimmed); ok { + return value, true + } + } + return "", false +} + +func decodeEbookXML(data []byte, v any) error { + data = normalizeEbookXMLVersion(data) + decoder := xml.NewDecoder(bytes.NewReader(data)) + decoder.CharsetReader = ebookXMLCharsetReader + return decoder.Decode(v) +} + +func normalizeEbookXMLVersion(data []byte) []byte { + for _, needle := range []string{`version="1.1"`, `version='1.1'`} { + idx := bytes.Index(data, []byte(needle)) + if idx < 0 || idx > 128 { + continue + } + out := append([]byte(nil), data...) + copy(out[idx:], strings.Replace(needle, "1.1", "1.0", 1)) + return out + } + return data +} + +func ebookXMLCharsetReader(label string, input io.Reader) (io.Reader, error) { + name := strings.ToLower(strings.TrimSpace(label)) + if name == "" || name == "utf-8" || name == "utf8" { + return input, nil + } + reader, err := charset.NewReaderLabel(name, input) + if err != nil { + return nil, fmt.Errorf("unsupported ebook XML encoding %q: %w", label, err) + } + return reader, nil +} + +func readPDFString(data []byte) (string, bool) { + switch data[0] { + case '(': + return readPDFLiteralString(data) + case '<': + if len(data) > 1 && data[1] == '<' { + return "", false + } + return readPDFHexString(data) + default: + return "", false + } +} + +func readPDFLiteralString(data []byte) (string, bool) { + if len(data) == 0 || data[0] != '(' { + return "", false + } + var out []byte + depth := 1 + escaped := false + for _, b := range data[1:] { + if escaped { + switch b { + case 'n': + out = append(out, '\n') + case 'r': + out = append(out, '\r') + case 't': + out = append(out, '\t') + case 'b': + out = append(out, '\b') + case 'f': + out = append(out, '\f') + default: + out = append(out, b) + } + escaped = false + continue + } + switch b { + case '\\': + escaped = true + case '(': + depth++ + out = append(out, b) + case ')': + depth-- + if depth == 0 { + return strings.TrimSpace(decodePDFLiteralBytes(out)), true + } + out = append(out, b) + default: + out = append(out, b) + } + } + return "", false +} + +func readPDFHexString(data []byte) (string, bool) { + if len(data) == 0 || data[0] != '<' { + return "", false + } + end := bytes.IndexByte(data[1:], '>') + if end < 0 { + return "", false + } + raw := data[1 : end+1] + var cleaned []byte + for _, b := range raw { + switch { + case b == ' ' || b == '\t' || b == '\r' || b == '\n': + continue + case (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F'): + cleaned = append(cleaned, b) + default: + return "", false + } + } + if len(cleaned)%2 == 1 { + cleaned = append(cleaned, '0') + } + decoded := make([]byte, hex.DecodedLen(len(cleaned))) + if _, err := hex.Decode(decoded, cleaned); err != nil { + return "", false + } + return strings.TrimSpace(decodePDFLiteralBytes(decoded)), true +} + +func decodePDFLiteralBytes(data []byte) string { + if len(data) == 0 { + return "" + } + switch { + case bytes.HasPrefix(data, []byte{0xfe, 0xff}): + if decoded, err := textunicode.UTF16(textunicode.BigEndian, textunicode.ExpectBOM).NewDecoder().Bytes(data); err == nil { + return string(decoded) + } + case bytes.HasPrefix(data, []byte{0xff, 0xfe}): + if decoded, err := textunicode.UTF16(textunicode.LittleEndian, textunicode.ExpectBOM).NewDecoder().Bytes(data); err == nil { + return string(decoded) + } + } + // The PDF spec says non-UTF-16 strings are PDFDocEncoding, but real-world + // producers commonly emit UTF-8 (PDF 2.0 even allows a UTF-8 BOM). Only + // fall back to the Windows-1252 approximation for non-UTF-8 bytes so + // UTF-8 metadata is not mojibaked. + data = bytes.TrimPrefix(data, []byte{0xef, 0xbb, 0xbf}) + if utf8.Valid(data) { + return string(data) + } + if decoded, err := charmap.Windows1252.NewDecoder().Bytes(data); err == nil { + return string(decoded) + } + return strings.ToValidUTF8(string(data), "") +} + +func splitEbookAuthors(value string) []string { + value = strings.TrimSpace(value) + if value == "" { + return nil + } + parts := strings.FieldsFunc(value, func(r rune) bool { + return r == ';' || r == '|' + }) + if len(parts) == 1 { + parts = strings.Split(value, " and ") + } + return uniqueTrimmedStrings(parts) +} + +func splitPDFKeywords(value string) []string { + if strings.TrimSpace(value) == "" { + return nil + } + return uniqueTrimmedStrings(strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ';' + })) +} + +func parsePDFDate(value string) (time.Time, bool) { + value = strings.TrimSpace(value) + if strings.HasPrefix(value, "D:") { + value = strings.TrimPrefix(value, "D:") + } + value = strings.TrimSuffix(value, "Z") + if len(value) >= 14 { + if t, err := time.Parse("20060102150405", value[:14]); err == nil && t.Year() > 0 { + return t, true + } + } + if len(value) >= 8 { + if t, err := time.Parse("20060102", value[:8]); err == nil && t.Year() > 0 { + return t, true + } + } + if len(value) >= 4 { + if t, err := time.Parse("2006", value[:4]); err == nil && t.Year() > 0 { + return t, true + } + } + return time.Time{}, false +} + +func readEPUBZipEntry(reader *zip.Reader, name string) ([]byte, error) { + for _, file := range reader.File { + if file.Name != name { + continue + } + entry, err := file.Open() + if err != nil { + return nil, err + } + defer entry.Close() + limited := io.LimitReader(entry, maxEPUBMetadataEntrySize+1) + data, err := io.ReadAll(limited) + if err != nil { + return nil, err + } + if len(data) > maxEPUBMetadataEntrySize { + return nil, fmt.Errorf("epub entry too large: %s", name) + } + return data, nil + } + return nil, fmt.Errorf("epub entry not found: %s", name) +} + +func epubOPFPath(container []byte) (string, error) { + var parsed struct { + Rootfiles []struct { + FullPath string `xml:"full-path,attr"` + } `xml:"rootfiles>rootfile"` + } + if err := decodeEbookXML(container, &parsed); err != nil { + return "", err + } + for _, rootfile := range parsed.Rootfiles { + if strings.TrimSpace(rootfile.FullPath) != "" { + return rootfile.FullPath, nil + } + } + return "", fmt.Errorf("epub container has no rootfile") +} + +func parseEPUBOPFMetadata(opf []byte, book *parsedEbook) error { + var parsed struct { + Metadata struct { + Titles []string `xml:"title"` + Creators []string `xml:"creator"` + Identifiers []string `xml:"identifier"` + Publisher string `xml:"publisher"` + Dates []string `xml:"date"` + Language string `xml:"language"` + Subjects []string `xml:"subject"` + Descriptions []string `xml:"description"` + Meta []struct { + Name string `xml:"name,attr"` + Property string `xml:"property,attr"` + Content string `xml:"content,attr"` + Value string `xml:",chardata"` + } `xml:"meta"` + } `xml:"metadata"` + } + if err := decodeEbookXML(opf, &parsed); err != nil { + return err + } + book.Title = firstNonEmpty(parsed.Metadata.Titles...) + book.Authors = append(book.Authors, parsed.Metadata.Creators...) + book.Publisher = parsed.Metadata.Publisher + book.Language = parsed.Metadata.Language + book.Genres = append(book.Genres, parsed.Metadata.Subjects...) + book.Description = firstNonEmpty(parsed.Metadata.Descriptions...) + for _, identifier := range parsed.Metadata.Identifiers { + if isbn := normalizeEbookISBN(identifier); isbn != "" { + book.ISBN = isbn + break + } + } + for _, date := range parsed.Metadata.Dates { + if t, ok := parseEbookDate(date); ok { + book.PublishedAt = t + book.Year = t.Year() + break + } + } + for _, meta := range parsed.Metadata.Meta { + name := strings.ToLower(strings.TrimSpace(firstNonEmpty(meta.Name, meta.Property))) + value := strings.TrimSpace(firstNonEmpty(meta.Content, meta.Value)) + switch name { + case "calibre:series", "belongs-to-collection": + book.Series = value + case "calibre:series_index", "group-position": + book.SeriesIndex = value + case "calibre:isbn", "isbn", "schema:isbn": + if book.ISBN == "" { + book.ISBN = normalizeEbookISBN(value) + } + } + } + return nil +} + +func extractEPUBCover(reader *zip.Reader, opfPath string, opf []byte) (*parsedEbookCover, error) { + var parsed struct { + Metadata struct { + Meta []struct { + Name string `xml:"name,attr"` + Content string `xml:"content,attr"` + } `xml:"meta"` + } `xml:"metadata"` + Manifest struct { + Items []struct { + ID string `xml:"id,attr"` + Href string `xml:"href,attr"` + MediaType string `xml:"media-type,attr"` + Properties string `xml:"properties,attr"` + } `xml:"item"` + } `xml:"manifest"` + } + if err := decodeEbookXML(opf, &parsed); err != nil { + return nil, err + } + + coverID := "" + for _, meta := range parsed.Metadata.Meta { + if strings.EqualFold(strings.TrimSpace(meta.Name), "cover") { + coverID = strings.TrimSpace(meta.Content) + break + } + } + + // An EPUB3 properties="cover-image" item is authoritative; the EPUB2 + // id frequently points at the XHTML cover *page* + // rather than the image, so it ranks lower and non-image manifest items + // are skipped entirely instead of shadowing a later real cover image. + var coverHref string + var coverType string + coverRank := 0 + for _, item := range parsed.Manifest.Items { + rank := 0 + for _, prop := range strings.Fields(strings.ToLower(item.Properties)) { + if prop == "cover-image" { + rank = 2 + break + } + } + if rank == 0 && coverID != "" && strings.TrimSpace(item.ID) == coverID { + rank = 1 + } + if rank <= coverRank || !isEPUBImageManifestItem(item.MediaType, item.Href) { + continue + } + coverHref = strings.TrimSpace(item.Href) + coverType = strings.TrimSpace(item.MediaType) + coverRank = rank + if coverRank == 2 { + break + } + } + if coverHref == "" { + return nil, fmt.Errorf("epub cover not referenced") + } + + coverPath := resolveEPUBRelativePath(opfPath, coverHref) + data, err := readEPUBZipEntry(reader, coverPath) + if err != nil { + return nil, err + } + if coverType == "" { + coverType = ebookImageContentType(coverPath) + } + return &parsedEbookCover{ContentType: coverType, Bytes: data}, nil +} + +func isEPUBImageManifestItem(mediaType, href string) bool { + mt := strings.ToLower(strings.TrimSpace(mediaType)) + if mt != "" { + return strings.HasPrefix(mt, "image/") + } + return ebookImageContentType(href) != "application/octet-stream" +} + +func resolveEPUBRelativePath(baseFile string, href string) string { + cleanHref := strings.TrimSpace(href) + if decoded, err := urlPathUnescape(cleanHref); err == nil { + cleanHref = decoded + } + baseDir := path.Dir(strings.ReplaceAll(baseFile, "\\", "/")) + if baseDir == "." || strings.HasPrefix(cleanHref, "/") { + baseDir = "" + } + return strings.TrimPrefix(path.Clean(path.Join(baseDir, cleanHref)), "/") +} + +func urlPathUnescape(value string) (string, error) { + var out strings.Builder + for i := 0; i < len(value); i++ { + if value[i] != '%' || i+2 >= len(value) { + out.WriteByte(value[i]) + continue + } + hi := fromHex(value[i+1]) + lo := fromHex(value[i+2]) + if hi < 0 || lo < 0 { + out.WriteByte(value[i]) + continue + } + out.WriteByte(byte(hi<<4 | lo)) + i += 2 + } + return out.String(), nil +} + +func fromHex(b byte) int { + switch { + case b >= '0' && b <= '9': + return int(b - '0') + case b >= 'a' && b <= 'f': + return int(b-'a') + 10 + case b >= 'A' && b <= 'F': + return int(b-'A') + 10 + default: + return -1 + } +} + +func ebookImageContentType(name string) string { + switch strings.ToLower(filepath.Ext(name)) { + case ".jpg", ".jpeg": + return "image/jpeg" + case ".png": + return "image/png" + case ".webp": + return "image/webp" + case ".gif": + return "image/gif" + case ".avif": + return "image/avif" + case ".bmp": + return "image/bmp" + default: + return "application/octet-stream" + } +} + +func parseEbookDate(value string) (time.Time, bool) { + value = strings.TrimSpace(value) + for _, layout := range []string{"2006-01-02", "2006-01", "2006"} { + if t, err := time.Parse(layout, value); err == nil { + return t, true + } + } + return time.Time{}, false +} diff --git a/internal/scanner/ebook_scan.go b/internal/scanner/ebook_scan.go new file mode 100644 index 00000000..ea560edc --- /dev/null +++ b/internal/scanner/ebook_scan.go @@ -0,0 +1,1345 @@ +package scanner + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + "unicode" + + "github.com/Silo-Server/silo-server/internal/idgen" + "github.com/Silo-Server/silo-server/internal/imageutil" + "github.com/Silo-Server/silo-server/internal/models" + "github.com/Silo-Server/silo-server/internal/titleutil" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type ebookSQLExecutor interface { + Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) +} + +type filesystemMediaItemReader interface { + GetByIDs(ctx context.Context, ids []string) ([]*models.MediaItem, error) +} + +func ebookScanWorkers() int { + if v := os.Getenv("SILO_EBOOK_SCAN_WORKERS"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + return n + } + } + return audiobookScanWorkers() +} + +func (s *Scanner) ScanEbookFolder(ctx context.Context, folder *models.MediaFolder) error { + if s == nil || folder == nil { + return fmt.Errorf("ScanEbookFolder: nil scanner or folder") + } + return s.scanEbookPaths(ctx, folder, folder.Paths, true) +} + +// ebookRootScan captures the walk outcome for one configured library root. +type ebookRootScan struct { + root string + files []string + rootErr error // root stat failed, or the root is not a directory + walkFailures int // entries within the subtree the walk could not read or resolve +} + +// failed reports whether the walk under this root is known to be incomplete. +// Files found under a failed root are still indexed, but the root is excluded +// from missing-file reconciliation: a transient mount/permission problem (or +// a mid-walk subtree error) must not cascade into marking — and, with +// empty_trash_after_scan, deleting — everything under it. +func (r *ebookRootScan) failed() bool { + return r.rootErr != nil || r.walkFailures > 0 +} + +// collectEbookRootScans walks every configured root with the shared +// logical-tree walker (so symlinked roots and symlinked subdirectories +// resolve and scan exactly like the video pipeline) and records per-root walk +// failures. The only error returned is context cancellation. +func collectEbookRootScans(ctx context.Context, folderID int, roots []string) ([]ebookRootScan, error) { + scans := make([]ebookRootScan, 0, len(roots)) + visitedPhysicalDirs := make(map[string]struct{}) + for _, root := range roots { + if err := ctx.Err(); err != nil { + return nil, err + } + cleanRoot := filepath.Clean(strings.TrimSpace(root)) + if cleanRoot == "" || cleanRoot == "." { + continue + } + scan := ebookRootScan{root: cleanRoot} + info, statErr := os.Stat(cleanRoot) + switch { + case statErr != nil: + // Unmounted/missing/permission-broken root: failed, not empty. + scan.rootErr = fmt.Errorf("stat root: %w", statErr) + case !info.IsDir(): + scan.rootErr = fmt.Errorf("root is not a directory after symlink resolution") + } + if statErr == nil { + if err := walkLogicalTree(ctx, cleanRoot, cleanRoot, walkModeEbook, visitedPhysicalDirs, &scan.files, &scan.walkFailures); err != nil { + return nil, err + } + } + if scan.failed() { + slog.Warn("ebook scan: root walk incomplete; root excluded from missing-file reconciliation", + "folder_id", folderID, + "root", cleanRoot, + "walk_failures", scan.walkFailures, + "error", scan.rootErr, + ) + } + scans = append(scans, scan) + } + return scans, nil +} + +// splitEbookReconcileRoots partitions walked roots into the set that may take +// part in missing-file reconciliation and reports whether any reconcilable +// root saw at least one ebook on disk. +func splitEbookReconcileRoots(scans []ebookRootScan) (reconcileRoots []string, sawFiles bool) { + reconcileRoots = make([]string, 0, len(scans)) + for i := range scans { + scan := &scans[i] + if scan.failed() { + continue + } + if len(scan.files) > 0 { + sawFiles = true + } + reconcileRoots = append(reconcileRoots, scan.root) + } + return reconcileRoots, sawFiles +} + +func (s *Scanner) scanEbookPaths(ctx context.Context, folder *models.MediaFolder, roots []string, fullScan bool) error { + if s == nil || folder == nil { + return fmt.Errorf("scanEbookPaths: nil scanner or folder") + } + scans, err := collectEbookRootScans(ctx, folder.ID, roots) + if err != nil { + return err + } + // Every discovered file is indexed, including files under roots whose + // walk partially failed: indexing is additive and safe, only the + // destructive reconciliation below is restricted to cleanly walked roots. + var candidates []string + for i := range scans { + candidates = append(candidates, scans[i].files...) + } + + if len(candidates) == 0 { + return s.reconcileEbookScan(ctx, folder, scans, nil, fullScan) + } + + workers := ebookScanWorkers() + slog.Info("ebook scan: starting", + "folder_id", folder.ID, + "candidates", len(candidates), + "workers", workers, + ) + reportEbookScanProgress(ctx, folder.ID, len(candidates), 0, 0, 0) + + ch := make(chan string, workers*2) + groupLocks := newEbookGroupLocks() + var ( + wg sync.WaitGroup + processed int64 + failed int64 + skipped int64 + failMu sync.Mutex + failures []error + cancelErr error + ) + start := time.Now() + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for path := range ch { + if ctx.Err() != nil { + return + } + if err := s.reconcileEbookFile(ctx, folder, path, &skipped, groupLocks); err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + failMu.Lock() + if cancelErr == nil { + cancelErr = err + } + failMu.Unlock() + return + } + atomic.AddInt64(&failed, 1) + failMu.Lock() + failures = append(failures, fmt.Errorf("%s: %w", path, err)) + failMu.Unlock() + slog.Warn("ebook scan: file failed", + "folder_id", folder.ID, + "path", path, + "error", err, + ) + } + n := atomic.AddInt64(&processed, 1) + if n%500 == 0 || n == int64(len(candidates)) { + failedCount := atomic.LoadInt64(&failed) + skippedCount := atomic.LoadInt64(&skipped) + slog.Info("ebook scan: progress", + "folder_id", folder.ID, + "processed", n, + "failed", failedCount, + "skipped", skippedCount, + "total", len(candidates), + "elapsed_sec", int(time.Since(start).Seconds()), + ) + reportEbookScanProgress(ctx, folder.ID, len(candidates), int(n), int(failedCount), int(skippedCount)) + } + } + }() + } + + for _, p := range candidates { + select { + case ch <- p: + case <-ctx.Done(): + close(ch) + wg.Wait() + return ctx.Err() + } + } + close(ch) + wg.Wait() + if err := ctx.Err(); err != nil { + return err + } + if cancelErr != nil { + return cancelErr + } + + slog.Info("ebook scan: completed", + "folder_id", folder.ID, + "processed", atomic.LoadInt64(&processed), + "failed", atomic.LoadInt64(&failed), + "skipped", atomic.LoadInt64(&skipped), + "elapsed_sec", int(time.Since(start).Seconds()), + ) + if processedCount := atomic.LoadInt64(&processed); processedCount > 0 { + failedCount := atomic.LoadInt64(&failed) + skippedCount := atomic.LoadInt64(&skipped) + if failedCount > 0 && skippedCount == 0 && failedCount == processedCount { + return fmt.Errorf("ebook scan failed for every attempted folder_id=%d: %w", folder.ID, errors.Join(failures...)) + } + } + + seenPaths := make(map[string]bool, len(candidates)) + for _, p := range candidates { + seenPaths[p] = true + } + return s.reconcileEbookScan(ctx, folder, scans, seenPaths, fullScan) +} + +// ebookCleanupGuardRepo is the slice of catalog.FolderRepository the +// empty-root cleanup guard needs. +type ebookCleanupGuardRepo interface { + ConsumeEmptyCleanupAllowance(ctx context.Context, id int) (bool, error) + SetScanWarning(ctx context.Context, id int, code, message string, warnedAt time.Time) error +} + +// ebookEmptyCleanupAllowed mirrors the video pipeline's empty-root guard: a +// scan that saw zero ebooks while the DB still has rows under the scanned +// roots may only reconcile (and therefore delete) when the operator has +// explicitly confirmed the cleanup. Subtree scans never consume the +// folder-level allowance; the next full scan converges them. +func ebookEmptyCleanupAllowed(ctx context.Context, repo ebookCleanupGuardRepo, folderID int, fullScan bool) (bool, error) { + if !fullScan || repo == nil { + return false, nil + } + allowed, err := repo.ConsumeEmptyCleanupAllowance(ctx, folderID) + if err != nil { + return false, fmt.Errorf("checking empty cleanup confirmation for folder %d: %w", folderID, err) + } + if !allowed { + if err := repo.SetScanWarning(ctx, folderID, + "empty_root", + "Scan found 0 media files; cleanup was skipped until deletion is confirmed.", + time.Now().UTC(), + ); err != nil { + return false, fmt.Errorf("recording empty-root warning for folder %d: %w", folderID, err) + } + } + return allowed, nil +} + +// reconcileEbookScan applies the post-walk safety policy and then performs +// missing-file reconciliation for the roots that walked cleanly. +func (s *Scanner) reconcileEbookScan(ctx context.Context, folder *models.MediaFolder, scans []ebookRootScan, seenPaths map[string]bool, fullScan bool) error { + reconcileRoots, sawFiles := splitEbookReconcileRoots(scans) + if len(reconcileRoots) == 0 { + if len(scans) > 0 { + slog.Warn("ebook scan: every root walk failed; skipping missing-file reconciliation", + "folder_id", folder.ID, + ) + } + return nil + } + if s.fileRepo == nil || s.libraryRepo == nil { + return nil + } + + if !sawFiles { + existingCount := 0 + for _, root := range reconcileRoots { + existing, err := s.fileRepo.GetByFolderAndPathPrefix(ctx, folder.ID, root) + if err != nil { + return fmt.Errorf("listing existing ebook files for %q: %w", root, err) + } + existingCount += len(existing) + } + if existingCount > 0 { + var guard ebookCleanupGuardRepo + if s.folderRepo != nil { + guard = s.folderRepo + } + allowed, err := ebookEmptyCleanupAllowed(ctx, guard, folder.ID, fullScan) + if err != nil { + return err + } + if !allowed { + slog.Warn("ebook scan: walk saw zero ebooks but the database has files under the scanned roots; skipping reconciliation until cleanup is confirmed", + "folder_id", folder.ID, + "existing_files", existingCount, + "full_scan", fullScan, + ) + return nil + } + } + } + + if err := s.reconcileMissingEbookFiles(ctx, folder, reconcileRoots, seenPaths); err != nil { + return err + } + if fullScan && s.folderRepo != nil { + // The cleanup either ran with files present or was explicitly + // confirmed; any prior empty-root warning is stale now. + if err := s.folderRepo.ClearScanWarning(ctx, folder.ID); err != nil { + return fmt.Errorf("clearing scan warning for folder %d: %w", folder.ID, err) + } + } + return nil +} + +// reconcileMissingEbookFiles mirrors the video/audio scan cleanup: DB files +// under the scanned roots that were not seen on disk are marked missing, the +// folder trash is optionally emptied, and library memberships are reconciled +// so items with no remaining files are removed (renames therefore converge on +// the newly indexed path instead of leaving a stale duplicate item). +func (s *Scanner) reconcileMissingEbookFiles(ctx context.Context, folder *models.MediaFolder, roots []string, seenPaths map[string]bool) error { + if s.fileRepo == nil || s.libraryRepo == nil || len(roots) == 0 { + return nil + } + + now := time.Now().UTC() + missing := 0 + for _, root := range roots { + existing, err := s.fileRepo.GetByFolderAndPathPrefix(ctx, folder.ID, root) + if err != nil { + return fmt.Errorf("listing existing ebook files for %q: %w", root, err) + } + for _, mf := range existing { + if mf == nil || seenPaths[mf.FilePath] { + continue + } + if mf.MissingSince == nil { + if err := s.fileRepo.MarkMissing(ctx, mf.ID, now); err != nil { + slog.Error("ebook scan: failed to mark file missing", + "folder_id", folder.ID, + "path", mf.FilePath, + "error", err, + ) + continue + } + } + missing++ + } + } + + if s.emptyTrashAfterScan { + trashed, err := s.fileRepo.DeleteMissingByFolder(ctx, folder.ID) + if err != nil { + return fmt.Errorf("emptying trash for folder %d: %w", folder.ID, err) + } + if trashed > 0 { + slog.Info("ebook scan: emptied trash", "folder_id", folder.ID, "deleted", trashed) + } + } + + removedMemberships, deletedItems, orphanedImageDirs, err := s.reconcileLibraryMemberships(ctx, folder.ID) + if err != nil { + return fmt.Errorf("reconciling library membership for folder %d: %w", folder.ID, err) + } + + // Best-effort S3 image cleanup for orphaned items. + if s.s3Client != nil && len(orphanedImageDirs) > 0 { + bucket := s.s3Client.Bucket() + for _, dir := range orphanedImageDirs { + _, _ = s.s3Client.DeletePrefix(ctx, bucket, dir) + } + } + + if missing > 0 || removedMemberships > 0 || deletedItems > 0 { + slog.Info("ebook scan: reconciled missing files", + "folder_id", folder.ID, + "missing", missing, + "memberships_removed", removedMemberships, + "items_deleted", deletedItems, + ) + } + return nil +} + +func reportEbookScanProgress(ctx context.Context, folderID int, total, processed, failed, skipped int) { + reportProgress(ctx, ProgressUpdate{ + Phase: "ebook_scan", + Message: fmt.Sprintf("Scanning ebooks in folder %d", folderID), + CurrentScope: strconv.Itoa(folderID), + TotalFiles: total, + FilesDiscovered: total, + FilesProcessed: processed, + Errors: failed, + Unchanged: skipped, + }) +} + +func (s *Scanner) reconcileEbookFile(ctx context.Context, folder *models.MediaFolder, filePath string, skipped *int64, groupLocks *ebookGroupLocks) error { + info, err := os.Stat(filePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return fmt.Errorf("stat ebook file %s: %w", filePath, err) + } + size := info.Size() + modifiedAt := normalizeFileModifiedAt(info.ModTime()) + + isUnchanged, skipErr := s.ebookFileShouldSkip(ctx, folder, filePath, size, modifiedAt) + if skipErr != nil { + slog.Warn("ebook scan: skip-check failed, falling through", + "folder_id", folder.ID, + "path", filePath, + "error", skipErr, + ) + } else if isUnchanged { + atomic.AddInt64(skipped, 1) + return nil + } + + parsed, err := parseEbookFile(filePath) + if err != nil { + return fmt.Errorf("parse ebook file %s: %w", filePath, err) + } + if parsed.Title == "" { + parsed.Title = ebookTitleFromPath(filePath) + } + + groupKey := ebookContentGroupKey(&parsed, filePath) + unlock := groupLocks.lock(groupKey) + defer unlock() + + contentID, curated, err := s.upsertEbookMediaItem(ctx, folder.ID, filePath, &parsed, groupKey) + if err != nil { + return fmt.Errorf("upsert ebook item: %w", err) + } + if err := s.upsertEbookMediaFile(ctx, folder, contentID, filePath, size, modifiedAt, &parsed, groupKey); err != nil { + return fmt.Errorf("upsert ebook file: %w", err) + } + if err := applyEbookLocalCover(ctx, s.itemRepo, s.imageCacher, contentID, filePath, &parsed); err != nil { + slog.Warn("ebook scan: local cover upload failed", + "folder_id", folder.ID, + "content_id", contentID, + "path", filePath, + "error", err, + ) + } + if err := s.upsertEbookPeople(ctx, contentID, &parsed, curated); err != nil { + return fmt.Errorf("upsert ebook people: %w", err) + } + if err := s.upsertEbookSeries(ctx, contentID, &parsed, curated); err != nil { + return fmt.Errorf("upsert ebook series: %w", err) + } + if err := insertEbookLibraryMembership(ctx, s.fileRepo.Pool(), contentID, folder.ID); err != nil { + return fmt.Errorf("upsert ebook library membership: %w", err) + } + if parsed.ISBN != "" { + if err := insertEbookISBNProviderID(ctx, s.fileRepo.Pool(), contentID, parsed.ISBN); err != nil { + return fmt.Errorf("upsert ebook ISBN provider id: %w", err) + } + } + slog.Info("ebook scan: indexed", + "folder_id", folder.ID, + "content_id", contentID, + "title", parsed.Title, + "authors", parsed.Authors, + "path", filePath, + ) + return nil +} + +func (s *Scanner) ebookFileShouldSkip(ctx context.Context, folder *models.MediaFolder, filePath string, size int64, modifiedAt time.Time) (bool, error) { + if s.fileRepo == nil || s.itemRepo == nil { + return false, nil + } + existing, err := s.fileRepo.ListByObservedRootPath(ctx, folder.ID, filePath) + if err != nil { + return false, fmt.Errorf("list existing files: %w", err) + } + if len(existing) != 1 { + return false, nil + } + mf := existing[0] + if mf.FilePath != filePath || mf.FileSize != size || mf.FileModifiedAt == nil || !sameFileModifiedAt(mf.FileModifiedAt, modifiedAt) { + return false, nil + } + if mf.ContentID == "" { + return false, nil + } + if mf.GroupKeyVersion != ebookGroupKeyVersion { + // The grouping scheme changed since this row was written; reprocess + // once so the stored key is rewritten under the current scheme and + // sibling-format lookups can find it again. + return false, nil + } + statuses, err := s.itemRepo.GetStatusByIDs(ctx, []string{mf.ContentID}) + if err != nil { + return false, fmt.Errorf("get item status: %w", err) + } + return !strings.EqualFold(strings.TrimSpace(statuses[mf.ContentID]), "unmatched"), nil +} + +// upsertEbookMediaItem resolves or creates the media item for the file and +// reports whether the item's metadata is curated (provider-matched), in which +// case dependent writes (people, series) must be fill-empty only. +func (s *Scanner) upsertEbookMediaItem(ctx context.Context, folderID int, filePath string, book *parsedEbook, groupKey string) (string, bool, error) { + if s.itemRepo == nil { + return "", false, fmt.Errorf("itemRepo not configured on Scanner") + } + if s.fileRepo == nil { + return "", false, fmt.Errorf("fileRepo not configured on Scanner") + } + + existingID, err := s.fileRepo.FindContentIDByRootPath(ctx, folderID, filePath, "ebook") + if err != nil { + return "", false, fmt.Errorf("find ebook by root path: %w", err) + } + if existingID != "" { + curated, err := updateExistingEbookMediaItem(ctx, s.itemRepo, s.itemRepo, existingID, book) + if err != nil { + return "", false, err + } + return existingID, curated, nil + } + if existing := s.findEbookByFilePath(ctx, filePath); existing != nil { + if ebookItemHasCuratedMetadata(existing) { + return existing.ContentID, true, nil + } + applyEbookToMediaItem(existing, book) + if existing.SortTitle == "" { + existing.SortTitle = titleutil.DeriveDefaultSortTitle(existing.Title) + } + if err := s.itemRepo.Upsert(ctx, existing); err != nil { + return "", false, err + } + return existing.ContentID, false, nil + } + if existing := s.findEbookByContentGroupKey(ctx, folderID, groupKey, book.Format, filePath); existing != nil { + // The sibling file joins the group either way, but curated metadata + // on a provider-matched item must not be clobbered by whatever this + // file happens to embed. + if ebookItemHasCuratedMetadata(existing) { + return existing.ContentID, true, nil + } + applyEbookToMediaItem(existing, book) + if existing.SortTitle == "" { + existing.SortTitle = titleutil.DeriveDefaultSortTitle(existing.Title) + } + if err := s.itemRepo.Upsert(ctx, existing); err != nil { + return "", false, err + } + return existing.ContentID, false, nil + } + contentID, err := resolveEbookMediaItem(ctx, s.fileRepo, s.itemRepo, folderID, filePath, book) + if err != nil { + return "", false, err + } + return contentID, false, nil +} + +func resolveEbookMediaItem( + ctx context.Context, + rootFinder filesystemRootContentFinder, + itemWriter filesystemMediaItemWriter, + folderID int, + filePath string, + book *parsedEbook, +) (string, error) { + if rootFinder == nil { + return "", fmt.Errorf("root content finder not configured") + } + if itemWriter == nil { + return "", fmt.Errorf("media item writer not configured") + } + existingID, err := rootFinder.FindContentIDByRootPath(ctx, folderID, filePath, "ebook") + if err != nil { + return "", fmt.Errorf("find ebook by root path: %w", err) + } + if existingID != "" { + return existingID, nil + } + + cleanTitle := strings.TrimSpace(book.Title) + if cleanTitle == "" { + cleanTitle = ebookTitleFromPath(filePath) + } + return createEbookMediaItem(ctx, itemWriter, book, cleanTitle) +} + +func createEbookMediaItem(ctx context.Context, itemWriter filesystemMediaItemWriter, book *parsedEbook, cleanTitle string) (string, error) { + id, err := idgen.NextID() + if err != nil { + return "", fmt.Errorf("generate content_id: %w", err) + } + item := &models.MediaItem{ + ContentID: id, + Type: "ebook", + // Explicit "pending" (the item upsert writes the literal status, so the + // DB default never applies): enrichment promotes it to "matched", which + // arms ebookItemHasCuratedMetadata against file-metadata clobbering. + Status: "pending", + Title: cleanTitle, + SortTitle: titleutil.DeriveDefaultSortTitle(cleanTitle), + Year: book.Year, + } + applyEbookToMediaItem(item, book) + if item.SortTitle == "" { + item.SortTitle = titleutil.DeriveDefaultSortTitle(item.Title) + } + if err := itemWriter.Upsert(ctx, item); err != nil { + return "", err + } + return id, nil +} + +// updateExistingEbookMediaItem refreshes a known item from file metadata and +// reports whether the item's metadata is curated (and was therefore left +// untouched). +func updateExistingEbookMediaItem(ctx context.Context, itemReader filesystemMediaItemReader, itemWriter filesystemMediaItemWriter, contentID string, book *parsedEbook) (bool, error) { + if itemReader == nil { + return false, fmt.Errorf("media item reader not configured") + } + if itemWriter == nil { + return false, fmt.Errorf("media item writer not configured") + } + items, err := itemReader.GetByIDs(ctx, []string{contentID}) + if err != nil { + return false, fmt.Errorf("get ebook media item %s: %w", contentID, err) + } + if len(items) == 0 || items[0] == nil { + return false, fmt.Errorf("ebook media item %s not found", contentID) + } + item := items[0] + if ebookItemHasCuratedMetadata(item) { + // A matched item's metadata was curated by a provider or a person; + // re-parsing the file (mtime change, group-key version bump) must not + // clobber it with embedded file metadata. + return true, nil + } + applyEbookToMediaItem(item, book) + if item.SortTitle == "" { + item.SortTitle = titleutil.DeriveDefaultSortTitle(item.Title) + } + return false, itemWriter.Upsert(ctx, item) +} + +// ebookItemHasCuratedMetadata reports whether the item's metadata is owned by +// a provider match (or manual curation) and therefore must not be overwritten +// by metadata embedded in scanned files. +func ebookItemHasCuratedMetadata(item *models.MediaItem) bool { + return item != nil && strings.EqualFold(strings.TrimSpace(item.Status), "matched") +} + +func applyEbookToMediaItem(item *models.MediaItem, book *parsedEbook) { + item.Type = "ebook" + if title := strings.TrimSpace(book.Title); title != "" { + item.Title = title + } + if book.Year > 0 { + item.Year = book.Year + } + if book.Description != "" && (item.Overview == "" || looksLikeHTML(item.Overview)) { + item.Overview = book.Description + } + if book.Publisher != "" { + item.Studios = mergeUniqueStrings(item.Studios, []string{book.Publisher}) + } + if len(book.Genres) > 0 { + item.Genres = mergeUniqueStrings(item.Genres, book.Genres) + } + if !book.PublishedAt.IsZero() && item.ReleaseDate == nil { + rd := book.PublishedAt.UTC().Format("2006-01-02") + item.ReleaseDate = &rd + } + if book.Language != "" && item.OriginalLanguage == "" { + item.OriginalLanguage = book.Language + } +} + +func (s *Scanner) findEbookByFilePath(ctx context.Context, filePath string) *models.MediaItem { + if s.fileRepo == nil { + return nil + } + var existingID string + err := s.fileRepo.Pool().QueryRow(ctx, ` + SELECT mf.content_id + FROM media_files mf + JOIN media_items mi ON mi.content_id = mf.content_id + WHERE mf.file_path = $1 + AND mi.type = 'ebook' + LIMIT 1 + `, filePath).Scan(&existingID) + if err != nil || existingID == "" { + return nil + } + items, err := s.itemRepo.GetByIDs(ctx, []string{existingID}) + if err != nil || len(items) == 0 { + return nil + } + return items[0] +} + +// findEbookByContentGroupKey locates an existing item that a sibling format of +// the same book should join. Grouping merges *different* formats only: an item +// that already owns another file of the same format is excluded, so two +// distinct books whose sparse metadata collides on the same key (e.g. every +// volume carrying the series name as its title) stay separate items instead of +// silently merging. +func (s *Scanner) findEbookByContentGroupKey(ctx context.Context, folderID int, groupKey string, format string, filePath string) *models.MediaItem { + if s.fileRepo == nil || s.itemRepo == nil || strings.TrimSpace(groupKey) == "" { + return nil + } + var existingID string + err := s.fileRepo.Pool().QueryRow(ctx, ` + SELECT mf.content_id + FROM media_files mf + JOIN media_items mi ON mi.content_id = mf.content_id + WHERE mf.media_folder_id = $1 + AND mf.group_key_version = $2 + AND mf.content_group_key = $3 + AND mf.missing_since IS NULL + AND mi.type = 'ebook' + AND NOT EXISTS ( + SELECT 1 FROM media_files dup + WHERE dup.content_id = mf.content_id + AND dup.missing_since IS NULL + AND lower(dup.container) = lower($4) + AND dup.file_path <> $5 + ) + ORDER BY CASE WHEN lower(trim(mi.status)) = 'matched' THEN 0 ELSE 1 END, + mf.id ASC + LIMIT 1 + `, folderID, ebookGroupKeyVersion, groupKey, format, filePath).Scan(&existingID) + if err != nil || existingID == "" { + return nil + } + items, err := s.itemRepo.GetByIDs(ctx, []string{existingID}) + if err != nil || len(items) == 0 { + return nil + } + return items[0] +} + +// ebookGroupKeyVersion versions the ebookContentGroupKey scheme. Bump it +// whenever the key shape changes so rows keyed under an older scheme are +// reprocessed and re-keyed (see ebookFileShouldSkip) instead of silently never +// matching sibling-format lookups again. +const ebookGroupKeyVersion = 2 + +func (s *Scanner) upsertEbookMediaFile(ctx context.Context, folder *models.MediaFolder, contentID string, filePath string, size int64, modifiedAt time.Time, book *parsedEbook, groupKey string) error { + mf := buildEbookMediaFile(folder, contentID, filePath, size, modifiedAt, book, groupKey) + if _, err := s.fileRepo.Upsert(ctx, mf); err != nil { + return fmt.Errorf("upsert media file %s: %w", filePath, err) + } + return nil +} + +func buildEbookMediaFile(folder *models.MediaFolder, contentID string, filePath string, size int64, modifiedAt time.Time, book *parsedEbook, groupKey string) models.MediaFile { + return models.MediaFile{ + ContentID: contentID, + MediaFolderID: folder.ID, + CanonicalRootPath: filePath, + ObservedRootPath: filePath, + ContentGroupKey: groupKey, + GroupKeyVersion: ebookGroupKeyVersion, + BaseTitle: book.Title, + BaseYear: book.Year, + BaseType: "ebook", + IdentityConfidence: ebookIdentityConfidence(book), + FilePath: filePath, + FileSize: size, + FileModifiedAt: &modifiedAt, + Container: book.Format, + Duration: book.PageCount, + ProbeSource: "local", + } +} + +func ebookContentGroupKey(book *parsedEbook, filePath string) string { + if book != nil { + if isbn := normalizeEbookISBN(book.ISBN); isbn != "" { + return "ebook:isbn:" + isbn + } + } + + title := "" + authors := []string(nil) + if book != nil { + title = strings.TrimSpace(book.Title) + authors = book.Authors + } + if title == "" { + title = ebookTitleFromPath(filePath) + } + normalizedTitle := normalizeEbookIdentityPart(title) + if normalizedTitle == "" { + cleanPath := strings.TrimSpace(filepath.Clean(filePath)) + if cleanPath == "." { + return "" + } + return "ebook:path:" + cleanPath + } + + normalizedAuthors := make([]string, 0, len(authors)) + seenAuthors := make(map[string]struct{}, len(authors)) + for _, author := range authors { + normalized := normalizeEbookIdentityPart(author) + if normalized == "" { + continue + } + if _, ok := seenAuthors[normalized]; ok { + continue + } + seenAuthors[normalized] = struct{}{} + normalizedAuthors = append(normalizedAuthors, normalized) + } + sort.Strings(normalizedAuthors) + if len(normalizedAuthors) > 0 { + return "ebook:title_author:" + normalizedTitle + "|" + strings.Join(normalizedAuthors, ",") + } + + dir := strings.TrimSpace(filepath.Clean(filepath.Dir(filePath))) + if dir == "." { + dir = "" + } + return "ebook:title:" + normalizedTitle + "|dir:" + normalizeEbookIdentityPart(dir) +} + +func ebookTitleFromPath(filePath string) string { + base := filepath.Base(filePath) + if base == "." || base == string(filepath.Separator) { + return "" + } + // ".fb2.zip" is a double extension that filepath.Ext (and the + // normalized ".fbz" format token) would leave half-stripped. + if strings.HasSuffix(strings.ToLower(base), ".fb2.zip") { + return base[:len(base)-len(".fb2.zip")] + } + return strings.TrimSuffix(base, filepath.Ext(base)) +} + +func normalizeEbookIdentityPart(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + var b strings.Builder + previousSpace := false + for _, r := range value { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + b.WriteRune(r) + previousSpace = false + continue + } + if unicode.IsSpace(r) || r == '&' || r == '-' || r == '_' || r == ':' || r == '/' || r == '\\' || r == ',' || r == '.' { + if b.Len() > 0 && !previousSpace { + b.WriteByte(' ') + previousSpace = true + } + } + } + return strings.TrimSpace(b.String()) +} + +type ebookGroupLocks struct { + mu sync.Mutex + locks map[string]*sync.Mutex +} + +func newEbookGroupLocks() *ebookGroupLocks { + return &ebookGroupLocks{locks: make(map[string]*sync.Mutex)} +} + +func (l *ebookGroupLocks) lock(key string) func() { + if l == nil || strings.TrimSpace(key) == "" { + return func() {} + } + l.mu.Lock() + groupLock := l.locks[key] + if groupLock == nil { + groupLock = &sync.Mutex{} + l.locks[key] = groupLock + } + l.mu.Unlock() + + groupLock.Lock() + return groupLock.Unlock +} + +// localEbookPosterPrefix mirrors the storage layout produced by +// imagecache.CacheEbookCover ("local/ebooks/{contentID}/poster/..."); it marks +// posters this pipeline owns and is therefore allowed to refresh. +const localEbookPosterPrefix = "local/ebooks/" + +type ebookCoverMetadataStore interface { + GetPoster(ctx context.Context, contentID string) (posterPath string, posterThumbhash string, err error) + SetLocalPoster(ctx context.Context, contentID, posterPath, thumbhash, localPrefix string) (bool, error) +} + +// applyEbookLocalCover records the best locally available cover for the file: +// a sidecar image that belongs to this book wins over the embedded cover, and +// exactly one cover is applied per reconcile. A sidecar discovery error does +// not block the embedded fallback. +func applyEbookLocalCover(ctx context.Context, store ebookCoverMetadataStore, cacher ebookCoverCacher, contentID string, ebookFilePath string, book *parsedEbook) error { + if store == nil || cacher == nil || contentID == "" { + return nil + } + var data []byte + var sidecarErr error + if ebookFilePath != "" { + cover, _, err := findSidecarBookCover(ebookFilePath) + if err != nil { + sidecarErr = err + } else if cover != nil { + data = cover.Bytes + } + } + if len(data) == 0 && book != nil && book.Cover != nil { + data = book.Cover.Bytes + } + if len(data) == 0 { + return sidecarErr + } + return errors.Join(sidecarErr, cacheEbookCoverBytes(ctx, store, cacher, contentID, data)) +} + +func cacheEbookCoverBytes(ctx context.Context, store ebookCoverMetadataStore, cacher ebookCoverCacher, contentID string, data []byte) error { + if len(data) == 0 { + return nil + } + existingPath, existingThumbhash, err := store.GetPoster(ctx, contentID) + if err != nil { + return fmt.Errorf("get ebook poster for cover: %w", err) + } + existingPath = strings.TrimSpace(existingPath) + if existingPath != "" { + // Provider or manually applied artwork always wins over scan covers. + if !strings.HasPrefix(existingPath, localEbookPosterPrefix) { + return nil + } + // Re-extracted bytes of an unchanged cover hash identically; skip + // the variant regeneration and upload churn. A replaced cover hashes + // differently and falls through to refresh the stale poster. + if thumbhash, err := imageutil.Thumbhash(data); err == nil && thumbhash == existingThumbhash { + return nil + } + } + basePath, ext, thumbhash, err := cacher.CacheEbookCover(ctx, data, contentID) + if err != nil { + return err + } + posterPath := strings.TrimRight(basePath, "/") + "/original" + ext + if _, err := store.SetLocalPoster(ctx, contentID, posterPath, thumbhash, localEbookPosterPrefix); err != nil { + return fmt.Errorf("set ebook local poster: %w", err) + } + return nil +} + +var sidecarCoverNames = []string{"cover", "folder", "front", "poster", "thumbnail"} +var sidecarCoverExtensions = []string{".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".bmp"} + +// findSidecarBookCover looks for cover art next to the ebook file. An image +// named after the book file always belongs to it; the generic artwork names +// (cover.jpg, folder.png, ...) are trusted only when this is the directory's +// sole ebook, so one cover.jpg in a flat multi-book folder is not applied to +// every book in it. +func findSidecarBookCover(ebookFilePath string) (*parsedEbookCover, string, error) { + if ebookFilePath == "" { + return nil, "", nil + } + entries, err := os.ReadDir(filepath.Dir(ebookFilePath)) + if err != nil { + return nil, "", err + } + byName := make(map[string]string, len(entries)) + ebookCount := 0 + for _, entry := range entries { + if entry.IsDir() { + continue + } + if SupportsEbookFile(entry.Name()) { + ebookCount++ + } + byName[strings.ToLower(entry.Name())] = filepath.Join(filepath.Dir(ebookFilePath), entry.Name()) + } + + var candidates []string + if base := strings.ToLower(ebookTitleFromPath(ebookFilePath)); base != "" { + for _, ext := range sidecarCoverExtensions { + candidates = append(candidates, base+ext) + } + } + if ebookCount <= 1 { + for _, name := range sidecarCoverNames { + for _, ext := range sidecarCoverExtensions { + candidates = append(candidates, name+ext) + } + } + } + for _, candidate := range candidates { + path := byName[candidate] + if path == "" { + continue + } + data, err := readSidecarCover(path) + if err != nil { + return nil, path, err + } + return &parsedEbookCover{ + ContentType: ebookImageContentType(path), + Bytes: data, + }, path, nil + } + return nil, "", nil +} + +func readSidecarCover(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return nil, err + } + if info.Size() > maxEPUBMetadataEntrySize { + return nil, fmt.Errorf("sidecar cover too large: %s", path) + } + data, err := io.ReadAll(io.LimitReader(file, maxEPUBMetadataEntrySize+1)) + if err != nil { + return nil, err + } + if len(data) > maxEPUBMetadataEntrySize { + return nil, fmt.Errorf("sidecar cover too large: %s", path) + } + if len(data) == 0 { + return nil, fmt.Errorf("sidecar cover empty: %s", path) + } + return data, nil +} + +func insertEbookLibraryMembership(ctx context.Context, exec ebookSQLExecutor, contentID string, folderID int) error { + _, err := exec.Exec(ctx, ` + INSERT INTO media_item_libraries (content_id, media_folder_id, first_seen_at) + VALUES ($1, $2, NOW()) + ON CONFLICT (content_id, media_folder_id) DO NOTHING + `, contentID, folderID) + return err +} + +func insertEbookISBNProviderID(ctx context.Context, exec ebookSQLExecutor, contentID string, isbn string) error { + _, err := exec.Exec(ctx, ` + INSERT INTO media_item_provider_ids (content_id, provider, provider_id, item_type) + VALUES ($1, 'isbn', $2, 'ebook') + ON CONFLICT DO NOTHING + `, contentID, isbn) + return err +} + +type ebookCredit struct { + Name string + Kind models.PersonKind +} + +func ebookPeopleCreditsEqual(existing []models.ItemPerson, desired []ebookCredit) bool { + existingAuthors := make([]models.ItemPerson, 0, len(existing)) + for _, p := range existing { + if p.Kind == models.PersonKindNarrator { + return false + } + if p.Kind == models.PersonKindAuthor { + existingAuthors = append(existingAuthors, p) + } + } + if len(existingAuthors) != len(desired) { + return false + } + type key struct { + name string + kind models.PersonKind + } + have := make(map[key]struct{}, len(existingAuthors)) + for _, p := range existingAuthors { + have[key{strings.ToLower(strings.TrimSpace(p.Person.Name)), p.Kind}] = struct{}{} + } + for _, d := range desired { + k := key{strings.ToLower(strings.TrimSpace(d.Name)), d.Kind} + if _, ok := have[k]; !ok { + return false + } + } + return true +} + +type ebookResolvedAuthor struct { + ID int64 + Name string +} + +func mergeEbookPeople(existing []models.ItemPerson, authors []ebookResolvedAuthor) []models.ItemPerson { + people := make([]models.ItemPerson, 0, len(existing)+len(authors)) + for _, p := range existing { + if p.Kind == models.PersonKindAuthor || p.Kind == models.PersonKindNarrator { + continue + } + p.SortOrder = len(people) + people = append(people, p) + } + for _, author := range authors { + people = append(people, models.ItemPerson{ + Person: models.Person{ID: author.ID, Name: author.Name}, + Kind: models.PersonKindAuthor, + SortOrder: len(people), + }) + } + return people +} + +func ebookPeopleForReplace(existing []models.ItemPerson, getErr error, authors []ebookResolvedAuthor) ([]models.ItemPerson, error) { + if getErr != nil { + return nil, fmt.Errorf("get ebook people: %w", getErr) + } + return mergeEbookPeople(existing, authors), nil +} + +// ebookPeopleWriteAllowed reports whether file-embedded authors may be written +// for the item. Curated (provider-matched) items are fill-empty only: file +// metadata may supply authors when the item has none, but must never replace +// provider-enriched author credits. +func ebookPeopleWriteAllowed(curated bool, existing []models.ItemPerson) bool { + if !curated { + return true + } + for _, p := range existing { + if p.Kind == models.PersonKindAuthor { + return false + } + } + return true +} + +func (s *Scanner) upsertEbookPeople(ctx context.Context, contentID string, book *parsedEbook, curated bool) error { + if s.personRepo == nil { + return fmt.Errorf("personRepo not configured on Scanner") + } + if s.itemRepo == nil { + return fmt.Errorf("itemRepo not configured on Scanner") + } + + var desired []ebookCredit + for _, author := range book.Authors { + if name := strings.TrimSpace(author); name != "" { + desired = append(desired, ebookCredit{Name: name, Kind: models.PersonKindAuthor}) + } + } + if len(desired) == 0 { + return nil + } + + existing, err := s.itemRepo.GetPeople(ctx, contentID) + if err != nil { + return fmt.Errorf("get ebook people: %w", err) + } + if !ebookPeopleWriteAllowed(curated, existing) { + return nil + } + if ebookPeopleCreditsEqual(existing, desired) { + return nil + } + + authors := make([]ebookResolvedAuthor, 0, len(desired)) + for _, c := range desired { + personID, err := s.personRepo.FindOrCreate(ctx, models.Person{Name: c.Name}) + if err != nil { + return fmt.Errorf("find-or-create person %q: %w", c.Name, err) + } + authors = append(authors, ebookResolvedAuthor{ID: personID, Name: c.Name}) + } + people, err := ebookPeopleForReplace(existing, nil, authors) + if err != nil { + return err + } + return s.itemRepo.ReplacePeople(ctx, contentID, people) +} + +func (s *Scanner) upsertEbookSeries(ctx context.Context, contentID string, book *parsedEbook, curated bool) error { + if s == nil { + return fmt.Errorf("Scanner not configured") + } + if s.fileRepo == nil { + return fmt.Errorf("fileRepo not configured on Scanner") + } + + var currentName *string + var currentIdx *float64 + err := s.fileRepo.Pool().QueryRow(ctx, ` + SELECT series_name, series_index FROM ebook_series WHERE content_id = $1 + `, contentID).Scan(¤tName, ¤tIdx) + + plan, err := planEbookSeriesWrite(book, currentName, currentIdx, err, curated) + if err != nil { + return err + } + + switch plan.Kind { + case ebookSeriesWriteNone: + return nil + case ebookSeriesWriteDelete: + if _, delErr := s.fileRepo.Pool().Exec(ctx, + `DELETE FROM ebook_series WHERE content_id = $1`, contentID); delErr != nil { + return fmt.Errorf("delete ebook_series row: %w", delErr) + } + return nil + case ebookSeriesWriteUpsert: + var idx any + if plan.Index != nil { + idx = *plan.Index + } + if _, err := s.fileRepo.Pool().Exec(ctx, ` + INSERT INTO ebook_series (content_id, series_name, series_index, updated_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (content_id) DO UPDATE SET + series_name = EXCLUDED.series_name, + series_index = EXCLUDED.series_index, + updated_at = NOW() + `, contentID, plan.Name, idx); err != nil { + return fmt.Errorf("upsert ebook_series row: %w", err) + } + return nil + default: + return fmt.Errorf("unknown ebook_series write kind: %d", plan.Kind) + } +} + +func ebookSeriesDesired(book *parsedEbook) (string, *float64) { + if book == nil { + return "", nil + } + return strings.TrimSpace(book.Series), parseSeriesIndex(book.SeriesIndex) +} + +type ebookSeriesWriteKind int + +const ( + ebookSeriesWriteNone ebookSeriesWriteKind = iota + ebookSeriesWriteDelete + ebookSeriesWriteUpsert +) + +type ebookSeriesWritePlan struct { + Kind ebookSeriesWriteKind + Name string + Index *float64 +} + +// planEbookSeriesWrite decides how the ebook_series row should change for +// file-embedded series metadata. fillOnly applies to curated +// (provider-matched) items: the file may supply a series when the item has +// none, but must never replace or delete an existing (provider-enriched) row. +func planEbookSeriesWrite(book *parsedEbook, currentName *string, currentIdx *float64, queryErr error, fillOnly bool) (ebookSeriesWritePlan, error) { + if queryErr != nil { + if !errors.Is(queryErr, pgx.ErrNoRows) { + return ebookSeriesWritePlan{}, fmt.Errorf("query ebook_series: %w", queryErr) + } + currentName = nil + currentIdx = nil + } + + desiredName, desiredIdx := ebookSeriesDesired(book) + if desiredName == "" { + if currentName == nil || fillOnly { + return ebookSeriesWritePlan{Kind: ebookSeriesWriteNone}, nil + } + return ebookSeriesWritePlan{Kind: ebookSeriesWriteDelete}, nil + } + + if currentName != nil { + if fillOnly { + return ebookSeriesWritePlan{Kind: ebookSeriesWriteNone}, nil + } + if *currentName == desiredName && floatPtrEqual(currentIdx, desiredIdx) { + return ebookSeriesWritePlan{Kind: ebookSeriesWriteNone}, nil + } + } + + return ebookSeriesWritePlan{ + Kind: ebookSeriesWriteUpsert, + Name: desiredName, + Index: desiredIdx, + }, nil +} + +func ebookIdentityConfidence(book *parsedEbook) string { + if book == nil { + return "low" + } + + score := 0 + if book.Title != "" { + score++ + } + if len(book.Authors) > 0 { + score++ + } + if book.Year > 0 { + score++ + } + if book.ISBN != "" { + score++ + } + + switch { + case score >= 4: + return "high" + case score > 0: + return "medium" + default: + return "low" + } +} diff --git a/internal/scanner/ebook_test.go b/internal/scanner/ebook_test.go new file mode 100644 index 00000000..38a998a5 --- /dev/null +++ b/internal/scanner/ebook_test.go @@ -0,0 +1,1896 @@ +package scanner + +import ( + "archive/zip" + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/models" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type recordingEbookExecutor struct { + queries []string + args [][]any +} + +func (r *recordingEbookExecutor) Exec(_ context.Context, query string, args ...any) (pgconn.CommandTag, error) { + r.queries = append(r.queries, query) + r.args = append(r.args, append([]any(nil), args...)) + return pgconn.CommandTag{}, nil +} + +type fakeFilesystemItemReader struct { + items []*models.MediaItem + err error +} + +func (f *fakeFilesystemItemReader) GetByIDs(_ context.Context, _ []string) ([]*models.MediaItem, error) { + return f.items, f.err +} + +type fakeEbookCoverCacher struct { + calls int + data []byte + contentID string + err error +} + +func (f *fakeEbookCoverCacher) CacheEbookCover(_ context.Context, data []byte, contentID string) (string, string, string, error) { + f.calls++ + f.data = append([]byte(nil), data...) + f.contentID = contentID + if f.err != nil { + return "", "", "", f.err + } + return "local/ebooks/" + contentID + "/poster", ".webp", "thumb", nil +} + +type fakeEbookMetadataUpdater struct { + posterPath string + posterThumbhash string + getErr error + contentID string + setPosterPath string + setThumbhash string + setPrefix string + setCalls int + err error +} + +func (f *fakeEbookMetadataUpdater) GetPoster(_ context.Context, _ string) (string, string, error) { + return f.posterPath, f.posterThumbhash, f.getErr +} + +func (f *fakeEbookMetadataUpdater) SetLocalPoster(_ context.Context, contentID, posterPath, thumbhash, localPrefix string) (bool, error) { + f.setCalls++ + f.contentID = contentID + f.setPosterPath = posterPath + f.setThumbhash = thumbhash + f.setPrefix = localPrefix + return f.err == nil, f.err +} + +func TestSupportsEbookFile(t *testing.T) { + cases := []struct { + path string + want bool + }{ + {"book.epub", true}, + {"book.PDF", true}, + {"book.mobi", true}, + {"book.azw", true}, + {"book.azw3", true}, + {"book.FB2", true}, + {"book.fb2.zip", true}, + {"book.fbz", true}, + {"book.cbz", true}, + {"book.cbr", true}, + {"book.txt", false}, + {"book.md", false}, + {"README.md", false}, + {"book.mp3", false}, + {"movie.mkv", false}, + } + for _, tc := range cases { + if got := SupportsEbookFile(tc.path); got != tc.want { + t.Errorf("SupportsEbookFile(%q) = %v, want %v", tc.path, got, tc.want) + } + } +} + +func TestParseEbookFileSupportsReaderFormatsWithoutEmbeddedMetadata(t *testing.T) { + for _, tc := range []struct { + name string + wantFormat string + }{ + {"book.pdf", "pdf"}, + {"book.mobi", "mobi"}, + {"book.azw", "azw"}, + {"book.azw3", "azw3"}, + {"book.cbr", "cbr"}, + } { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), tc.name) + if err := os.WriteFile(path, []byte("placeholder"), 0o644); err != nil { + t.Fatalf("write file: %v", err) + } + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + if got.Format != tc.wantFormat { + t.Fatalf("Format = %q, want %q", got.Format, tc.wantFormat) + } + }) + } +} + +func TestNormalizeEbookISBNHandlesCommonLabels(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"ISBN-13: 978-0-306-40615-7", "9780306406157"}, + {"ISBN-10: 0-9752298-0-x", "097522980X"}, + } + for _, tc := range cases { + if got := normalizeEbookISBN(tc.in); got != tc.want { + t.Errorf("normalizeEbookISBN(%q) = %q, want %q", tc.in, got, tc.want) + } + } +} + +func TestParseEbookEPUBMetadata(t *testing.T) { + path := writeTestEPUB(t, []string{"ISBN: 978-0-306-40615-7"}) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Format != "epub" { + t.Fatalf("Format = %q, want epub", got.Format) + } + if got.Title != "The Test Ebook" { + t.Fatalf("Title = %q, want The Test Ebook", got.Title) + } + if strings.Join(got.Authors, ", ") != "Ada Writer, Ben Author" { + t.Fatalf("Authors = %v, want Ada Writer and Ben Author", got.Authors) + } + if got.ISBN != "9780306406157" { + t.Fatalf("ISBN = %q, want normalized ISBN", got.ISBN) + } + if got.Year != 2024 { + t.Fatalf("Year = %d, want 2024", got.Year) + } + if got.Publisher != "Silo Press" { + t.Fatalf("Publisher = %q, want Silo Press", got.Publisher) + } + wantPublishedAt := time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC) + if !got.PublishedAt.Equal(wantPublishedAt) { + t.Fatalf("PublishedAt = %v, want %v", got.PublishedAt, wantPublishedAt) + } + if got.Language != "en" { + t.Fatalf("Language = %q, want en", got.Language) + } + if strings.Join(got.Genres, ", ") != "Fiction, Adventure" { + t.Fatalf("Genres = %v, want Fiction and Adventure", got.Genres) + } + if got.Series != "Test Series" || got.SeriesIndex != "2" { + t.Fatalf("Series = %q/%q, want Test Series/2", got.Series, got.SeriesIndex) + } +} + +func TestParseEbookEPUBMetadataStripsHTMLDescription(t *testing.T) { + path := writeTestEPUBWithDescription(t, `<div><p><strong>Shanghai Dreams ...</strong> Magnus is stationed at Shanghai.<br>Published by The Electronic Book Company</p></div>`) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + want := "Shanghai Dreams ... Magnus is stationed at Shanghai. Published by The Electronic Book Company" + if got.Description != want { + t.Fatalf("Description = %q, want %q", got.Description, want) + } +} + +func TestParseEbookEPUBSkipsUUIDIdentifierBeforeISBN(t *testing.T) { + path := writeTestEPUB(t, []string{ + "550e8400-e29b-41d4-a716-446655440000", + "ISBN: 978-0-306-40615-7", + }) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + if got.ISBN != "9780306406157" { + t.Fatalf("ISBN = %q, want normalized ISBN after skipping UUID", got.ISBN) + } +} + +func TestParseEbookEPUBMetadataReadsISBNFromMetaTags(t *testing.T) { + path := writeTestEPUBWithMeta(t, nil, ` `) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + if got.ISBN != "9780306406157" { + t.Fatalf("ISBN = %q, want normalized ISBN from calibre meta tag", got.ISBN) + } +} + +func TestParseEbookEPUBMetadataHandlesLatin1OPF(t *testing.T) { + path := writeTestEPUBWithOPFBytes(t, []byte("\n"+ + ""+ + "Caf\xe9 Society"+ + "Fran\xe7ois Author"+ + "Cr\xe8me Press"+ + "")) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Café Society" || strings.Join(got.Authors, ", ") != "François Author" || got.Publisher != "Crème Press" { + t.Fatalf("parsed latin1 metadata = title %q authors %v publisher %q", got.Title, got.Authors, got.Publisher) + } +} + +func TestParseEbookEPUBMetadataHandlesWindows1251OPF(t *testing.T) { + path := writeTestEPUBWithOPFBytes(t, []byte("\n"+ + ""+ + "\xc2\xee\xe9\xed\xe0 \xe8 \xec\xe8\xf0"+ + "\xd2\xee\xeb\xf1\xf2\xee\xe9"+ + "")) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Война и мир" || strings.Join(got.Authors, ", ") != "Толстой" { + t.Fatalf("parsed windows-1251 metadata = title %q authors %v", got.Title, got.Authors) + } +} + +func TestParseEbookEPUBMetadataAllowsXMLVersion11(t *testing.T) { + path := writeTestEPUBWithOPFBytes(t, []byte(` + + Versioned Ebook + Ada Writer +`)) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Versioned Ebook" || strings.Join(got.Authors, ", ") != "Ada Writer" { + t.Fatalf("parsed XML 1.1 metadata = title %q authors %v", got.Title, got.Authors) + } +} + +func TestParseEbookEPUBExtractsManifestCover(t *testing.T) { + path := writeTestEPUBWithCover(t, "Images/cover.jpg", "image/jpeg", []byte("epub-cover-bytes")) + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Cover == nil { + t.Fatal("Cover = nil, want embedded EPUB cover") + } + if got.Cover.ContentType != "image/jpeg" { + t.Fatalf("Cover.ContentType = %q, want image/jpeg", got.Cover.ContentType) + } + if string(got.Cover.Bytes) != "epub-cover-bytes" { + t.Fatalf("Cover.Bytes = %q, want EPUB cover bytes", string(got.Cover.Bytes)) + } +} + +func TestParseEbookEPUBCoverSkipsXHTMLCoverPage(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.epub") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create epub: %v", err) + } + zw := zip.NewWriter(file) + add := func(name, body string) { + t.Helper() + w, err := zw.Create(name) + if err != nil { + t.Fatalf("create zip entry %s: %v", name, err) + } + if _, err := w.Write([]byte(body)); err != nil { + t.Fatalf("write zip entry %s: %v", name, err) + } + } + add("META-INF/container.xml", ` + + + + +`) + // EPUB2-style meta cover pointing at the XHTML cover *page*, with the + // real image declared later via properties="cover-image". + add("OPS/content.opf", ` + + + The Test Ebook + + + + + + +`) + add("OPS/cover.xhtml", "") + add("OPS/Images/cover.jpg", "real-cover-bytes") + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close epub: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Cover == nil { + t.Fatal("Cover = nil, want cover-image manifest item") + } + if got.Cover.ContentType != "image/jpeg" || string(got.Cover.Bytes) != "real-cover-bytes" { + t.Fatalf("Cover = %q/%q, want the image item, not the XHTML cover page", got.Cover.ContentType, string(got.Cover.Bytes)) + } +} + +func TestReadEPUBZipEntryRejectsOversizedEntry(t *testing.T) { + path := filepath.Join(t.TempDir(), "large.epub") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create epub: %v", err) + } + zw := zip.NewWriter(file) + w, err := zw.Create("OPS/content.opf") + if err != nil { + t.Fatalf("create zip entry: %v", err) + } + if _, err := w.Write([]byte(strings.Repeat("x", maxEPUBMetadataEntrySize+1))); err != nil { + t.Fatalf("write zip entry: %v", err) + } + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close file: %v", err) + } + + reader, err := zip.OpenReader(path) + if err != nil { + t.Fatalf("open zip: %v", err) + } + defer reader.Close() + + _, err = readEPUBZipEntry(&reader.Reader, "OPS/content.opf") + if err == nil || !strings.Contains(err.Error(), "epub entry too large") { + t.Fatalf("readEPUBZipEntry error = %v, want size limit error", err) + } +} + +func TestParseEbookFB2Metadata(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.fb2") + if err := os.WriteFile(path, []byte(` + + + + science fiction + AdaWriter + FB2 Test Ebook + 5 April 2022 + en + + + + FB2 Press + 2022 + 978-0-306-40615-7 + + +`), 0o644); err != nil { + t.Fatalf("write fb2: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Format != "fb2" || got.Title != "FB2 Test Ebook" { + t.Fatalf("Format/Title = %q/%q, want fb2/FB2 Test Ebook", got.Format, got.Title) + } + if strings.Join(got.Authors, ", ") != "Ada Writer" { + t.Fatalf("Authors = %v, want Ada Writer", got.Authors) + } + if got.ISBN != "9780306406157" { + t.Fatalf("ISBN = %q, want normalized ISBN", got.ISBN) + } + if got.Publisher != "FB2 Press" { + t.Fatalf("Publisher = %q, want FB2 Press", got.Publisher) + } + if got.Year != 2022 { + t.Fatalf("Year = %d, want 2022", got.Year) + } + wantPublishedAt := time.Date(2022, 4, 5, 0, 0, 0, 0, time.UTC) + if !got.PublishedAt.Equal(wantPublishedAt) { + t.Fatalf("PublishedAt = %v, want %v", got.PublishedAt, wantPublishedAt) + } + if got.Language != "en" { + t.Fatalf("Language = %q, want en", got.Language) + } + if strings.Join(got.Genres, ", ") != "science fiction" { + t.Fatalf("Genres = %v, want science fiction", got.Genres) + } + if got.Series != "FB2 Series" || got.SeriesIndex != "3" { + t.Fatalf("Series = %q/%q, want FB2 Series/3", got.Series, got.SeriesIndex) + } +} + +func TestParseEbookFBZMetadata(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.fb2.zip") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create fbz: %v", err) + } + zw := zip.NewWriter(file) + w, err := zw.Create("nested/book.fb2") + if err != nil { + t.Fatalf("create fb2 entry: %v", err) + } + if _, err := w.Write([]byte(` + + + + fantasy + AdaWriter + FBZ Test Ebook + + en + + + + Archive Press + 2021 + 978-0-306-40615-7 + + +`)); err != nil { + t.Fatalf("write fb2 entry: %v", err) + } + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close fbz: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Format != "fbz" || got.Title != "FBZ Test Ebook" { + t.Fatalf("Format/Title = %q/%q, want fbz/FBZ Test Ebook", got.Format, got.Title) + } + if strings.Join(got.Authors, ", ") != "Ada Writer" { + t.Fatalf("Authors = %v, want Ada Writer", got.Authors) + } + if got.ISBN != "9780306406157" { + t.Fatalf("ISBN = %q, want normalized ISBN", got.ISBN) + } + if got.Publisher != "Archive Press" || got.Year != 2021 { + t.Fatalf("Publisher/Year = %q/%d, want Archive Press/2021", got.Publisher, got.Year) + } + if got.Series != "Archive Series" || got.SeriesIndex != "4" { + t.Fatalf("Series = %q/%q, want Archive Series/4", got.Series, got.SeriesIndex) + } +} + +func TestParseEbookPDFInfoMetadata(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte(`%PDF-1.7 +1 0 obj +<< /Title (PDF Test Ebook) + /Author (Ada Writer; Ben Author) + /Subject (ISBN 978-0-306-40615-7) + /Keywords (science fiction, adventure) + /CreationDate (D:20240102030405Z) +>> +endobj +trailer +<< /Info 1 0 R >> +%%EOF`), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Format != "pdf" || got.Title != "PDF Test Ebook" { + t.Fatalf("Format/Title = %q/%q, want pdf/PDF Test Ebook", got.Format, got.Title) + } + if strings.Join(got.Authors, ", ") != "Ada Writer, Ben Author" { + t.Fatalf("Authors = %v, want Ada Writer and Ben Author", got.Authors) + } + if got.ISBN != "9780306406157" { + t.Fatalf("ISBN = %q, want normalized ISBN", got.ISBN) + } + if got.Year != 2024 { + t.Fatalf("Year = %d, want 2024", got.Year) + } + if strings.Join(got.Genres, ", ") != "science fiction, adventure" { + t.Fatalf("Genres = %v, want science fiction and adventure", got.Genres) + } +} + +func TestParseEbookPDFInfoMetadataDecodesUTF16BELiterals(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte("%PDF-1.7\n"+ + "1 0 obj\n"+ + "<< /Title (\xfe\xff\x00C\x00a\x00f\x00e)\n"+ + " /Author (\xfe\xff\x00T\x00h\x00o\x00m\x00a\x00s\x00 \x00D\x00 \x00S\x00e\x00e\x00l\x00e\x00y)\n"+ + ">>\nendobj\n%%EOF"), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Cafe" || strings.Join(got.Authors, ", ") != "Thomas D Seeley" { + t.Fatalf("PDF metadata = title %q authors %v, want decoded UTF-16BE", got.Title, got.Authors) + } +} + +func TestParseEbookPDFInfoMetadataPreservesUTF8Literals(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte("%PDF-1.7\n"+ + "1 0 obj\n"+ + "<< /Title (Caf\xc3\xa9 Society)\n"+ + " /Author (\xef\xbb\xbfFran\xc3\xa7ois Author)\n"+ + ">>\nendobj\n%%EOF"), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Café Society" { + t.Fatalf("Title = %q, want UTF-8 literal preserved without Windows-1252 mojibake", got.Title) + } + if strings.Join(got.Authors, ", ") != "François Author" { + t.Fatalf("Authors = %v, want UTF-8 BOM stripped and bytes preserved", got.Authors) + } +} + +func TestParseEbookPDFInfoMetadataFallsBackToWindows1252(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte("%PDF-1.7\n"+ + "1 0 obj\n"+ + "<< /Title (Caf\xe9 Society)\n"+ + ">>\nendobj\n%%EOF"), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Café Society" { + t.Fatalf("Title = %q, want Windows-1252 fallback for non-UTF-8 literal", got.Title) + } +} + +func TestParseEbookPDFInfoMetadataDecodesHexStrings(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte(`%PDF-1.7 +1 0 obj +<< /Title + /Author <41646120577269746572> + /CreationDate (0000-01-01) +>> +endobj +%%EOF`), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Title != "Hex Book" || strings.Join(got.Authors, ", ") != "Ada Writer" { + t.Fatalf("PDF hex metadata = title %q authors %v", got.Title, got.Authors) + } + if got.Year != 0 || !got.PublishedAt.IsZero() { + t.Fatalf("bad PDF date produced year/date = %d/%v, want ignored", got.Year, got.PublishedAt) + } +} + +func TestParseEbookCBZPageCount(t *testing.T) { + path := filepath.Join(t.TempDir(), "comic.cbz") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create cbz: %v", err) + } + zw := zip.NewWriter(file) + for _, name := range []string{ + "Comic/001.jpg", + "Comic/002.PNG", + "Comic/003.webp", + "Comic/notes.txt", + "__MACOSX/._001.jpg", + } { + w, err := zw.Create(name) + if err != nil { + t.Fatalf("create zip entry %s: %v", name, err) + } + if _, err := w.Write([]byte("placeholder")); err != nil { + t.Fatalf("write zip entry %s: %v", name, err) + } + } + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close cbz: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Format != "cbz" { + t.Fatalf("Format = %q, want cbz", got.Format) + } + if got.PageCount != 3 { + t.Fatalf("PageCount = %d, want 3", got.PageCount) + } +} + +func TestParseEbookCBZExtractsFirstReadablePageAsCover(t *testing.T) { + path := filepath.Join(t.TempDir(), "comic.cbz") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create cbz: %v", err) + } + zw := zip.NewWriter(file) + for _, entry := range []struct { + name string + body []byte + }{ + {name: "Comic/002.png", body: []byte("second-page")}, + {name: "Comic/001.jpg", body: []byte("first-page")}, + {name: "Comic/notes.txt", body: []byte("ignored")}, + } { + w, err := zw.Create(entry.name) + if err != nil { + t.Fatalf("create zip entry %s: %v", entry.name, err) + } + if _, err := w.Write(entry.body); err != nil { + t.Fatalf("write zip entry %s: %v", entry.name, err) + } + } + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close cbz: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Cover == nil { + t.Fatal("Cover = nil, want first CBZ page as cover") + } + if got.Cover.ContentType != "image/jpeg" { + t.Fatalf("Cover.ContentType = %q, want image/jpeg", got.Cover.ContentType) + } + if string(got.Cover.Bytes) != "first-page" { + t.Fatalf("Cover.Bytes = %q, want first page bytes", string(got.Cover.Bytes)) + } +} + +func TestParseEbookCBZPicksNaturallyFirstPageAsCover(t *testing.T) { + path := filepath.Join(t.TempDir(), "comic.cbz") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create cbz: %v", err) + } + zw := zip.NewWriter(file) + for _, entry := range []struct { + name string + body string + }{ + {name: "ch10/page10.jpg", body: "chapter-ten"}, + {name: "ch2/page2.jpg", body: "chapter-two"}, + } { + w, err := zw.Create(entry.name) + if err != nil { + t.Fatalf("create zip entry %s: %v", entry.name, err) + } + if _, err := w.Write([]byte(entry.body)); err != nil { + t.Fatalf("write zip entry %s: %v", entry.name, err) + } + } + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close cbz: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + + if got.Cover == nil { + t.Fatal("Cover = nil, want first page in natural order") + } + if string(got.Cover.Bytes) != "chapter-two" { + t.Fatalf("Cover.Bytes = %q, want ch2 before ch10 (natural order, not byte order)", string(got.Cover.Bytes)) + } +} + +func TestEbookTitleFromPathStripsCompoundFB2Extension(t *testing.T) { + cases := map[string]string{ + "/lib/Anna Karenina.fb2.zip": "Anna Karenina", + "/lib/Anna Karenina.FB2.ZIP": "Anna Karenina", + "/lib/Book.epub": "Book", + "/lib/vol.1.pdf": "vol.1", + } + for path, want := range cases { + if got := ebookTitleFromPath(path); got != want { + t.Errorf("ebookTitleFromPath(%q) = %q, want %q", path, got, want) + } + } +} + +func TestEbookSeriesDesiredParsesIndex(t *testing.T) { + name, idx := ebookSeriesDesired(&parsedEbook{ + Series: " The Expanse ", + SeriesIndex: "2 of 9", + }) + + if name != "The Expanse" { + t.Fatalf("series name = %q, want The Expanse", name) + } + if idx == nil || *idx != 2 { + t.Fatalf("series index = %v, want 2", idx) + } +} + +func TestUpsertEbookSeriesNilScannerReturnsError(t *testing.T) { + var s *Scanner + if err := s.upsertEbookSeries(context.Background(), "content-1", &parsedEbook{Series: "Series"}, false); err == nil { + t.Fatal("upsertEbookSeries nil scanner error = nil, want error") + } +} + +func TestUpsertEbookSeriesNilFileRepoReturnsError(t *testing.T) { + s := &Scanner{} + if err := s.upsertEbookSeries(context.Background(), "content-1", &parsedEbook{Series: "Series"}, false); err == nil { + t.Fatal("upsertEbookSeries nil fileRepo error = nil, want error") + } +} + +func TestPlanEbookSeriesWriteInsertsWhenRowAbsent(t *testing.T) { + plan, err := planEbookSeriesWrite(&parsedEbook{Series: " Series ", SeriesIndex: "2 of 9"}, nil, nil, pgx.ErrNoRows, false) + if err != nil { + t.Fatalf("planEbookSeriesWrite: %v", err) + } + if plan.Kind != ebookSeriesWriteUpsert || plan.Name != "Series" { + t.Fatalf("plan = %+v, want upsert Series", plan) + } + if plan.Index == nil || *plan.Index != 2 { + t.Fatalf("index = %v, want 2", plan.Index) + } +} + +func TestPlanEbookSeriesWriteBlankSeriesDeletesExistingAndSkipsAbsent(t *testing.T) { + currentName := "Series" + plan, err := planEbookSeriesWrite(&parsedEbook{Series: " "}, ¤tName, nil, nil, false) + if err != nil { + t.Fatalf("planEbookSeriesWrite delete: %v", err) + } + if plan.Kind != ebookSeriesWriteDelete { + t.Fatalf("blank existing plan = %+v, want delete", plan) + } + + plan, err = planEbookSeriesWrite(&parsedEbook{Series: ""}, nil, nil, pgx.ErrNoRows, false) + if err != nil { + t.Fatalf("planEbookSeriesWrite absent: %v", err) + } + if plan.Kind != ebookSeriesWriteNone { + t.Fatalf("blank absent plan = %+v, want none", plan) + } +} + +func TestPlanEbookSeriesWriteSkipsIdenticalRow(t *testing.T) { + currentName := "Series" + currentIdx := 3.5 + plan, err := planEbookSeriesWrite(&parsedEbook{Series: "Series", SeriesIndex: "3.5"}, ¤tName, ¤tIdx, nil, false) + if err != nil { + t.Fatalf("planEbookSeriesWrite: %v", err) + } + if plan.Kind != ebookSeriesWriteNone { + t.Fatalf("identical plan = %+v, want none", plan) + } +} + +func TestPlanEbookSeriesWriteAllowsNullNumericIndex(t *testing.T) { + plan, err := planEbookSeriesWrite(&parsedEbook{Series: "Series", SeriesIndex: "appendix"}, nil, nil, pgx.ErrNoRows, false) + if err != nil { + t.Fatalf("planEbookSeriesWrite: %v", err) + } + if plan.Kind != ebookSeriesWriteUpsert || plan.Index != nil { + t.Fatalf("plan = %+v, want upsert with nil index", plan) + } +} + +func TestPlanEbookSeriesWriteFillOnlyNeverReplacesOrDeletesExistingRow(t *testing.T) { + currentName := "Provider Series" + currentIdx := 1.0 + + // A curated item with an existing (provider-enriched) row must not be + // replaced by a differing file-embedded series... + plan, err := planEbookSeriesWrite(&parsedEbook{Series: "File Series", SeriesIndex: "4"}, ¤tName, ¤tIdx, nil, true) + if err != nil { + t.Fatalf("planEbookSeriesWrite replace: %v", err) + } + if plan.Kind != ebookSeriesWriteNone { + t.Fatalf("fill-only replace plan = %+v, want none", plan) + } + + // ...nor deleted when the file carries no series at all. + plan, err = planEbookSeriesWrite(&parsedEbook{Series: ""}, ¤tName, ¤tIdx, nil, true) + if err != nil { + t.Fatalf("planEbookSeriesWrite delete: %v", err) + } + if plan.Kind != ebookSeriesWriteNone { + t.Fatalf("fill-only delete plan = %+v, want none", plan) + } +} + +func TestPlanEbookSeriesWriteFillOnlyInsertsWhenRowAbsent(t *testing.T) { + plan, err := planEbookSeriesWrite(&parsedEbook{Series: "File Series", SeriesIndex: "2"}, nil, nil, pgx.ErrNoRows, true) + if err != nil { + t.Fatalf("planEbookSeriesWrite: %v", err) + } + if plan.Kind != ebookSeriesWriteUpsert || plan.Name != "File Series" { + t.Fatalf("plan = %+v, want fill-empty upsert", plan) + } +} + +func TestEbookPeopleWriteAllowedFillsEmptyOnlyWhenCurated(t *testing.T) { + providerAuthor := []models.ItemPerson{{Person: models.Person{Name: "Provider Author"}, Kind: models.PersonKindAuthor}} + nonAuthorOnly := []models.ItemPerson{{Person: models.Person{Name: "Manual Writer"}, Kind: models.PersonKindWriter}} + + cases := []struct { + name string + curated bool + existing []models.ItemPerson + want bool + }{ + {"pending item always writes", false, providerAuthor, true}, + {"curated with provider authors is read-only", true, providerAuthor, false}, + {"curated without author credits fills empty", true, nil, true}, + {"curated with only non-author credits fills empty", true, nonAuthorOnly, true}, + } + for _, tc := range cases { + if got := ebookPeopleWriteAllowed(tc.curated, tc.existing); got != tc.want { + t.Errorf("%s: ebookPeopleWriteAllowed = %v, want %v", tc.name, got, tc.want) + } + } +} + +func TestPlanEbookSeriesWriteReturnsQueryError(t *testing.T) { + queryErr := errors.New("query failed") + if _, err := planEbookSeriesWrite(&parsedEbook{Series: "Series"}, nil, nil, queryErr, false); !errors.Is(err, queryErr) { + t.Fatalf("planEbookSeriesWrite error = %v, want query error", err) + } +} + +func TestEbookIdentityConfidenceReflectsMetadataCompleteness(t *testing.T) { + book := &parsedEbook{Title: "Tagged Ebook", Authors: []string{"Author"}, Year: 2024, ISBN: "9780306406157"} + if got := ebookIdentityConfidence(book); got != "high" { + t.Fatalf("complete metadata confidence = %q, want high", got) + } + + book = &parsedEbook{Title: "Tagged Ebook", Authors: []string{"Author"}, Year: 2024} + if got := ebookIdentityConfidence(book); got != "medium" { + t.Fatalf("partial metadata confidence = %q, want medium", got) + } + + book = &parsedEbook{} + if got := ebookIdentityConfidence(book); got != "low" { + t.Fatalf("empty metadata confidence = %q, want low", got) + } +} + +func TestEbookContentGroupKeyGroupsMultipleSiblingFormatsByTitleAuthor(t *testing.T) { + want := ebookContentGroupKey(&parsedEbook{ + Format: "epub", + Title: " The Test Ebook ", + Authors: []string{"Ada Writer", "Ben Author"}, + }, "/library/Ada Writer/The Test Ebook.epub") + if want == "" { + t.Fatal("ebookContentGroupKey returned blank") + } + for _, tc := range []struct { + format string + path string + }{ + {format: "mobi", path: "/library/Ada Writer/The Test Ebook.mobi"}, + {format: "azw3", path: "/library/Ada Writer/The Test Ebook.azw3"}, + {format: "pdf", path: "/library/Ada Writer/The Test Ebook.pdf"}, + {format: "fb2", path: "/library/Ada Writer/The Test Ebook.fb2"}, + } { + got := ebookContentGroupKey(&parsedEbook{ + Format: tc.format, + Title: "The Test Ebook", + Authors: []string{"ben author", "ada writer"}, + }, tc.path) + if got != want { + t.Fatalf("%s content group key = %q, want %q", tc.format, got, want) + } + if strings.Contains(got, tc.format) { + t.Fatalf("content group key should not include file format %q: %q", tc.format, got) + } + } +} + +func TestEbookContentGroupKeyPrefersISBNAcrossTitleVariants(t *testing.T) { + first := ebookContentGroupKey(&parsedEbook{ + Format: "epub", + Title: "The Test Ebook", + Authors: []string{"Ada Writer"}, + ISBN: "9780306406157", + }, "/library/Ada Writer/The Test Ebook.epub") + second := ebookContentGroupKey(&parsedEbook{ + Format: "pdf", + Title: "The Test Ebook: Revised Edition", + Authors: []string{"Someone Else"}, + ISBN: "9780306406157", + }, "/library/Other Name.pdf") + + if first != second { + t.Fatalf("ISBN-backed content group keys differ: %q != %q", first, second) + } +} + +func TestResolveEbookMediaItemCreatesNewWhenRootHasNoClaim(t *testing.T) { + finder := &fakeRootContentFinder{} + writer := &fakeFilesystemItemWriter{} + + got, err := resolveEbookMediaItem(context.Background(), finder, writer, 7, "/library/Author/Book.epub", &parsedEbook{ + Title: "Book", Year: 2024, Authors: []string{"Author"}, Description: "Overview", Publisher: "Publisher", Genres: []string{"Fiction"}, Language: "en", + }) + if err != nil { + t.Fatalf("resolveEbookMediaItem: %v", err) + } + if got == "" || len(writer.upserts) != 1 { + t.Fatalf("contentID/upserts = %q/%d, want id and one upsert", got, len(writer.upserts)) + } + item := writer.upserts[0] + if item.Type != "ebook" || item.Title != "Book" || item.Year != 2024 || item.Overview != "Overview" || item.OriginalLanguage != "en" { + t.Fatalf("upserted ebook item = %+v", item) + } + if item.Status != "pending" { + t.Fatalf("Status = %q, want pending so enrichment can promote it to matched", item.Status) + } +} + +func TestResolveEbookMediaItemReusesRootScopedContentID(t *testing.T) { + finder := &fakeRootContentFinder{contentID: "ebook-root-id"} + writer := &fakeFilesystemItemWriter{} + + got, err := resolveEbookMediaItem(context.Background(), finder, writer, 7, "/library/Author/Book.epub", &parsedEbook{Title: "Book"}) + if err != nil { + t.Fatalf("resolveEbookMediaItem: %v", err) + } + if got != "ebook-root-id" { + t.Fatalf("contentID = %q, want root-scoped id", got) + } + if len(writer.upserts) != 0 { + t.Fatalf("unexpected item upsert for existing root: %d", len(writer.upserts)) + } +} + +func TestResolveEbookExistingRootAppliesParsedMetadata(t *testing.T) { + reader := &fakeFilesystemItemReader{items: []*models.MediaItem{{ + ContentID: "ebook-root-id", + Type: "ebook", + Title: "Old Title", + Year: 2020, + OriginalLanguage: "", + }}} + writer := &fakeFilesystemItemWriter{} + + curated, err := updateExistingEbookMediaItem(context.Background(), reader, writer, "ebook-root-id", &parsedEbook{ + Title: "New Title", + Year: 2026, + Publisher: "New Press", + Genres: []string{"Fiction"}, + Language: "en", + }) + if err != nil { + t.Fatalf("updateExistingEbookMediaItem: %v", err) + } + if curated { + t.Fatal("curated = true, want false for an unmatched item") + } + if len(writer.upserts) != 1 { + t.Fatalf("upserts = %d, want 1", len(writer.upserts)) + } + item := writer.upserts[0] + if item.ContentID != "ebook-root-id" || item.Type != "ebook" || item.Title != "New Title" || item.Year != 2026 || item.OriginalLanguage != "en" { + t.Fatalf("updated item = %+v", item) + } + if strings.Join(item.Studios, ",") != "New Press" || strings.Join(item.Genres, ",") != "Fiction" { + t.Fatalf("updated item studios/genres = %+v/%+v", item.Studios, item.Genres) + } +} + +func TestResolveEbookExistingRootReplacesHTMLOverview(t *testing.T) { + reader := &fakeFilesystemItemReader{items: []*models.MediaItem{{ + ContentID: "ebook-root-id", + Type: "ebook", + Title: "Old Title", + Overview: "

Raw overview

", + }}} + writer := &fakeFilesystemItemWriter{} + + _, err := updateExistingEbookMediaItem(context.Background(), reader, writer, "ebook-root-id", &parsedEbook{ + Title: "New Title", + Description: "Clean overview", + }) + if err != nil { + t.Fatalf("updateExistingEbookMediaItem: %v", err) + } + if len(writer.upserts) != 1 { + t.Fatalf("upserts = %d, want 1", len(writer.upserts)) + } + if writer.upserts[0].Overview != "Clean overview" { + t.Fatalf("Overview = %q, want Clean overview", writer.upserts[0].Overview) + } +} + +func TestApplyEbookToMediaItemPreservesExistingYearWhenParsedYearMissing(t *testing.T) { + item := &models.MediaItem{ + Type: "ebook", + Title: "Existing", + Year: 1999, + } + + applyEbookToMediaItem(item, &parsedEbook{Title: "Updated", Year: 0}) + + if item.Title != "Updated" { + t.Fatalf("Title = %q, want Updated", item.Title) + } + if item.Year != 1999 { + t.Fatalf("Year = %d, want preserved 1999", item.Year) + } +} + +func TestEbookPeopleCreditsEqualAuthorsOnly(t *testing.T) { + existing := []models.ItemPerson{ + {Person: models.Person{Name: "Ada Writer"}, Kind: models.PersonKindAuthor, SortOrder: 0}, + {Person: models.Person{Name: "Ben Author"}, Kind: models.PersonKindAuthor, SortOrder: 1}, + {Person: models.Person{Name: "Manual Contributor"}, Kind: models.PersonKindWriter, SortOrder: 2}, + } + desired := []ebookCredit{ + {Name: "Ada Writer", Kind: models.PersonKindAuthor}, + {Name: "Ben Author", Kind: models.PersonKindAuthor}, + } + if !ebookPeopleCreditsEqual(existing, desired) { + t.Fatal("expected matching author credits to compare equal while ignoring non-authors") + } + + existing[1] = models.ItemPerson{Person: models.Person{Name: "Other Author"}, Kind: models.PersonKindAuthor, SortOrder: 1} + if ebookPeopleCreditsEqual(existing, desired) { + t.Fatal("expected different author credit to make ebook author set differ") + } +} + +func TestEbookPeopleCreditsEqualRejectsStaleNarratorCredit(t *testing.T) { + existing := []models.ItemPerson{ + {Person: models.Person{Name: "Ada Writer"}, Kind: models.PersonKindAuthor, SortOrder: 0}, + {Person: models.Person{Name: "Legacy Narrator"}, Kind: models.PersonKindNarrator, SortOrder: 1}, + } + desired := []ebookCredit{ + {Name: "Ada Writer", Kind: models.PersonKindAuthor}, + } + + if ebookPeopleCreditsEqual(existing, desired) { + t.Fatal("expected stale narrator credit to force ebook people replacement") + } +} + +func TestEbookPeopleMergePreservesExistingNonAuthorCreditsExceptNarrators(t *testing.T) { + existing := []models.ItemPerson{ + {Person: models.Person{ID: 10, Name: "Old Author"}, Kind: models.PersonKindAuthor, SortOrder: 0}, + {Person: models.Person{ID: 20, Name: "Manual Writer"}, Kind: models.PersonKindWriter, SortOrder: 1, Character: "essay"}, + {Person: models.Person{ID: 30, Name: "Manual Narrator"}, Kind: models.PersonKindNarrator, SortOrder: 2}, + } + authors := []ebookResolvedAuthor{ + {ID: 40, Name: "New Author"}, + } + + got := mergeEbookPeople(existing, authors) + if len(got) != 2 { + t.Fatalf("merged people len = %d, want 2: %+v", len(got), got) + } + if got[0].Person.ID != 20 || got[0].Kind != models.PersonKindWriter || got[0].Character != "essay" { + t.Fatalf("first preserved non-author = %+v", got[0]) + } + if got[1].Person.ID != 40 || got[1].Kind != models.PersonKindAuthor || got[1].SortOrder != 1 { + t.Fatalf("new author credit = %+v", got[1]) + } +} + +func TestEbookPeopleReplacePlanReturnsGetPeopleError(t *testing.T) { + wantErr := errors.New("get people failed") + _, err := ebookPeopleForReplace(nil, wantErr, []ebookResolvedAuthor{{ID: 40, Name: "New Author"}}) + if !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want %v", err, wantErr) + } +} + +func TestScanEbookBuildMediaFileSetsCorePersistenceFields(t *testing.T) { + modifiedAt := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC) + book := &parsedEbook{Format: "epub", Title: "Book", Authors: []string{"Author"}, Year: 2024, ISBN: "9780306406157", PageCount: 321} + + got := buildEbookMediaFile(&models.MediaFolder{ID: 44}, "content-1", "/library/Book.epub", 1234, modifiedAt, book, ebookContentGroupKey(book, "/library/Book.epub")) + if got.ContentID != "content-1" || got.MediaFolderID != 44 { + t.Fatalf("ids = %q/%d, want content-1/44", got.ContentID, got.MediaFolderID) + } + if got.BaseType != "ebook" || got.BaseTitle != "Book" || got.BaseYear != 2024 || got.Container != "epub" || got.ProbeSource != "local" { + t.Fatalf("ebook file metadata = %+v", got) + } + if got.Duration != 321 { + t.Fatalf("Duration = %d, want ebook page count", got.Duration) + } + if got.CanonicalRootPath != "/library/Book.epub" || got.ObservedRootPath != "/library/Book.epub" || got.FilePath != "/library/Book.epub" { + t.Fatalf("ebook paths = canonical %q observed %q file %q", got.CanonicalRootPath, got.ObservedRootPath, got.FilePath) + } + if got.ContentGroupKey != "ebook:isbn:9780306406157" { + t.Fatalf("ContentGroupKey = %q, want ISBN-backed ebook group", got.ContentGroupKey) + } + if got.GroupKeyVersion != ebookGroupKeyVersion { + t.Fatalf("GroupKeyVersion = %d, want %d", got.GroupKeyVersion, ebookGroupKeyVersion) + } + if got.FileSize != 1234 || got.FileModifiedAt == nil || !got.FileModifiedAt.Equal(modifiedAt) { + t.Fatalf("file facts = size %d modified %v", got.FileSize, got.FileModifiedAt) + } +} + +func TestApplyEbookLocalCoverCachesEmbeddedAndSetsPoster(t *testing.T) { + cacher := &fakeEbookCoverCacher{} + updater := &fakeEbookMetadataUpdater{} + + err := applyEbookLocalCover(context.Background(), updater, cacher, "content-1", filepath.Join(t.TempDir(), "book.epub"), &parsedEbook{ + Cover: &parsedEbookCover{ + ContentType: "image/jpeg", + Bytes: []byte("cover-bytes"), + }, + }) + if err != nil { + t.Fatalf("applyEbookLocalCover: %v", err) + } + + if cacher.calls != 1 || cacher.contentID != "content-1" || string(cacher.data) != "cover-bytes" { + t.Fatalf("cache call = calls %d content %q data %q", cacher.calls, cacher.contentID, string(cacher.data)) + } + if updater.setCalls != 1 || updater.contentID != "content-1" { + t.Fatalf("set call = calls %d content %q", updater.setCalls, updater.contentID) + } + if updater.setPosterPath != "local/ebooks/content-1/poster/original.webp" { + t.Fatalf("poster path = %q", updater.setPosterPath) + } + if updater.setThumbhash != "thumb" { + t.Fatalf("poster thumbhash = %q", updater.setThumbhash) + } + if updater.setPrefix != localEbookPosterPrefix { + t.Fatalf("local prefix = %q, want %q", updater.setPrefix, localEbookPosterPrefix) + } +} + +func TestApplyEbookLocalCoverPreservesProviderPoster(t *testing.T) { + cacher := &fakeEbookCoverCacher{} + updater := &fakeEbookMetadataUpdater{posterPath: "ebook-metadata/ebooks/content-1/poster/original.webp"} + + err := applyEbookLocalCover(context.Background(), updater, cacher, "content-1", filepath.Join(t.TempDir(), "book.epub"), &parsedEbook{ + Cover: &parsedEbookCover{Bytes: []byte("cover-bytes")}, + }) + if err != nil { + t.Fatalf("applyEbookLocalCover: %v", err) + } + + if cacher.calls != 0 { + t.Fatalf("cache calls = %d, want 0", cacher.calls) + } + if updater.setCalls != 0 { + t.Fatalf("set calls = %d, want 0", updater.setCalls) + } +} + +func TestApplyEbookLocalCoverRefreshesStaleLocalPoster(t *testing.T) { + cacher := &fakeEbookCoverCacher{} + updater := &fakeEbookMetadataUpdater{ + posterPath: "local/ebooks/content-1/poster/original.webp", + posterThumbhash: "stale-thumb", + } + + err := applyEbookLocalCover(context.Background(), updater, cacher, "content-1", filepath.Join(t.TempDir(), "book.epub"), &parsedEbook{ + Cover: &parsedEbookCover{Bytes: []byte("replacement-cover-bytes")}, + }) + if err != nil { + t.Fatalf("applyEbookLocalCover: %v", err) + } + + if cacher.calls != 1 || string(cacher.data) != "replacement-cover-bytes" { + t.Fatalf("cache call = calls %d data %q, want refresh of locally owned poster", cacher.calls, string(cacher.data)) + } + if updater.setCalls != 1 { + t.Fatalf("set calls = %d, want 1", updater.setCalls) + } +} + +func TestApplyEbookLocalCoverPrefersSidecarOverEmbedded(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "book.epub"), []byte("book"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), []byte("ebook-sidecar-cover"), 0o644); err != nil { + t.Fatalf("write cover: %v", err) + } + cacher := &fakeEbookCoverCacher{} + updater := &fakeEbookMetadataUpdater{} + + err := applyEbookLocalCover(context.Background(), updater, cacher, "content-1", filepath.Join(dir, "book.epub"), &parsedEbook{ + Cover: &parsedEbookCover{Bytes: []byte("embedded-cover")}, + }) + if err != nil { + t.Fatalf("applyEbookLocalCover: %v", err) + } + + if cacher.calls != 1 || string(cacher.data) != "ebook-sidecar-cover" { + t.Fatalf("cache call = calls %d data %q, want sidecar bytes", cacher.calls, string(cacher.data)) + } + if updater.setCalls != 1 || updater.setPosterPath != "local/ebooks/content-1/poster/original.webp" { + t.Fatalf("poster update = calls %d path %q", updater.setCalls, updater.setPosterPath) + } +} + +func TestFindSidecarBookCoverIgnoresGenericNamesInMultiBookDirectories(t *testing.T) { + dir := t.TempDir() + for _, name := range []string{"alpha.epub", "beta.epub"} { + if err := os.WriteFile(filepath.Join(dir, name), []byte("book"), 0o644); err != nil { + t.Fatalf("write ebook %s: %v", name, err) + } + } + if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), []byte("shared-cover"), 0o644); err != nil { + t.Fatalf("write cover: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "beta.jpg"), []byte("beta-cover"), 0o644); err != nil { + t.Fatalf("write cover: %v", err) + } + + cover, _, err := findSidecarBookCover(filepath.Join(dir, "alpha.epub")) + if err != nil { + t.Fatalf("findSidecarBookCover(alpha): %v", err) + } + if cover != nil { + t.Fatalf("alpha cover = %q, want none (generic cover.jpg must not claim every book)", string(cover.Bytes)) + } + + cover, _, err = findSidecarBookCover(filepath.Join(dir, "beta.epub")) + if err != nil { + t.Fatalf("findSidecarBookCover(beta): %v", err) + } + if cover == nil || string(cover.Bytes) != "beta-cover" { + t.Fatalf("beta cover = %v, want filename-matched beta.jpg", cover) + } +} + +func TestScanEbookPersistenceSQLWritesLibraryMembershipAndISBNOnly(t *testing.T) { + ctx := context.Background() + exec := &recordingEbookExecutor{} + + if err := insertEbookLibraryMembership(ctx, exec, "content-1", 44); err != nil { + t.Fatalf("insertEbookLibraryMembership: %v", err) + } + if err := insertEbookISBNProviderID(ctx, exec, "content-1", "9780306406157"); err != nil { + t.Fatalf("insertEbookISBNProviderID: %v", err) + } + if len(exec.queries) != 2 { + t.Fatalf("queries = %d, want 2", len(exec.queries)) + } + if !strings.Contains(exec.queries[0], "media_item_libraries") || exec.args[0][0] != "content-1" || exec.args[0][1] != 44 { + t.Fatalf("library membership write = query %q args %+v", exec.queries[0], exec.args[0]) + } + if !strings.Contains(exec.queries[1], "media_item_provider_ids") || !strings.Contains(exec.queries[1], "'isbn'") || !strings.Contains(exec.queries[1], "'ebook'") { + t.Fatalf("ISBN provider write query = %q", exec.queries[1]) + } + if strings.Contains(exec.queries[1], "asin") || exec.args[1][0] != "content-1" || exec.args[1][1] != "9780306406157" { + t.Fatalf("ISBN provider write args/query = query %q args %+v", exec.queries[1], exec.args[1]) + } +} + +func TestCollectEbookRootScansExcludesUnmountedRootFromReconciliation(t *testing.T) { + healthy := t.TempDir() + if err := os.WriteFile(filepath.Join(healthy, "book.epub"), []byte("x"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + unmounted := filepath.Join(t.TempDir(), "gone") + + scans, err := collectEbookRootScans(context.Background(), 44, []string{unmounted, healthy}) + if err != nil { + t.Fatalf("collectEbookRootScans: %v", err) + } + if len(scans) != 2 { + t.Fatalf("scans = %d, want 2", len(scans)) + } + if !scans[0].failed() || scans[0].rootErr == nil { + t.Fatalf("unmounted root scan = %+v, want failed with rootErr", scans[0]) + } + if scans[1].failed() || len(scans[1].files) != 1 { + t.Fatalf("healthy root scan = %+v, want 1 file and no failure", scans[1]) + } + + reconcileRoots, sawFiles := splitEbookReconcileRoots(scans) + if !sawFiles { + t.Fatal("sawFiles = false, want true for the healthy root") + } + if len(reconcileRoots) != 1 || reconcileRoots[0] != healthy { + t.Fatalf("reconcileRoots = %v, want only the healthy root: an unmounted root must never be reconciled (no mass-missing, no deletion)", reconcileRoots) + } +} + +func TestCollectEbookRootScansTreatsNonDirectoryRootAsFailed(t *testing.T) { + dir := t.TempDir() + fileRoot := filepath.Join(dir, "book.epub") + if err := os.WriteFile(fileRoot, []byte("x"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + + scans, err := collectEbookRootScans(context.Background(), 44, []string{fileRoot}) + if err != nil { + t.Fatalf("collectEbookRootScans: %v", err) + } + if len(scans) != 1 || !scans[0].failed() { + t.Fatalf("scans = %+v, want one failed scan for a non-directory root", scans) + } + // The file itself is still indexed; only reconciliation is withheld. + if len(scans[0].files) != 1 { + t.Fatalf("files = %v, want the ebook file root indexed", scans[0].files) + } + if roots, _ := splitEbookReconcileRoots(scans); len(roots) != 0 { + t.Fatalf("reconcileRoots = %v, want none", roots) + } +} + +func TestCollectEbookRootScansMidWalkSubtreeErrorExcludesRoot(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("permission-based subtree failure cannot be simulated as root") + } + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "readable.epub"), []byte("x"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + locked := filepath.Join(root, "zz-locked") + if err := os.Mkdir(locked, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(filepath.Join(locked, "hidden.epub"), []byte("x"), 0o644); err != nil { + t.Fatalf("write hidden ebook: %v", err) + } + if err := os.Chmod(locked, 0o000); err != nil { + t.Fatalf("chmod: %v", err) + } + t.Cleanup(func() { _ = os.Chmod(locked, 0o755) }) + + scans, err := collectEbookRootScans(context.Background(), 44, []string{root}) + if err != nil { + t.Fatalf("collectEbookRootScans: %v", err) + } + if len(scans) != 1 { + t.Fatalf("scans = %d, want 1", len(scans)) + } + if scans[0].walkFailures == 0 || !scans[0].failed() { + t.Fatalf("scan = %+v, want walk failure recorded for unreadable subtree", scans[0]) + } + if len(scans[0].files) != 1 { + t.Fatalf("files = %v, want the readable ebook still indexed", scans[0].files) + } + // The unseen hidden.epub must not be marked missing: the whole root sits + // out of reconciliation. + if roots, _ := splitEbookReconcileRoots(scans); len(roots) != 0 { + t.Fatalf("reconcileRoots = %v, want none after a mid-walk subtree error", roots) + } +} + +func TestCollectEbookRootScansFollowsSymlinkedRoot(t *testing.T) { + target := t.TempDir() + if err := os.WriteFile(filepath.Join(target, "book.epub"), []byte("x"), 0o644); err != nil { + t.Fatalf("write ebook: %v", err) + } + link := filepath.Join(t.TempDir(), "library") + if err := os.Symlink(target, link); err != nil { + t.Fatalf("symlink: %v", err) + } + + scans, err := collectEbookRootScans(context.Background(), 44, []string{link}) + if err != nil { + t.Fatalf("collectEbookRootScans: %v", err) + } + if len(scans) != 1 || scans[0].failed() { + t.Fatalf("scans = %+v, want one clean scan through the symlinked root", scans) + } + want := filepath.Join(link, "book.epub") + if len(scans[0].files) != 1 || scans[0].files[0] != want { + t.Fatalf("files = %v, want %q recorded under the logical (symlink) path", scans[0].files, want) + } + if roots, sawFiles := splitEbookReconcileRoots(scans); !sawFiles || len(roots) != 1 { + t.Fatalf("reconcileRoots/sawFiles = %v/%v, want symlinked root reconcilable", roots, sawFiles) + } +} + +type fakeEbookCleanupGuard struct { + allowance bool + consumeCalls int + warnings []string + consumeErr error +} + +func (f *fakeEbookCleanupGuard) ConsumeEmptyCleanupAllowance(_ context.Context, _ int) (bool, error) { + f.consumeCalls++ + return f.allowance, f.consumeErr +} + +func (f *fakeEbookCleanupGuard) SetScanWarning(_ context.Context, _ int, code, _ string, _ time.Time) error { + f.warnings = append(f.warnings, code) + return nil +} + +func TestEbookEmptyCleanupAllowedRequiresExplicitAllowance(t *testing.T) { + guard := &fakeEbookCleanupGuard{allowance: false} + + allowed, err := ebookEmptyCleanupAllowed(context.Background(), guard, 44, true) + if err != nil { + t.Fatalf("ebookEmptyCleanupAllowed: %v", err) + } + if allowed { + t.Fatal("allowed = true, want false: an empty root must not reconcile without confirmation") + } + if guard.consumeCalls != 1 { + t.Fatalf("consumeCalls = %d, want 1", guard.consumeCalls) + } + if len(guard.warnings) != 1 || guard.warnings[0] != "empty_root" { + t.Fatalf("warnings = %v, want one empty_root scan warning", guard.warnings) + } +} + +func TestEbookEmptyCleanupAllowedConsumesAllowanceOnce(t *testing.T) { + guard := &fakeEbookCleanupGuard{allowance: true} + + allowed, err := ebookEmptyCleanupAllowed(context.Background(), guard, 44, true) + if err != nil { + t.Fatalf("ebookEmptyCleanupAllowed: %v", err) + } + if !allowed { + t.Fatal("allowed = false, want true once the operator confirmed cleanup") + } + if len(guard.warnings) != 0 { + t.Fatalf("warnings = %v, want none for a confirmed cleanup", guard.warnings) + } +} + +func TestEbookEmptyCleanupAllowedNeverConsumesForSubtreeScans(t *testing.T) { + guard := &fakeEbookCleanupGuard{allowance: true} + + allowed, err := ebookEmptyCleanupAllowed(context.Background(), guard, 44, false) + if err != nil { + t.Fatalf("ebookEmptyCleanupAllowed: %v", err) + } + if allowed || guard.consumeCalls != 0 { + t.Fatalf("allowed/consumeCalls = %v/%d, want subtree scans to skip cleanup without touching the folder allowance", allowed, guard.consumeCalls) + } +} + +func TestEbookEmptyCleanupAllowedNilGuardDeniesCleanup(t *testing.T) { + allowed, err := ebookEmptyCleanupAllowed(context.Background(), nil, 44, true) + if err != nil || allowed { + t.Fatalf("allowed/err = %v/%v, want denial when no guard repository is wired", allowed, err) + } +} + +func TestScanEbookFolderReturnsErrorWhenEveryReconcileFails(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "bad.epub"), []byte("not a real epub"), 0o644); err != nil { + t.Fatalf("write fake ebook: %v", err) + } + + s := &Scanner{} + err := s.ScanEbookFolder(context.Background(), &models.MediaFolder{ID: 44, Paths: []string{root}}) + if err == nil { + t.Fatal("ScanEbookFolder returned nil, want aggregate failure") + } + if !strings.Contains(err.Error(), "folder_id=44") { + t.Fatalf("error = %q, want folder id", err) + } +} + +func TestScanEbookFolderReturnsCanceledContext(t *testing.T) { + root := t.TempDir() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + s := &Scanner{} + err := s.ScanEbookFolder(ctx, &models.MediaFolder{ID: 44, Paths: []string{root}}) + if !errors.Is(err, context.Canceled) { + t.Fatalf("ScanEbookFolder error = %v, want context.Canceled", err) + } +} + +func TestScanEbookFolderReportsProgress(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "broken.epub"), []byte("not a zip"), 0o644); err != nil { + t.Fatalf("write ebook fixture: %v", err) + } + var updates []ProgressUpdate + ctx := WithProgressReporter(context.Background(), func(update ProgressUpdate) { + updates = append(updates, update) + }) + + err := (&Scanner{}).ScanEbookFolder(ctx, &models.MediaFolder{ID: 44, Paths: []string{root}}) + if err == nil { + t.Fatal("ScanEbookFolder returned nil, want aggregate failure") + } + + if len(updates) == 0 { + t.Fatal("expected ebook scanner progress updates") + } + if updates[0].Phase != "ebook_scan" || updates[0].TotalFiles != 1 { + t.Fatalf("first progress update = %+v, want ebook_scan discovery total", updates[0]) + } + last := updates[len(updates)-1] + if last.FilesProcessed != 1 || last.Errors != 1 { + t.Fatalf("last progress update = %+v, want processed/error counts", last) + } +} + +func writeTestEPUB(t *testing.T, identifiers []string) string { + return writeTestEPUBWithMeta(t, identifiers, "") +} + +func writeTestEPUBWithMeta(t *testing.T, identifiers []string, extraMetaXML string) string { + return writeTestEPUBWithDescriptionAndMeta(t, identifiers, "Back cover copy", extraMetaXML) +} + +func writeTestEPUBWithDescription(t *testing.T, description string) string { + return writeTestEPUBWithDescriptionAndMeta(t, []string{"ISBN: 978-0-306-40615-7"}, description, "") +} + +func writeTestEPUBWithDescriptionAndMeta(t *testing.T, identifiers []string, description string, extraMetaXML string) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "book.epub") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create epub: %v", err) + } + zw := zip.NewWriter(file) + add := func(name, body string) { + t.Helper() + w, err := zw.Create(name) + if err != nil { + t.Fatalf("create zip entry %s: %v", name, err) + } + if _, err := w.Write([]byte(body)); err != nil { + t.Fatalf("write zip entry %s: %v", name, err) + } + } + add("META-INF/container.xml", ` + + + + +`) + var identifierXML strings.Builder + for _, identifier := range identifiers { + identifierXML.WriteString(" ") + identifierXML.WriteString(identifier) + identifierXML.WriteString("\n") + } + add("OPS/content.opf", ` + + + The Test Ebook + Ada Writer + Ben Author +`+identifierXML.String()+` + Silo Press + 2024-03-10 + en + Fiction + Adventure + `+description+` + + +`+extraMetaXML+` + +`) + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close epub: %v", err) + } + return path +} + +func writeTestEPUBWithOPFBytes(t *testing.T, opf []byte) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "book.epub") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create epub: %v", err) + } + zw := zip.NewWriter(file) + add := func(name string, body []byte) { + t.Helper() + w, err := zw.Create(name) + if err != nil { + t.Fatalf("create zip entry %s: %v", name, err) + } + if _, err := w.Write(body); err != nil { + t.Fatalf("write zip entry %s: %v", name, err) + } + } + add("META-INF/container.xml", []byte(` + + + + +`)) + add("OPS/content.opf", opf) + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close epub: %v", err) + } + return path +} + +func writeTestEPUBWithCover(t *testing.T, coverHref string, coverType string, coverBytes []byte) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "book.epub") + file, err := os.Create(path) + if err != nil { + t.Fatalf("create epub: %v", err) + } + zw := zip.NewWriter(file) + add := func(name string, body []byte) { + t.Helper() + w, err := zw.Create(name) + if err != nil { + t.Fatalf("create zip entry %s: %v", name, err) + } + if _, err := w.Write(body); err != nil { + t.Fatalf("write zip entry %s: %v", name, err) + } + } + add("META-INF/container.xml", []byte(` + + + + +`)) + add("OPS/content.opf", []byte(` + + + The Test Ebook + + + + + +`)) + add("OPS/"+coverHref, coverBytes) + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("close epub: %v", err) + } + return path +} + +func TestUpdateExistingEbookMediaItemPreservesCuratedMatchedItem(t *testing.T) { + reader := &fakeFilesystemItemReader{items: []*models.MediaItem{{ + ContentID: "ebook-root-id", + Type: "ebook", + Title: "Curated Title", + Year: 2020, + Status: "matched", + }}} + writer := &fakeFilesystemItemWriter{} + + curated, err := updateExistingEbookMediaItem(context.Background(), reader, writer, "ebook-root-id", &parsedEbook{ + Title: "Embedded File Title", + Year: 2026, + }) + if err != nil { + t.Fatalf("updateExistingEbookMediaItem: %v", err) + } + if !curated { + t.Fatal("curated = false, want true for a matched item") + } + if len(writer.upserts) != 0 { + t.Fatalf("upserts = %d, want 0: matched items must not be overwritten by file metadata", len(writer.upserts)) + } +} + +func TestEbookItemHasCuratedMetadata(t *testing.T) { + cases := []struct { + item *models.MediaItem + want bool + }{ + {nil, false}, + {&models.MediaItem{Status: "matched"}, true}, + {&models.MediaItem{Status: " Matched "}, true}, + {&models.MediaItem{Status: "pending"}, false}, + {&models.MediaItem{Status: "unmatched"}, false}, + {&models.MediaItem{}, false}, + } + for _, tc := range cases { + if got := ebookItemHasCuratedMetadata(tc.item); got != tc.want { + t.Errorf("ebookItemHasCuratedMetadata(%+v) = %v, want %v", tc.item, got, tc.want) + } + } +} + +func TestParseEbookPDFReadsInfoDictionaryFromFileTail(t *testing.T) { + // Non-linearized PDFs put the Info dictionary at the end of the file and + // typically have no Info keys in the head at all; the parser must find the + // trailing dictionary past the head scan window and use it to fill every + // key the head did not produce. + head := "%PDF-1.7\n" + + "1 0 obj\n" + + "<< /Author (Ada Writer)\n" + + ">>\nendobj\n" + tail := "2 0 obj\n" + + "<< /Title (Tail Info Title)\n" + + " /CreationDate (D:20240102030405Z)\n" + + ">>\nendobj\n" + + "trailer\n<< /Info 2 0 R >>\n%%EOF" + padding := strings.Repeat("x", maxPDFMetadataScanSize) + + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte(head+padding+tail), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + if got.Title != "Tail Info Title" { + t.Fatalf("Title = %q, want tail Info dictionary value", got.Title) + } + if strings.Join(got.Authors, ", ") != "Ada Writer" { + t.Fatalf("Authors = %v, want head-only value preserved", got.Authors) + } + if got.Year != 2024 { + t.Fatalf("Year = %d, want 2024 from tail CreationDate", got.Year) + } +} + +func TestParseEbookPDFHeadInfoWinsOverTailStreamGarbage(t *testing.T) { + // Linearized PDFs carry the real Info dictionary in the head; a + // "/Title"-shaped byte pattern inside a trailing compressed stream must + // not replace it. + head := "%PDF-1.7\n" + + "1 0 obj\n" + + "<< /Linearized 1 >>\nendobj\n" + + "2 0 obj\n" + + "<< /Title (Real Head Title)\n" + + " /Author (Ada Writer)\n" + + ">>\nendobj\n" + tail := "3 0 obj\n" + + "<< /Length 64 >>\nstream\n" + + "\x01\x02/Title (\xfe\xba\xad binary stream garbage)\x03\x04\n" + + "endstream\nendobj\n" + + "trailer\n<< /Info 2 0 R >>\n%%EOF" + padding := strings.Repeat("x", maxPDFMetadataScanSize) + + path := filepath.Join(t.TempDir(), "book.pdf") + if err := os.WriteFile(path, []byte(head+padding+tail), 0o644); err != nil { + t.Fatalf("write pdf: %v", err) + } + + got, err := parseEbookFile(path) + if err != nil { + t.Fatalf("parseEbookFile: %v", err) + } + if got.Title != "Real Head Title" { + t.Fatalf("Title = %q, want authoritative head value over tail stream garbage", got.Title) + } +} + +func TestFindPDFInfoValueRequiresProperKeyDelimiter(t *testing.T) { + // "/TitleSort" must not satisfy a "/Title" lookup, and the real key later + // in the window must still be found. + data := []byte(`<< /TitleSort (Adventures of, The) /Title (The Adventures) >>`) + value, ok := findPDFInfoValue(data, "Title") + if !ok || value != "The Adventures" { + t.Fatalf("findPDFInfoValue = %q, %v; want the properly delimited /Title", value, ok) + } +} + +func TestFindPDFInfoValueRetriesPastUnparseableOccurrences(t *testing.T) { + // A key match inside binary stream data whose "value" is not a PDF string + // must not end the search; the real key/value pair later in the window + // still wins. + data := []byte("stream\x00/Title \x07\x08garbage\x00endstream\n" + + "<< /Title (Recovered Title) >>") + value, ok := findPDFInfoValue(data, "Title") + if !ok || value != "Recovered Title" { + t.Fatalf("findPDFInfoValue = %q, %v; want recovery past unparseable match", value, ok) + } + + if _, ok := findPDFInfoValue([]byte("/Titled (Nope)"), "Title"); ok { + t.Fatal("findPDFInfoValue matched a longer key sharing the prefix") + } +} + +func TestParseEbookFB2RejectsOversizedFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "book.fb2") + if err := os.WriteFile(path, make([]byte, maxEPUBMetadataEntrySize+1), 0o644); err != nil { + t.Fatalf("write fb2: %v", err) + } + + _, err := parseEbookFile(path) + if err == nil || !strings.Contains(err.Error(), "too large") { + t.Fatalf("parseEbookFile error = %v, want size-cap rejection", err) + } +} diff --git a/internal/scanner/root_observation.go b/internal/scanner/root_observation.go index 5986ab22..da848a7f 100644 --- a/internal/scanner/root_observation.go +++ b/internal/scanner/root_observation.go @@ -86,7 +86,7 @@ func observationFromAssignment(assignment fileRootAssignment) RootObservation { } func observationFromSnapshot(snapshot models.ScannedMediaRoot) RootObservation { - hasFolderIDs := naming.ParseFolderIDs(filepath.Base(snapshot.RootPath), snapshot.InferredType) != nil + hasFolderIDs := naming.ParseFolderIDs(filepath.Base(snapshot.RootPath)) != nil observation := RootObservation{ RootPath: snapshot.RootPath, SampleFilePath: snapshot.SampleFilePath, diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 927708d3..a084268c 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -97,9 +97,12 @@ func shouldSkipMovieSupplementalFile(path string) bool { } // Scanner discovers and indexes media files in media folders. -// scannerImageCacher is the slice of imagecache.Cacher the audiobook -// branch uses. Wired via SetImageCacher; nil-safe when absent. -type scannerImageCacher = audiobookCoverCacher +// scannerImageCacher is the slice of imagecache.Cacher the book scanners use. +// Wired via SetImageCacher; nil-safe when absent. +type scannerImageCacher interface { + audiobookCoverCacher + ebookCoverCacher +} type Scanner struct { fileRepo *FileRepository @@ -126,9 +129,9 @@ type Scanner struct { seriesQueueSyncer SeriesQueueSyncer } -// SetImageCacher installs the imagecache.Cacher used by the audiobook -// branch to push embedded M4B cover art into the public assets bucket. -// Optional; if unset, audiobook covers are not extracted. +// SetImageCacher installs the imagecache.Cacher used by book scanners to push +// embedded cover art into the public assets bucket. Optional; if unset, local +// covers are not extracted. func (s *Scanner) SetImageCacher(cacher scannerImageCacher) { if s == nil { return @@ -241,6 +244,13 @@ func (s *Scanner) ScanFolder(ctx context.Context, folder *models.MediaFolder) (* return &ScanResult{}, nil } + if isEbookLibraryType(folder.Type) { + if err := s.ScanEbookFolder(watchCtx, folder); err != nil { + return nil, err + } + return &ScanResult{}, nil + } + return s.scanPaths(watchCtx, folder, folder.Paths, folder.Paths, true) } @@ -250,9 +260,46 @@ func (s *Scanner) ScanSubtree(ctx context.Context, folder *models.MediaFolder, s cleanSubtree := filepath.Clean(subtreePath) watchCtx, stopWatch := s.watchFolderContext(ctx, folder.ID) defer stopWatch() + if isAudiobookLibraryType(folder.Type) { + scanRoot, err := cleanScopedAudiobookScanRoot(subtreePath) + if err != nil { + return nil, err + } + if err := s.ScanAudiobookFolder(watchCtx, scopedFolderPaths(folder, []string{scanRoot})); err != nil { + return nil, err + } + if err := s.syncFolderScopedAudioLibraryState(watchCtx, folder.ID); err != nil { + return nil, err + } + return &ScanResult{}, nil + } + if isEbookLibraryType(folder.Type) { + if err := s.scanEbookPaths(watchCtx, folder, []string{cleanSubtree}, false); err != nil { + return nil, err + } + return &ScanResult{}, nil + } return s.scanPaths(watchCtx, folder, []string{cleanSubtree}, []string{cleanSubtree}, false) } +func cleanScopedAudiobookScanRoot(path string) (string, error) { + clean := filepath.Clean(path) + if clean == "" || clean == "." || clean == ".." || clean == string(filepath.Separator) || + strings.HasPrefix(clean, ".."+string(filepath.Separator)) || !filepath.IsAbs(clean) { + return "", fmt.Errorf("invalid audiobook scan root: %s", path) + } + return clean, nil +} + +func scopedFolderPaths(folder *models.MediaFolder, paths []string) *models.MediaFolder { + if folder == nil { + return nil + } + clone := *folder + clone.Paths = paths + return &clone +} + func isMovieLibraryType(libraryType string) bool { switch strings.ToLower(strings.TrimSpace(libraryType)) { case "movie", "movies": @@ -278,6 +325,15 @@ func isPodcastLibraryType(libraryType string) bool { } } +func isEbookLibraryType(libraryType string) bool { + switch strings.ToLower(strings.TrimSpace(libraryType)) { + case "ebook", "ebooks": + return true + default: + return false + } +} + // walkMode tells walkLogicalTree which file extensions to surface and // which library-specific filename heuristics (sample/extra skipping) // to apply. @@ -288,6 +344,7 @@ const ( walkModeMovie // movie library: video extensions + sample/extra skipping walkModeAudiobook // audiobook library: audio extensions, no skipping walkModePodcast // podcast library: audio extensions, no skipping + walkModeEbook // ebook library: ebook extensions, no skipping ) // walkModeFor derives a walkMode from a media_folders.type string. @@ -301,6 +358,8 @@ func walkModeFor(folderType string) walkMode { return walkModeAudiobook case isPodcastLibraryType(folderType): return walkModePodcast + case isEbookLibraryType(folderType): + return walkModeEbook default: return walkModeVideo } @@ -312,6 +371,8 @@ func (m walkMode) acceptsExt(ext string) bool { switch m { case walkModeAudiobook, walkModePodcast: return audioExtensions[ext] + case walkModeEbook: + return ebookExtensions[ext] default: return videoExtensions[ext] } @@ -333,6 +394,16 @@ func isIgnoredDirectoryPath(path string) bool { return ignoredDirNames[strings.ToLower(filepath.Base(path))] } +// recordWalkFailure counts an entry the walk could not read or resolve. +// Callers that pass a non-nil counter (the book scanners) use it to exclude +// incompletely walked roots from missing-file reconciliation; the video walk +// passes nil and keeps its historical log-and-continue behavior. +func recordWalkFailure(failures *int) { + if failures != nil { + *failures++ + } +} + func walkLogicalTree( ctx context.Context, logicalPath string, @@ -340,6 +411,7 @@ func walkLogicalTree( mode walkMode, visitedPhysicalDirs map[string]struct{}, filePaths *[]string, + walkFailures *int, ) error { if ctx != nil { if err := ctx.Err(); err != nil { @@ -350,6 +422,7 @@ func walkLogicalTree( info, err := os.Lstat(physicalPath) if err != nil { slog.Warn("scanner: walk lstat failed", "path", logicalPath, "physical_path", physicalPath, "error", err) + recordWalkFailure(walkFailures) return nil } @@ -357,15 +430,17 @@ func walkLogicalTree( resolved, err := filepath.EvalSymlinks(physicalPath) if err != nil { slog.Warn("scanner: symlink resolve failed", "path", logicalPath, "physical_path", physicalPath, "error", err) + recordWalkFailure(walkFailures) return nil } targetInfo, err := os.Stat(resolved) if err != nil { slog.Warn("scanner: symlink stat failed", "path", logicalPath, "resolved_path", resolved, "error", err) + recordWalkFailure(walkFailures) return nil } if targetInfo.IsDir() { - return walkLogicalTree(ctx, logicalPath, resolved, mode, visitedPhysicalDirs, filePaths) + return walkLogicalTree(ctx, logicalPath, resolved, mode, visitedPhysicalDirs, filePaths, walkFailures) } if mode == walkModeMovie && shouldSkipMovieSupplementalFile(logicalPath) { return nil @@ -389,6 +464,7 @@ func walkLogicalTree( canonicalDir, err := canonicalWalkPath(physicalPath) if err != nil { slog.Warn("scanner: canonical path resolution failed", "path", logicalPath, "physical_path", physicalPath, "error", err) + recordWalkFailure(walkFailures) return nil } if _, seen := visitedPhysicalDirs[canonicalDir]; seen { @@ -406,6 +482,7 @@ func walkLogicalTree( entries, err := os.ReadDir(physicalPath) if err != nil { slog.Warn("scanner: directory read failed", "path", logicalPath, "physical_path", physicalPath, "error", err) + recordWalkFailure(walkFailures) return nil } for _, entry := range entries { @@ -422,15 +499,17 @@ func walkLogicalTree( resolved, err := filepath.EvalSymlinks(physicalChild) if err != nil { slog.Warn("scanner: symlink resolve failed", "path", logicalChild, "physical_path", physicalChild, "error", err) + recordWalkFailure(walkFailures) continue } targetInfo, err := os.Stat(resolved) if err != nil { slog.Warn("scanner: symlink stat failed", "path", logicalChild, "resolved_path", resolved, "error", err) + recordWalkFailure(walkFailures) continue } if targetInfo.IsDir() { - if err := walkLogicalTree(ctx, logicalChild, resolved, mode, visitedPhysicalDirs, filePaths); err != nil { + if err := walkLogicalTree(ctx, logicalChild, resolved, mode, visitedPhysicalDirs, filePaths, walkFailures); err != nil { return err } continue @@ -445,7 +524,7 @@ func walkLogicalTree( } if entry.IsDir() { - if err := walkLogicalTree(ctx, logicalChild, physicalChild, mode, visitedPhysicalDirs, filePaths); err != nil { + if err := walkLogicalTree(ctx, logicalChild, physicalChild, mode, visitedPhysicalDirs, filePaths, walkFailures); err != nil { return err } continue @@ -477,7 +556,7 @@ func collectLogicalFilePaths(ctx context.Context, walkRoots []string, libraryTyp if cleanRoot == "" || cleanRoot == "." { continue } - if err := walkLogicalTree(ctx, cleanRoot, cleanRoot, mode, visitedPhysicalDirs, &filePaths); err != nil { + if err := walkLogicalTree(ctx, cleanRoot, cleanRoot, mode, visitedPhysicalDirs, &filePaths, nil); err != nil { return nil, err } } @@ -1454,8 +1533,23 @@ func (s *Scanner) ScanFile(ctx context.Context, filePath string, folder *models. return err } + cleanFile := filepath.Clean(filePath) + if isAudiobookLibraryType(folder.Type) { + if !SupportsAudioFile(cleanFile) { + return fmt.Errorf("unrecognized audio extension: %s", strings.ToLower(filepath.Ext(cleanFile))) + } + scanRoot, err := cleanScopedAudiobookScanRoot(filepath.Dir(cleanFile)) + if err != nil { + return err + } + if err := s.ScanAudiobookFolder(ctx, scopedFolderPaths(folder, []string{scanRoot})); err != nil { + return err + } + return s.syncFolderScopedAudioLibraryState(ctx, folder.ID) + } + // Verify the file extension is recognized. - ext := strings.ToLower(filepath.Ext(filePath)) + ext := strings.ToLower(filepath.Ext(cleanFile)) if !videoExtensions[ext] { return fmt.Errorf("unrecognized video extension: %s", ext) } diff --git a/internal/scanner/scanner_test.go b/internal/scanner/scanner_test.go index edde4348..d548c264 100644 --- a/internal/scanner/scanner_test.go +++ b/internal/scanner/scanner_test.go @@ -200,3 +200,76 @@ func TestIsPodcastLibraryType(t *testing.T) { } } } + +func TestIsEbookLibraryType(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"ebooks", true}, + {"ebook", true}, + {"Ebook", true}, + {" EBOOKS ", true}, + {"audiobooks", false}, + {"movies", false}, + {"", false}, + } + for _, tc := range cases { + if got := isEbookLibraryType(tc.in); got != tc.want { + t.Errorf("isEbookLibraryType(%q) = %v, want %v", tc.in, got, tc.want) + } + } +} + +func TestWalkModeForEbookLibraryTypes(t *testing.T) { + for _, libraryType := range []string{"ebook", "ebooks", " EBOOKS "} { + if got := walkModeFor(libraryType); got != walkModeEbook { + t.Fatalf("walkModeFor(%q) = %v, want %v", libraryType, got, walkModeEbook) + } + } +} + +func TestWalkModeEbookAcceptsEbookExtensionsOnly(t *testing.T) { + for _, ext := range []string{".epub", ".pdf", ".mobi", ".azw", ".azw3", ".fb2", ".fbz", ".cbz", ".cbr"} { + if !walkModeEbook.acceptsExt(ext) { + t.Fatalf("walkModeEbook should accept %s", ext) + } + } + for _, ext := range []string{".txt", ".md", ".mp4", ".mp3", ".mkv"} { + if walkModeEbook.acceptsExt(ext) { + t.Fatalf("walkModeEbook should reject %s", ext) + } + } +} + +func TestScanFolderEbookLibraryRoutesToEbookScanner(t *testing.T) { + scanner := &Scanner{} + result, err := scanner.ScanFolder(context.Background(), &models.MediaFolder{ + ID: 0, + Type: " ebooks ", + Paths: []string{t.TempDir()}, + }) + if err != nil { + t.Fatalf("ScanFolder ebook error = %v, want nil", err) + } + if result == nil { + t.Fatal("ScanFolder ebook result = nil, want empty result") + } +} + +func TestScanSubtreeEbookLibraryRoutesToEbookScanner(t *testing.T) { + subtree := t.TempDir() + + scanner := &Scanner{} + result, err := scanner.ScanSubtree(context.Background(), &models.MediaFolder{ + ID: 0, + Type: "ebook", + Paths: []string{subtree}, + }, subtree) + if err != nil { + t.Fatalf("ScanSubtree ebook error = %v, want nil", err) + } + if result == nil { + t.Fatal("ScanSubtree ebook result = nil, want empty result") + } +} diff --git a/internal/sections/continue_type.go b/internal/sections/continue_type.go new file mode 100644 index 00000000..815a1859 --- /dev/null +++ b/internal/sections/continue_type.go @@ -0,0 +1,105 @@ +package sections + +import ( + "encoding/json" + "fmt" + "strings" +) + +type ContinueType string + +const ( + ContinueTypeWatching ContinueType = "watching" + ContinueTypeListening ContinueType = "listening" + ContinueTypeReading ContinueType = "reading" +) + +type continueTypeConfig struct { + ContinueType string `json:"continue_type"` + FilterType string `json:"filter_type"` + MediaScope string `json:"media_scope"` +} + +func ContinueTypeConfig(typ ContinueType) json.RawMessage { + if !IsValidContinueType(typ) { + typ = ContinueTypeWatching + } + raw, err := json.Marshal(map[string]string{"continue_type": string(typ)}) + if err != nil { + return json.RawMessage(`{"continue_type":"watching"}`) + } + return raw +} + +func ContinueTypeFromConfig(config json.RawMessage) ContinueType { + typ, _ := ParseContinueType(config) + return typ +} + +func ParseContinueType(config json.RawMessage) (ContinueType, error) { + var raw continueTypeConfig + if len(config) > 0 { + if err := json.Unmarshal(config, &raw); err != nil { + return ContinueTypeWatching, err + } + } + + if strings.TrimSpace(raw.ContinueType) != "" { + typ := ContinueType(strings.ToLower(strings.TrimSpace(raw.ContinueType))) + if !IsValidContinueType(typ) { + return ContinueTypeWatching, fmt.Errorf("continue_type must be 'watching', 'listening', or 'reading'") + } + return typ, nil + } + + switch strings.ToLower(strings.TrimSpace(raw.FilterType)) { + case "audiobook": + return ContinueTypeListening, nil + case "ebook": + return ContinueTypeReading, nil + } + + switch strings.ToLower(strings.TrimSpace(raw.MediaScope)) { + case "audiobook": + return ContinueTypeListening, nil + case "ebook": + return ContinueTypeReading, nil + } + + return ContinueTypeWatching, nil +} + +func IsValidContinueType(typ ContinueType) bool { + switch typ { + case ContinueTypeWatching, ContinueTypeListening, ContinueTypeReading: + return true + default: + return false + } +} + +func ContinueTypeMatchesItem(typ ContinueType, itemType string) bool { + switch typ { + case ContinueTypeWatching: + return itemType == "movie" || itemType == "episode" + case ContinueTypeListening: + return itemType == "audiobook" + case ContinueTypeReading: + return itemType == "ebook" + default: + return false + } +} + +func ContinueTypeAllowsNextUp(typ ContinueType) bool { + return typ == ContinueTypeWatching +} + +func IsAudiobookLibraryType(libraryType string) bool { + switch strings.ToLower(strings.TrimSpace(libraryType)) { + case "audiobook", "audiobooks": + return true + default: + return false + } +} diff --git a/internal/sections/defaults.go b/internal/sections/defaults.go index 1ba0ca58..409546bc 100644 --- a/internal/sections/defaults.go +++ b/internal/sections/defaults.go @@ -11,12 +11,23 @@ import ( // DefaultHomeSections returns the canonical default sections for the home scope. // These are used for seeding fresh installs and the admin "Restore Defaults" action. func DefaultHomeSections(libraries []*models.MediaFolder) []*PageSection { - emptyCfg := json.RawMessage(`{}`) result := []*PageSection{ - {ID: "default-continue-watching", Scope: "home", Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: emptyCfg, Enabled: true}, + {ID: "default-continue-watching", Scope: "home", Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: ContinueTypeConfig(ContinueTypeWatching), Enabled: true}, + } + if hasAudiobookLibrary(libraries) { + result = append(result, &PageSection{ + ID: "default-continue-listening", + Scope: "home", + Position: 1, + SectionType: SectionContinueWatching, + Title: "Continue Listening", + ItemLimit: 20, + Config: ContinueTypeConfig(ContinueTypeListening), + Enabled: true, + }) } - position := 1 + position := len(result) for _, library := range libraries { if library == nil { continue @@ -63,6 +74,15 @@ func DefaultHomeSections(libraries []*models.MediaFolder) []*PageSection { return result } +func hasAudiobookLibrary(libraries []*models.MediaFolder) bool { + for _, library := range libraries { + if library != nil && IsAudiobookLibraryType(library.Type) { + return true + } + } + return false +} + func generatedHomeLibraryRecentID(section *PageSection, libraryID int) string { kind := generatedHomeLibraryRecentKindForSection(section) if kind == generatedHomeLibraryRecentKindReleasedEpisodes { @@ -147,11 +167,12 @@ func defaultRecentEpisodesConfig() json.RawMessage { // library defaults. func DefaultLibrarySectionsForType(libraryID *int, libraryType string) []*PageSection { emptyCfg := json.RawMessage(`{}`) + continueWatchingCfg := ContinueTypeConfig(ContinueTypeWatching) switch libraryType { case "movies", "movie": return []*PageSection{ - {ID: "default-continue-watching", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: emptyCfg, Enabled: true}, + {ID: "default-continue-watching", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: continueWatchingCfg, Enabled: true}, {ID: "default-recently-added-movies", Scope: "library", LibraryID: libraryID, Position: 1, SectionType: SectionRecentlyAdded, Title: "Recently Added Movies", ItemLimit: 20, Config: defaultMediaScopeConfig("movie"), Enabled: true}, {ID: "default-recently-released-movies", Scope: "library", LibraryID: libraryID, Position: 2, SectionType: SectionRecentlyReleased, Title: "Recently Released Movies", ItemLimit: 20, Config: defaultMediaScopeConfig("movie"), Enabled: true}, {ID: "default-top-rated-movies", Scope: "library", LibraryID: libraryID, Position: 3, SectionType: SectionCustomFilter, Title: "Top Rated Movies", ItemLimit: 20, Config: defaultTopRatedConfig("movie"), Enabled: true}, @@ -160,7 +181,7 @@ func DefaultLibrarySectionsForType(libraryID *int, libraryType string) []*PageSe } case "series": return []*PageSection{ - {ID: "default-continue-watching", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: emptyCfg, Enabled: true}, + {ID: "default-continue-watching", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: continueWatchingCfg, Enabled: true}, {ID: "default-recently-added-tv", Scope: "library", LibraryID: libraryID, Position: 1, SectionType: SectionRecentlyAdded, Title: "Recently Added TV", ItemLimit: 20, Config: defaultMediaScopeConfig("series"), Enabled: true}, {ID: "default-recently-released-episodes", Scope: "library", LibraryID: libraryID, Position: 2, SectionType: SectionCustomFilter, Title: "Recently Released Episodes", ItemLimit: 20, Config: defaultRecentEpisodesConfig(), Enabled: true}, {ID: "default-top-rated-tv", Scope: "library", LibraryID: libraryID, Position: 3, SectionType: SectionCustomFilter, Title: "Top Rated TV", ItemLimit: 20, Config: defaultTopRatedConfig("series"), Enabled: true}, @@ -168,12 +189,23 @@ func DefaultLibrarySectionsForType(libraryID *int, libraryType string) []*PageSe {ID: "default-random-tv", Scope: "library", LibraryID: libraryID, Position: 5, SectionType: SectionRandom, Title: "Random Picks", ItemLimit: 20, Config: defaultMediaScopeConfig("series"), Enabled: true}, } case "audiobooks", "audiobook": + // Continue Listening is featured: the library Home tab renders it as + // the "Now Listening" resume hero instead of a backdrop carousel. return []*PageSection{ - {ID: "default-continue-listening", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Listening", ItemLimit: 20, Config: emptyCfg, Enabled: true}, - {ID: "default-recently-added-audiobooks", Scope: "library", LibraryID: libraryID, Position: 1, SectionType: SectionRecentlyAdded, Title: "Recently Added Audiobooks", ItemLimit: 20, Config: defaultMediaScopeConfig("audiobook"), Enabled: true}, - {ID: "default-recently-released-audiobooks", Scope: "library", LibraryID: libraryID, Position: 2, SectionType: SectionRecentlyReleased, Title: "Recently Released Audiobooks", ItemLimit: 20, Config: defaultMediaScopeConfig("audiobook"), Enabled: true}, + {ID: "default-continue-listening", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Listening", Featured: true, ItemLimit: 20, Config: ContinueTypeConfig(ContinueTypeListening), Enabled: true}, + {ID: "default-next-in-series", Scope: "library", LibraryID: libraryID, Position: 1, SectionType: SectionNextInSeries, Title: "Next in Your Series", ItemLimit: 20, Config: emptyCfg, Enabled: true}, + {ID: "default-recently-added-audiobooks", Scope: "library", LibraryID: libraryID, Position: 2, SectionType: SectionRecentlyAdded, Title: "Recently Added Audiobooks", ItemLimit: 20, Config: defaultMediaScopeConfig("audiobook"), Enabled: true}, + {ID: "default-recently-released-audiobooks", Scope: "library", LibraryID: libraryID, Position: 3, SectionType: SectionRecentlyReleased, Title: "Recently Released Audiobooks", ItemLimit: 20, Config: defaultMediaScopeConfig("audiobook"), Enabled: true}, + {ID: "default-recommended-for-you", Scope: "library", LibraryID: libraryID, Position: 4, SectionType: SectionRecommendedForYou, Title: "Recommended for You", ItemLimit: 20, Config: emptyCfg, Enabled: true}, + {ID: "default-random-audiobooks", Scope: "library", LibraryID: libraryID, Position: 5, SectionType: SectionRandom, Title: "Random Picks", ItemLimit: 20, Config: defaultMediaScopeConfig("audiobook"), Enabled: true}, + } + case "ebooks", "ebook": + return []*PageSection{ + {ID: "default-continue-reading", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Reading", ItemLimit: 20, Config: ContinueTypeConfig(ContinueTypeReading), Enabled: true}, + {ID: "default-recently-added-ebooks", Scope: "library", LibraryID: libraryID, Position: 1, SectionType: SectionRecentlyAdded, Title: "Recently Added Ebooks", ItemLimit: 20, Config: defaultMediaScopeConfig("ebook"), Enabled: true}, + {ID: "default-recently-released-ebooks", Scope: "library", LibraryID: libraryID, Position: 2, SectionType: SectionRecentlyReleased, Title: "Recently Released Ebooks", ItemLimit: 20, Config: defaultMediaScopeConfig("ebook"), Enabled: true}, {ID: "default-recommended-for-you", Scope: "library", LibraryID: libraryID, Position: 3, SectionType: SectionRecommendedForYou, Title: "Recommended for You", ItemLimit: 20, Config: emptyCfg, Enabled: true}, - {ID: "default-random-audiobooks", Scope: "library", LibraryID: libraryID, Position: 4, SectionType: SectionRandom, Title: "Random Picks", ItemLimit: 20, Config: defaultMediaScopeConfig("audiobook"), Enabled: true}, + {ID: "default-random-ebooks", Scope: "library", LibraryID: libraryID, Position: 4, SectionType: SectionRandom, Title: "Random Picks", ItemLimit: 20, Config: defaultMediaScopeConfig("ebook"), Enabled: true}, } default: return DefaultLibrarySections(libraryID) @@ -186,7 +218,7 @@ func DefaultLibrarySectionsForType(libraryID *int, libraryType string) []*PageSe func DefaultLibrarySections(libraryID *int) []*PageSection { emptyCfg := json.RawMessage(`{}`) return []*PageSection{ - {ID: "default-continue-watching", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: emptyCfg, Enabled: true}, + {ID: "default-continue-watching", Scope: "library", LibraryID: libraryID, Position: 0, SectionType: SectionContinueWatching, Title: "Continue Watching", ItemLimit: 20, Config: ContinueTypeConfig(ContinueTypeWatching), Enabled: true}, {ID: "default-recently-added", Scope: "library", LibraryID: libraryID, Position: 1, SectionType: SectionRecentlyAdded, Title: "Recently Added", ItemLimit: 20, Config: emptyCfg, Enabled: true}, {ID: "default-recently-released", Scope: "library", LibraryID: libraryID, Position: 2, SectionType: SectionRecentlyReleased, Title: "Recently Released", ItemLimit: 20, Config: emptyCfg, Enabled: true}, } diff --git a/internal/sections/defaults_test.go b/internal/sections/defaults_test.go index 4d331036..7b1b898e 100644 --- a/internal/sections/defaults_test.go +++ b/internal/sections/defaults_test.go @@ -17,6 +17,7 @@ func TestDefaultHomeSectionsWithoutLibraries(t *testing.T) { if sections[0].SectionType != SectionContinueWatching { t.Fatalf("expected continue watching, got %s", sections[0].SectionType) } + assertContinueType(t, sections[0].Config, ContinueTypeWatching) } func TestDefaultHomeSectionsWithLibraries(t *testing.T) { @@ -61,6 +62,7 @@ func TestDefaultHomeSectionsWithLibraries(t *testing.T) { t.Fatalf("section %d position = %d, want %d", tt.index, section.Position, tt.position) } if tt.libraryID == 0 { + assertContinueType(t, section.Config, ContinueTypeWatching) continue } libraryID, ok := ParseGeneratedHomeLibraryRecentConfig(section.Config) @@ -81,6 +83,46 @@ func TestDefaultHomeSectionsWithLibraries(t *testing.T) { }) } +func TestDefaultHomeSectionsWithAudiobookLibrary(t *testing.T) { + libraries := []*models.MediaFolder{ + {ID: 7, Name: "Movies", Type: "movies", SortOrder: 1}, + {ID: 10, Name: "Books", Type: "audiobooks", SortOrder: 2}, + } + + got := DefaultHomeSections(libraries) + if len(got) != 9 { + t.Fatalf("expected 9 default home sections, got %d", len(got)) + } + + tests := []struct { + index int + id string + title string + position int + continueType ContinueType + }{ + {index: 0, id: "default-continue-watching", title: "Continue Watching", position: 0, continueType: ContinueTypeWatching}, + {index: 1, id: "default-continue-listening", title: "Continue Listening", position: 1, continueType: ContinueTypeListening}, + } + for _, tt := range tests { + section := got[tt.index] + if section.ID != tt.id { + t.Fatalf("section %d id = %q, want %q", tt.index, section.ID, tt.id) + } + if section.Title != tt.title { + t.Fatalf("section %d title = %q, want %q", tt.index, section.Title, tt.title) + } + if section.Position != tt.position { + t.Fatalf("section %d position = %d, want %d", tt.index, section.Position, tt.position) + } + assertContinueType(t, section.Config, tt.continueType) + } + + if got[2].Position != 2 || got[2].Title != "Recently Added in Movies" { + t.Fatalf("first generated library row = position %d title %q", got[2].Position, got[2].Title) + } +} + func TestDefaultLibrarySectionsForTypeMovies(t *testing.T) { libraryID := 42 got := DefaultLibrarySectionsForType(&libraryID, "movies") @@ -122,6 +164,7 @@ func TestDefaultLibrarySectionsForTypeMovies(t *testing.T) { t.Fatalf("section %d featured = true, want false", tt.index) } } + assertContinueType(t, got[0].Config, ContinueTypeWatching) assertQueryDefinition(t, got[1].Config, catalog.QueryDefinition{ MediaScope: "movie", @@ -185,6 +228,7 @@ func TestDefaultLibrarySectionsForTypeSeries(t *testing.T) { t.Fatalf("section %d featured = true, want false", tt.index) } } + assertContinueType(t, got[0].Config, ContinueTypeWatching) assertQueryDefinition(t, got[1].Config, catalog.QueryDefinition{ MediaScope: "series", @@ -217,8 +261,74 @@ func TestDefaultLibrarySectionsForTypeAudiobooks(t *testing.T) { libraryID := 10 got := DefaultLibrarySectionsForType(&libraryID, "audiobooks") + if len(got) != 6 { + t.Fatalf("expected 6 audiobook default sections, got %d", len(got)) + } + + tests := []struct { + index int + id string + sectionType SectionType + title string + position int + featured bool + }{ + {index: 0, id: "default-continue-listening", sectionType: SectionContinueWatching, title: "Continue Listening", position: 0, featured: true}, + {index: 1, id: "default-next-in-series", sectionType: SectionNextInSeries, title: "Next in Your Series", position: 1}, + {index: 2, id: "default-recently-added-audiobooks", sectionType: SectionRecentlyAdded, title: "Recently Added Audiobooks", position: 2}, + {index: 3, id: "default-recently-released-audiobooks", sectionType: SectionRecentlyReleased, title: "Recently Released Audiobooks", position: 3}, + {index: 4, id: "default-recommended-for-you", sectionType: SectionRecommendedForYou, title: "Recommended for You", position: 4}, + {index: 5, id: "default-random-audiobooks", sectionType: SectionRandom, title: "Random Picks", position: 5}, + } + + for _, tt := range tests { + section := got[tt.index] + if section.ID != tt.id { + t.Fatalf("section %d id = %q, want %q", tt.index, section.ID, tt.id) + } + if section.SectionType != tt.sectionType { + t.Fatalf("section %d type = %q, want %q", tt.index, section.SectionType, tt.sectionType) + } + if section.Title != tt.title { + t.Fatalf("section %d title = %q, want %q", tt.index, section.Title, tt.title) + } + if section.Position != tt.position { + t.Fatalf("section %d position = %d, want %d", tt.index, section.Position, tt.position) + } + if section.Featured != tt.featured { + t.Fatalf("section %d featured = %v, want %v", tt.index, section.Featured, tt.featured) + } + } + assertContinueType(t, got[0].Config, ContinueTypeListening) + + assertEmptyJSON(t, got[1].Config) + assertQueryDefinition(t, got[2].Config, catalog.QueryDefinition{ + MediaScope: "audiobook", + Match: "all", + Groups: []catalog.QueryGroup{}, + Sort: catalog.QuerySort{Field: "added_at", Order: "desc"}, + }) + assertQueryDefinition(t, got[3].Config, catalog.QueryDefinition{ + MediaScope: "audiobook", + Match: "all", + Groups: []catalog.QueryGroup{}, + Sort: catalog.QuerySort{Field: "added_at", Order: "desc"}, + }) + assertEmptyJSON(t, got[4].Config) + assertQueryDefinition(t, got[5].Config, catalog.QueryDefinition{ + MediaScope: "audiobook", + Match: "all", + Groups: []catalog.QueryGroup{}, + Sort: catalog.QuerySort{Field: "added_at", Order: "desc"}, + }) +} + +func TestDefaultLibrarySectionsForTypeEbooks(t *testing.T) { + libraryID := 11 + got := DefaultLibrarySectionsForType(&libraryID, "ebooks") + if len(got) != 5 { - t.Fatalf("expected 5 audiobook default sections, got %d", len(got)) + t.Fatalf("expected 5 ebook default sections, got %d", len(got)) } tests := []struct { @@ -228,11 +338,11 @@ func TestDefaultLibrarySectionsForTypeAudiobooks(t *testing.T) { title string position int }{ - {index: 0, id: "default-continue-listening", sectionType: SectionContinueWatching, title: "Continue Listening", position: 0}, - {index: 1, id: "default-recently-added-audiobooks", sectionType: SectionRecentlyAdded, title: "Recently Added Audiobooks", position: 1}, - {index: 2, id: "default-recently-released-audiobooks", sectionType: SectionRecentlyReleased, title: "Recently Released Audiobooks", position: 2}, + {index: 0, id: "default-continue-reading", sectionType: SectionContinueWatching, title: "Continue Reading", position: 0}, + {index: 1, id: "default-recently-added-ebooks", sectionType: SectionRecentlyAdded, title: "Recently Added Ebooks", position: 1}, + {index: 2, id: "default-recently-released-ebooks", sectionType: SectionRecentlyReleased, title: "Recently Released Ebooks", position: 2}, {index: 3, id: "default-recommended-for-you", sectionType: SectionRecommendedForYou, title: "Recommended for You", position: 3}, - {index: 4, id: "default-random-audiobooks", sectionType: SectionRandom, title: "Random Picks", position: 4}, + {index: 4, id: "default-random-ebooks", sectionType: SectionRandom, title: "Random Picks", position: 4}, } for _, tt := range tests { @@ -254,21 +364,22 @@ func TestDefaultLibrarySectionsForTypeAudiobooks(t *testing.T) { } } + assertContinueType(t, got[0].Config, ContinueTypeReading) assertQueryDefinition(t, got[1].Config, catalog.QueryDefinition{ - MediaScope: "audiobook", + MediaScope: "ebook", Match: "all", Groups: []catalog.QueryGroup{}, Sort: catalog.QuerySort{Field: "added_at", Order: "desc"}, }) assertQueryDefinition(t, got[2].Config, catalog.QueryDefinition{ - MediaScope: "audiobook", + MediaScope: "ebook", Match: "all", Groups: []catalog.QueryGroup{}, Sort: catalog.QuerySort{Field: "added_at", Order: "desc"}, }) assertEmptyJSON(t, got[3].Config) assertQueryDefinition(t, got[4].Config, catalog.QueryDefinition{ - MediaScope: "audiobook", + MediaScope: "ebook", Match: "all", Groups: []catalog.QueryGroup{}, Sort: catalog.QuerySort{Field: "added_at", Order: "desc"}, @@ -310,6 +421,7 @@ func TestDefaultLibrarySectionsForTypeMixed(t *testing.T) { t.Fatalf("section %d position = %d, want %d", tt.index, section.Position, tt.position) } } + assertContinueType(t, got[0].Config, ContinueTypeWatching) } func assertQueryDefinition(t *testing.T, raw json.RawMessage, want catalog.QueryDefinition) { @@ -357,6 +469,17 @@ func assertEmptyJSON(t *testing.T, raw json.RawMessage) { } } +func assertContinueType(t *testing.T, raw json.RawMessage, want ContinueType) { + t.Helper() + got, err := ParseContinueType(raw) + if err != nil { + t.Fatalf("ParseContinueType(%s): %v", string(raw), err) + } + if got != want { + t.Fatalf("continue_type = %q, want %q (config %s)", got, want, string(raw)) + } +} + func TestHomeDefaultsIncludeRecipeRichSet(t *testing.T) { defs := DefaultHomeSections(nil) want := []SectionType{ diff --git a/internal/sections/fetcher.go b/internal/sections/fetcher.go index 279f809f..c28ee8dc 100644 --- a/internal/sections/fetcher.go +++ b/internal/sections/fetcher.go @@ -68,11 +68,13 @@ type trendingSnapshotGetter interface { // Fetcher runs section queries against the database. type Fetcher struct { pool *pgxpool.Pool + progressFilter *catalog.ContinueWatchingProgressFilter StoreProvider userstore.UserStoreProvider CollectionRepo *catalog.LibraryCollectionRepository RecommendationRepo *recommendations.Repo // retained for non-reader call sites RecommendationReader recommendationReader NextUpRepo *catalog.NextUpRepository + AudiobookNextRepo *catalog.AudiobookNextRepository // TrendingSnapshots reads the persisted external-trending snapshots that // back the trending_discover section. Nil renders that section empty. @@ -91,7 +93,11 @@ type Fetcher struct { // NewFetcher creates a new section Fetcher. func NewFetcher(pool *pgxpool.Pool) *Fetcher { - return &Fetcher{pool: pool, Clock: recipes.RealClock{}} + return &Fetcher{ + pool: pool, + progressFilter: catalog.NewContinueWatchingProgressFilter(pool), + Clock: recipes.RealClock{}, + } } type editorialCandidateLoader func(context.Context, string, *int, []int, catalog.AccessFilter) ([]string, error) @@ -250,6 +256,10 @@ func (f *Fetcher) FetchOne(ctx context.Context, resolved ResolvedSection, librar result, err = f.fetchNextUpSection(ctx, resolved, libraryID, libraryIDs, userID, profileID, filter) return result, err } + if resolved.SectionType == SectionNextInSeries { + result, err = f.fetchNextInSeriesSection(ctx, resolved, libraryID, libraryIDs, userID, profileID, filter) + return result, err + } if resolved.SectionType == SectionEditorialSpotlight { var items []*models.MediaItem var total int @@ -357,6 +367,7 @@ func (f *Fetcher) fetchContinueWatchingSection(ctx context.Context, resolved Res if limit <= 0 { limit = 20 } + continueType := ContinueTypeFromConfig(resolved.Config) cfgFilters := ParseConfigFilters(resolved.Config) configLibraryIDs := cfgFilters.LibraryIDs() @@ -367,78 +378,53 @@ func (f *Fetcher) fetchContinueWatchingSection(ctx context.Context, resolved Res effectiveLibraryIDs = configLibraryIDs } - progressEntries, err := store.ListProgress(ctx, profileID, "in_progress", limit, 0) - if err != nil { - return SectionWithItems{}, fmt.Errorf("listing progress: %w", err) - } - progressEntries = f.filterContinueWatchingDismissals(ctx, store, profileID, progressEntries) - - // Build in-progress items - var orderedItems []*models.MediaItem + dismissals := f.listContinueWatchingDismissals(ctx, store, profileID) + orderedItems := make([]*models.MediaItem, 0, limit) itemMeta := make(map[string]SectionItemMeta) - if len(progressEntries) > 0 { - contentIDs := make([]string, 0, len(progressEntries)) - for _, entry := range progressEntries { - contentIDs = append(contentIDs, entry.MediaItemID) - } - - mediaItems, err := f.fetchItemsByContentIDs(ctx, contentIDs, effectiveLibID, effectiveLibraryIDs, filter) + // Ebook resume points live in ebook_reader_progress rather than the + // watch-progress store, so reading sections pull from that table and skip + // the next-up handling below. + if continueType == ContinueTypeReading { + orderedItems, err = f.collectContinueProgressItems(ctx, store, profileID, dismissals, continueType, effectiveLibID, effectiveLibraryIDs, filter, limit, orderedItems, itemMeta, + func(pageLimit, offset int) ([]userstore.WatchProgress, error) { + entries, err := f.listEbookContinueWatchingProgress(ctx, userID, profileID, pageLimit, offset) + if err != nil { + return nil, fmt.Errorf("listing ebook reader progress: %w", err) + } + return entries, nil + }) if err != nil { return SectionWithItems{}, err } - - episodeItems, episodeMeta, err := f.fetchEpisodeTargetsByContentIDs(ctx, contentIDs, effectiveLibID, effectiveLibraryIDs, filter) - if err != nil { - return SectionWithItems{}, err - } - - itemByID := make(map[string]*models.MediaItem, len(mediaItems)+len(episodeItems)) - for _, item := range mediaItems { - itemByID[item.ContentID] = item - } - for _, item := range episodeItems { - itemByID[item.ContentID] = item - } - - maps.Copy(itemMeta, episodeMeta) - supersededEpisodeProgress, err := f.fetchSupersededEpisodeProgressIDs(ctx, store, profileID, progressEntries) - if err != nil { - return SectionWithItems{}, err - } - progressEntries = filterSupersededEpisodeProgressEntries(progressEntries, supersededEpisodeProgress) - - for _, entry := range progressEntries { - meta := itemMeta[entry.MediaItemID] - position := entry.PositionSeconds - duration := entry.DurationSeconds - meta.PositionSeconds = &position - meta.DurationSeconds = &duration - meta.ProgressUpdatedAt = &entry.UpdatedAt - meta.ItemSource = "in_progress" - if updatedAt, parseErr := time.Parse(time.RFC3339, entry.UpdatedAt); parseErr == nil { - meta.SortTimestamp = updatedAt - } - itemMeta[entry.MediaItemID] = meta - } - - orderedItems = make([]*models.MediaItem, 0, len(progressEntries)) - for _, entry := range progressEntries { - item, ok := itemByID[entry.MediaItemID] - if !ok { - continue - } - orderedItems = append(orderedItems, item) - } + return SectionWithItems{ + ResolvedSection: resolved, + Items: orderedItems, + TotalCount: len(orderedItems), + ItemMeta: itemMeta, + }, nil } - // Fetch next-up items (combined mode by default) - nextUpMode, _ := store.GetSetting(ctx, "next_up_mode") - if nextUpMode == "" { - nextUpMode = "combined" + orderedItems, err = f.collectContinueProgressItems(ctx, store, profileID, dismissals, continueType, effectiveLibID, effectiveLibraryIDs, filter, limit, orderedItems, itemMeta, + func(pageLimit, offset int) ([]userstore.WatchProgress, error) { + entries, err := store.ListProgress(ctx, profileID, "in_progress", pageLimit, offset) + if err != nil { + return nil, fmt.Errorf("listing progress: %w", err) + } + return entries, nil + }) + if err != nil { + return SectionWithItems{}, err } - if nextUpMode == "combined" { + nextUpMode := "" + if ContinueTypeAllowsNextUp(continueType) { + nextUpMode, _ = store.GetSetting(ctx, "next_up_mode") + if nextUpMode == "" { + nextUpMode = "combined" + } + } + if ContinueTypeAllowsNextUp(continueType) && nextUpMode == "combined" { nextUpItems, nextUpMeta, nextUpErr := f.FetchNextUpItems(ctx, userID, profileID, effectiveLibID, effectiveLibraryIDs, filter, limit) if nextUpErr != nil { slog.Error("fetching next-up items", "error", nextUpErr) @@ -448,18 +434,6 @@ func (f *Fetcher) fetchContinueWatchingSection(ctx context.Context, resolved Res } } - // Apply type filter after next-up items are appended - if cfgFilters.FilterType != "" { - filtered := make([]*models.MediaItem, 0, len(orderedItems)) - for _, item := range orderedItems { - if (cfgFilters.FilterType == "movie" && item.Type == "movie") || - (cfgFilters.FilterType == "series" && item.Type == "episode") { - filtered = append(filtered, item) - } - } - orderedItems = filtered - } - if nextUpMode == "combined" && len(orderedItems) > 1 { orderedItems = collapseContinueWatchingSeriesCandidates(orderedItems, itemMeta) } @@ -484,6 +458,215 @@ func (f *Fetcher) fetchContinueWatchingSection(ctx context.Context, resolved Res }, nil } +// collectContinueProgressItems pages through in-progress entries from +// listPage, drops dismissed entries, resolves the remainder to media items, +// and appends them to orderedItems until limit is reached, the source is +// exhausted, or continueProgressMaxScanned entries have been scanned. Paging +// past the requested limit matters because dismissal filtering and +// library/access scoping happen after the source query: trimming at the +// source LIMIT would under-fill the section whenever dismissals exist. +func (f *Fetcher) collectContinueProgressItems( + ctx context.Context, + store userstore.UserStore, + profileID string, + dismissals catalog.HomeDismissalIndex, + continueType ContinueType, + libraryID *int, + libraryIDs []int, + filter catalog.AccessFilter, + limit int, + orderedItems []*models.MediaItem, + itemMeta map[string]SectionItemMeta, + listPage func(pageLimit, offset int) ([]userstore.WatchProgress, error), +) ([]*models.MediaItem, error) { + // LIMIT/OFFSET pages over a live, updated_at-ordered source: a progress + // write between page fetches shifts rows across page boundaries, so the + // same content_id can be returned on two consecutive pages. Track what has + // already been appended so a card never appears twice in one section. + seen := make(map[string]struct{}, limit) + for _, item := range orderedItems { + seen[item.ContentID] = struct{}{} + } + for offset := 0; len(orderedItems) < limit && offset < continueProgressMaxScanned; offset += continueProgressPageSize { + pageLimit := continueProgressPageSize + if remaining := continueProgressMaxScanned - offset; remaining < pageLimit { + pageLimit = remaining + } + progressEntries, err := listPage(pageLimit, offset) + if err != nil { + return nil, err + } + if len(progressEntries) == 0 { + break + } + rawProgressCount := len(progressEntries) + progressEntries = dismissals.FilterProgress(progressEntries) + + pageItems, pageMeta, err := f.fetchContinueProgressItems(ctx, store, profileID, progressEntries, continueType, libraryID, libraryIDs, filter) + if err != nil { + return nil, err + } + orderedItems = appendUniqueContinueItems(orderedItems, pageItems, seen, limit) + maps.Copy(itemMeta, pageMeta) + if rawProgressCount < pageLimit { + break + } + } + return orderedItems, nil +} + +// appendUniqueContinueItems appends pageItems to orderedItems up to limit, +// skipping content IDs already recorded in seen and recording every appended +// ID. Pages come from a live LIMIT/OFFSET scan, so without this guard an item +// shifted across a page boundary by a concurrent progress write would surface +// twice in the same section. +func appendUniqueContinueItems(orderedItems, pageItems []*models.MediaItem, seen map[string]struct{}, limit int) []*models.MediaItem { + for _, item := range pageItems { + if len(orderedItems) >= limit { + break + } + if _, dup := seen[item.ContentID]; dup { + continue + } + seen[item.ContentID] = struct{}{} + orderedItems = append(orderedItems, item) + } + return orderedItems +} + +// ebookContinueWatchingProgressQuery lists in-progress ebook resume points for +// Continue Reading. Like the video path (pgstore ListProgress "in_progress"), +// rows the user hid via user_history_hidden_items are excluded while reading +// activity after hidden_before counts again. +var ebookContinueWatchingProgressQuery = fmt.Sprintf(` + SELECT content_id, progress::double precision, updated_at + FROM ebook_reader_progress + WHERE user_id = $1 + AND profile_id = $2 + AND progress > 0 + AND progress < %s + AND NOT EXISTS ( + SELECT 1 + FROM user_history_hidden_items hhi + WHERE hhi.user_id = ebook_reader_progress.user_id + AND hhi.profile_id = ebook_reader_progress.profile_id + AND hhi.media_item_id = ebook_reader_progress.content_id + AND ebook_reader_progress.updated_at <= hhi.hidden_before + ) + ORDER BY updated_at DESC, content_id ASC + LIMIT $3 OFFSET $4 +`, catalog.EbookFinishedProgressThresholdSQL) + +func (f *Fetcher) listEbookContinueWatchingProgress(ctx context.Context, userID int, profileID string, limit, offset int) ([]userstore.WatchProgress, error) { + if f.pool == nil || userID <= 0 || profileID == "" || limit <= 0 { + return nil, nil + } + + rows, err := f.pool.Query(ctx, ebookContinueWatchingProgressQuery, userID, profileID, limit, offset) + if err != nil { + return nil, err + } + defer rows.Close() + + entries := make([]userstore.WatchProgress, 0) + for rows.Next() { + var contentID string + var progress float64 + var updatedAt time.Time + if err := rows.Scan(&contentID, &progress, &updatedAt); err != nil { + return nil, err + } + entries = append(entries, userstore.WatchProgress{ + MediaItemID: contentID, + PositionSeconds: progress, + DurationSeconds: 1, + UpdatedAt: updatedAt.UTC().Format(time.RFC3339), + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + return entries, nil +} + +const ( + continueProgressPageSize = 100 + continueProgressMaxScanned = 1000 +) + +func (f *Fetcher) fetchContinueProgressItems(ctx context.Context, store userstore.UserStore, profileID string, entries []userstore.WatchProgress, continueType ContinueType, libraryID *int, libraryIDs []int, filter catalog.AccessFilter) ([]*models.MediaItem, map[string]SectionItemMeta, error) { + if len(entries) == 0 { + return nil, map[string]SectionItemMeta{}, nil + } + + contentIDs := make([]string, 0, len(entries)) + for _, entry := range entries { + contentIDs = append(contentIDs, entry.MediaItemID) + } + + mediaItems, err := f.fetchItemsByContentIDs(ctx, contentIDs, libraryID, libraryIDs, filter) + if err != nil { + return nil, nil, err + } + + episodeItems, episodeMeta, err := f.fetchEpisodeTargetsByContentIDs(ctx, contentIDs, libraryID, libraryIDs, filter) + if err != nil { + return nil, nil, err + } + + itemByID := make(map[string]*models.MediaItem, len(mediaItems)+len(episodeItems)) + for _, item := range mediaItems { + itemByID[item.ContentID] = item + } + for _, item := range episodeItems { + itemByID[item.ContentID] = item + } + + matchingEntries := make([]userstore.WatchProgress, 0, len(entries)) + hasEpisodeEntries := false + for _, entry := range entries { + item, ok := itemByID[entry.MediaItemID] + if !ok || !ContinueTypeMatchesItem(continueType, item.Type) { + continue + } + if item.Type == "episode" { + hasEpisodeEntries = true + } + matchingEntries = append(matchingEntries, entry) + } + if ContinueTypeAllowsNextUp(continueType) && hasEpisodeEntries { + supersededEpisodeProgress, err := f.progressFilter.SupersededEpisodeProgressIDs(ctx, store, profileID, matchingEntries) + if err != nil { + return nil, nil, err + } + matchingEntries = catalog.FilterSupersededProgress(matchingEntries, supersededEpisodeProgress) + } + + itemMeta := make(map[string]SectionItemMeta, len(matchingEntries)) + orderedItems := make([]*models.MediaItem, 0, len(matchingEntries)) + for _, entry := range matchingEntries { + item, ok := itemByID[entry.MediaItemID] + if !ok { + continue + } + meta := episodeMeta[entry.MediaItemID] + position := entry.PositionSeconds + duration := entry.DurationSeconds + meta.PositionSeconds = &position + meta.DurationSeconds = &duration + progressUpdatedAt := entry.UpdatedAt + meta.ProgressUpdatedAt = &progressUpdatedAt + meta.ItemSource = "in_progress" + if updatedAt, parseErr := time.Parse(time.RFC3339, entry.UpdatedAt); parseErr == nil { + meta.SortTimestamp = updatedAt + } + itemMeta[entry.MediaItemID] = meta + orderedItems = append(orderedItems, item) + } + + return orderedItems, itemMeta, nil +} + func (f *Fetcher) fetchNextUpSection(ctx context.Context, resolved ResolvedSection, libraryID *int, libraryIDs []int, userID int, profileID string, filter catalog.AccessFilter) (SectionWithItems, error) { emptyResult := SectionWithItems{ ResolvedSection: resolved, @@ -595,140 +778,94 @@ func (f *Fetcher) FetchNextUpItems(ctx context.Context, userID int, profileID st return orderedItems, meta, nil } -type progressLister interface { - ListProgress(ctx context.Context, profileID, status string, limit, offset int) ([]userstore.WatchProgress, error) -} - -const supersededProgressPageSize = 500 - -type progressSnapshot struct { - ContentID string - UpdatedAt time.Time -} - -func (f *Fetcher) fetchSupersededEpisodeProgressIDs(ctx context.Context, store progressLister, profileID string, entries []userstore.WatchProgress) (map[string]struct{}, error) { - inProgress := progressSnapshots(entries) - if len(inProgress) == 0 { - return map[string]struct{}{}, nil +// fetchNextInSeriesSection surfaces the next unstarted audiobook per series +// the profile has finished a book of. Candidates come from AudiobookNextRepo +// with library scoping pushed into the SQL — otherwise finished series whose +// next book lives in another library would consume the candidate limit and +// starve a library-scoped section. Content-rating access filtering happens +// while resolving candidate IDs to items, so the candidate query still +// over-fetches to compensate for that post-resolution trim. +func (f *Fetcher) fetchNextInSeriesSection(ctx context.Context, resolved ResolvedSection, libraryID *int, libraryIDs []int, userID int, profileID string, filter catalog.AccessFilter) (SectionWithItems, error) { + emptyResult := SectionWithItems{ + ResolvedSection: resolved, + Items: []*models.MediaItem{}, + TotalCount: 0, + ItemMeta: map[string]SectionItemMeta{}, } - completed, err := completedProgressSnapshots(ctx, store, profileID) + if f.AudiobookNextRepo == nil || userID <= 0 || profileID == "" { + return emptyResult, nil + } + + limit := resolved.ItemLimit + if limit <= 0 { + limit = 20 + } + candidateLimit := limit * 3 + if candidateLimit > 100 { + candidateLimit = 100 + } + + results, err := f.AudiobookNextRepo.ListNextInSeries(ctx, catalog.NextInSeriesQuery{ + UserID: userID, + ProfileID: profileID, + Limit: candidateLimit, + LibraryID: libraryID, + LibraryIDs: effectiveFetchLibraryIDs(libraryIDs, filter), + DisabledLibraryIDs: filter.DisabledLibraryIDs, + }) if err != nil { - return nil, err + return SectionWithItems{}, err } - if len(completed) == 0 { - return map[string]struct{}{}, nil + if len(results) == 0 { + return emptyResult, nil } - inProgressIDs, inProgressUpdatedAts := splitProgressSnapshots(inProgress) - completedIDs, completedUpdatedAts := splitProgressSnapshots(completed) - query := buildSupersededEpisodeProgressQuery() - rows, err := f.pool.Query(ctx, query, inProgressIDs, inProgressUpdatedAts, completedIDs, completedUpdatedAts) + contentIDs := make([]string, len(results)) + for i, res := range results { + contentIDs[i] = res.ContentID + } + + items, err := f.fetchItemsByContentIDs(ctx, contentIDs, libraryID, libraryIDs, filter) if err != nil { - return nil, fmt.Errorf("querying superseded episode progress: %w", err) + return SectionWithItems{}, fmt.Errorf("fetching next-in-series items: %w", err) } - defer rows.Close() - superseded := make(map[string]struct{}) - for rows.Next() { - var mediaItemID string - if err := rows.Scan(&mediaItemID); err != nil { - return nil, fmt.Errorf("scanning superseded episode progress: %w", err) + itemByID := make(map[string]*models.MediaItem, len(items)) + for _, item := range items { + itemByID[item.ContentID] = item + } + + meta := make(map[string]SectionItemMeta, len(results)) + ordered := make([]*models.MediaItem, 0, limit) + for _, res := range results { + if len(ordered) >= limit { + break } - superseded[mediaItemID] = struct{}{} - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("iterating superseded episode progress: %w", err) - } - return superseded, nil -} - -func completedProgressSnapshots(ctx context.Context, store progressLister, profileID string) ([]progressSnapshot, error) { - seen := make(map[string]struct{}) - snapshots := make([]progressSnapshot, 0) - - for offset := 0; ; offset += supersededProgressPageSize { - entries, err := store.ListProgress(ctx, profileID, "completed", supersededProgressPageSize, offset) - if err != nil { - return nil, fmt.Errorf("listing completed progress for superseded episodes: %w", err) + item, ok := itemByID[res.ContentID] + if !ok { + continue } - - for _, snapshot := range progressSnapshots(entries) { - contentID := snapshot.ContentID - if _, ok := seen[contentID]; ok { - continue + m := SectionItemMeta{ + SeriesTitle: res.SeriesName, + ItemSource: "next_in_series", + SortTimestamp: res.LastFinishedAt, + } + if res.SeriesIndex != nil { + if n := int(*res.SeriesIndex); float64(n) == *res.SeriesIndex { + m.Badges = append(m.Badges, fmt.Sprintf("Book %d", n)) } - seen[contentID] = struct{}{} - snapshots = append(snapshots, snapshot) } - - if len(entries) < supersededProgressPageSize { - return snapshots, nil - } - } -} - -func progressSnapshots(entries []userstore.WatchProgress) []progressSnapshot { - snapshots := make([]progressSnapshot, 0, len(entries)) - for _, entry := range entries { - contentID := strings.TrimSpace(entry.MediaItemID) - if contentID == "" { - continue - } - updatedAt, err := time.Parse(time.RFC3339, entry.UpdatedAt) - if err != nil || updatedAt.IsZero() { - continue - } - snapshots = append(snapshots, progressSnapshot{ - ContentID: contentID, - UpdatedAt: updatedAt.UTC(), - }) - } - return snapshots -} - -func splitProgressSnapshots(snapshots []progressSnapshot) ([]string, []time.Time) { - contentIDs := make([]string, len(snapshots)) - updatedAts := make([]time.Time, len(snapshots)) - for i, snapshot := range snapshots { - contentIDs[i] = snapshot.ContentID - updatedAts[i] = snapshot.UpdatedAt - } - return contentIDs, updatedAts -} - -func buildSupersededEpisodeProgressQuery() string { - return ` - WITH in_progress(content_id, updated_at) AS ( - SELECT * FROM unnest($1::text[], $2::timestamptz[]) - ), - completed(content_id, updated_at) AS ( - SELECT * FROM unnest($3::text[], $4::timestamptz[]) - ) - SELECT DISTINCT ip.content_id - FROM in_progress ip_progress - JOIN episodes ip ON ip.content_id = ip_progress.content_id - JOIN episodes done - ON done.series_id = ip.series_id - AND (done.season_number, done.episode_number) > (ip.season_number, ip.episode_number) - JOIN completed done_progress - ON done_progress.content_id = done.content_id - WHERE done_progress.updated_at > ip_progress.updated_at` -} - -func filterSupersededEpisodeProgressEntries(entries []userstore.WatchProgress, superseded map[string]struct{}) []userstore.WatchProgress { - if len(entries) == 0 || len(superseded) == 0 { - return entries + meta[res.ContentID] = m + ordered = append(ordered, item) } - filtered := make([]userstore.WatchProgress, 0, len(entries)) - for _, entry := range entries { - if _, ok := superseded[entry.MediaItemID]; ok { - continue - } - filtered = append(filtered, entry) - } - return filtered + return SectionWithItems{ + ResolvedSection: resolved, + Items: ordered, + TotalCount: len(ordered), + ItemMeta: meta, + }, nil } func collapseContinueWatchingSeriesCandidates(items []*models.MediaItem, meta map[string]SectionItemMeta) []*models.MediaItem { @@ -792,33 +929,13 @@ func episodeOrdinal(meta SectionItemMeta) int { return season*100000 + episode } -func (f *Fetcher) filterContinueWatchingDismissals(ctx context.Context, store userstore.UserStore, profileID string, entries []userstore.WatchProgress) []userstore.WatchProgress { - if len(entries) == 0 { - return entries - } - +func (f *Fetcher) listContinueWatchingDismissals(ctx context.Context, store userstore.UserStore, profileID string) catalog.HomeDismissalIndex { dismissals, err := store.ListHomeDismissals(ctx, profileID, userstore.HomeSurfaceContinueWatching) if err != nil { slog.Error("listing continue watching dismissals", "profile_id", profileID, "error", err) - return entries + return catalog.HomeDismissalIndex{} } - if len(dismissals) == 0 { - return entries - } - - dismissalByItemID := make(map[string]userstore.HomeItemDismissal, len(dismissals)) - for _, dismissal := range dismissals { - dismissalByItemID[dismissal.MediaItemID] = dismissal - } - - filtered := make([]userstore.WatchProgress, 0, len(entries)) - for _, entry := range entries { - dismissal, ok := dismissalByItemID[entry.MediaItemID] - if !ok || dismissal.ProgressUpdatedAt == nil || *dismissal.ProgressUpdatedAt != entry.UpdatedAt { - filtered = append(filtered, entry) - } - } - return filtered + return catalog.NewHomeDismissalIndex(dismissals) } func (f *Fetcher) filterNextUpDismissals(ctx context.Context, userID int, profileID string, results []catalog.NextUpResult) []catalog.NextUpResult { @@ -2803,6 +2920,19 @@ func applyConfigTypeFilter(alias string, filterType string, conditions *[]string *argIdx++ } +func matchesContinueWatchingFilter(filterType string, itemType string) bool { + switch filterType { + case "movie", "series", "episode": + return ContinueTypeMatchesItem(ContinueTypeWatching, itemType) + case "audiobook": + return ContinueTypeMatchesItem(ContinueTypeListening, itemType) + case "ebook": + return ContinueTypeMatchesItem(ContinueTypeReading, itemType) + default: + return true + } +} + // fetchSeasonalThemed returns items for a seasonal_themed section. // // Multi-theme mode (EnabledThemes set): picks the highest-priority enabled diff --git a/internal/sections/fetcher_continue_watching_test.go b/internal/sections/fetcher_continue_watching_test.go index 53c31591..6efaeaf6 100644 --- a/internal/sections/fetcher_continue_watching_test.go +++ b/internal/sections/fetcher_continue_watching_test.go @@ -1,13 +1,11 @@ package sections import ( - "context" "strings" "testing" "time" "github.com/Silo-Server/silo-server/internal/models" - "github.com/Silo-Server/silo-server/internal/userstore" ) func TestCollapseContinueWatchingSeriesCandidatesPrefersNewestInProgressEpisode(t *testing.T) { @@ -87,82 +85,140 @@ func TestCollapseContinueWatchingSeriesCandidatesKeepsNextUpWhenNoInProgress(t * } } -func TestFilterSupersededEpisodeProgressEntriesDropsOlderPartialsAfterLaterCompletedEpisode(t *testing.T) { +func TestMatchesContinueWatchingFilterIncludesAudiobooks(t *testing.T) { t.Parallel() - entries := []userstore.WatchProgress{ - {MediaItemID: "boys-s1e1"}, - {MediaItemID: "boys-s5e3"}, - {MediaItemID: "movie-1"}, - } - superseded := map[string]struct{}{ - "boys-s1e1": {}, - "boys-s5e3": {}, + tests := []struct { + name string + filterType string + itemType string + want bool + }{ + {name: "movie keeps movie", filterType: "movie", itemType: "movie", want: true}, + {name: "movie keeps episode through watching type", filterType: "movie", itemType: "episode", want: true}, + {name: "series keeps episode", filterType: "series", itemType: "episode", want: true}, + {name: "audiobook keeps audiobook", filterType: "audiobook", itemType: "audiobook", want: true}, + {name: "audiobook rejects movie", filterType: "audiobook", itemType: "movie", want: false}, + {name: "ebook keeps ebook", filterType: "ebook", itemType: "ebook", want: true}, + {name: "unknown passes through audiobook", filterType: "unknown", itemType: "audiobook", want: true}, } - filtered := filterSupersededEpisodeProgressEntries(entries, superseded) - - if len(filtered) != 1 || filtered[0].MediaItemID != "movie-1" { - t.Fatalf("filtered entries = %+v, want only movie-1", filtered) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := matchesContinueWatchingFilter(tt.filterType, tt.itemType); got != tt.want { + t.Fatalf("matchesContinueWatchingFilter(%q, %q) = %v, want %v", tt.filterType, tt.itemType, got, tt.want) + } + }) } } -func TestCompletedProgressSnapshotsPagesThroughConfiguredStore(t *testing.T) { +func TestParseContinueType(t *testing.T) { t.Parallel() - entries := make([]userstore.WatchProgress, supersededProgressPageSize+1) - for i := range entries { - entries[i] = userstore.WatchProgress{ - MediaItemID: "done-" + time.Unix(int64(i), 0).Format("150405"), - UpdatedAt: time.Date(2025, 1, 1, 0, 0, i, 0, time.UTC).Format(time.RFC3339), - } + tests := []struct { + name string + config string + want ContinueType + }{ + {name: "missing defaults watching", config: `{}`, want: ContinueTypeWatching}, + {name: "explicit watching", config: `{"continue_type":"watching"}`, want: ContinueTypeWatching}, + {name: "explicit listening", config: `{"continue_type":"listening"}`, want: ContinueTypeListening}, + {name: "legacy audiobook filter", config: `{"filter_type":"audiobook"}`, want: ContinueTypeListening}, + {name: "legacy audiobook media scope", config: `{"media_scope":"audiobook"}`, want: ContinueTypeListening}, + {name: "future reading", config: `{"continue_type":"reading"}`, want: ContinueTypeReading}, } - store := &stubProgressLister{entries: entries} - snapshots, err := completedProgressSnapshots(context.Background(), store, "p1") - if err != nil { - t.Fatalf("completedProgressSnapshots: %v", err) - } - if len(snapshots) != len(entries) { - t.Fatalf("completed snapshots count = %d, want %d", len(snapshots), len(entries)) - } - if len(store.calls) != 2 { - t.Fatalf("ListProgress calls = %+v, want 2 paged calls", store.calls) - } - if store.calls[0] != (progressListCall{profileID: "p1", status: "completed", limit: supersededProgressPageSize, offset: 0}) { - t.Fatalf("first ListProgress call = %+v", store.calls[0]) - } - if store.calls[1] != (progressListCall{profileID: "p1", status: "completed", limit: supersededProgressPageSize, offset: supersededProgressPageSize}) { - t.Fatalf("second ListProgress call = %+v", store.calls[1]) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseContinueType([]byte(tt.config)) + if err != nil { + t.Fatalf("ParseContinueType(%s): %v", tt.config, err) + } + if got != tt.want { + t.Fatalf("ParseContinueType(%s) = %q, want %q", tt.config, got, tt.want) + } + }) } } -func TestBuildSupersededEpisodeProgressQueryUsesStoreSnapshotsWithFreshnessGate(t *testing.T) { +func TestParseContinueTypeRejectsUnknownExplicitType(t *testing.T) { t.Parallel() - query := buildSupersededEpisodeProgressQuery() - expectedFragments := []string{ - "unnest($1::text[], $2::timestamptz[])", - "unnest($3::text[], $4::timestamptz[])", - "FROM in_progress ip_progress", - "done_progress.updated_at > ip_progress.updated_at", + if _, err := ParseContinueType([]byte(`{"continue_type":"scrolling"}`)); err == nil { + t.Fatal("ParseContinueType accepted unknown continue_type") } - for _, fragment := range expectedFragments { - if !strings.Contains(query, fragment) { - t.Fatalf("expected superseded progress query to contain %q, got:\n%s", fragment, query) +} + +func TestEbookContinueWatchingProgressQueryGatesHiddenHistory(t *testing.T) { + t.Parallel() + + query := strings.Join(strings.Fields(ebookContinueWatchingProgressQuery), " ") + + // Continue Reading must honor user_history_hidden_items with the same + // semantics as the video path (pgstore ListProgress "in_progress"): a row + // is hidden while updated_at <= hidden_before and counts again once new + // reading activity lands after hidden_before. + for _, term := range []string{ + "FROM ebook_reader_progress", + "NOT EXISTS", + "FROM user_history_hidden_items hhi", + "hhi.media_item_id = ebook_reader_progress.content_id", + "ebook_reader_progress.updated_at <= hhi.hidden_before", + } { + if !strings.Contains(query, term) { + t.Fatalf("ebook continue watching query missing %q:\n%s", term, query) } } - unexpectedFragments := []string{ - "user_watch_progress", - "user_history_hidden_items", +} + +func TestAppendUniqueContinueItemsDedupesAcrossPages(t *testing.T) { + t.Parallel() + + seen := make(map[string]struct{}) + limit := 4 + + pageOne := []*models.MediaItem{ + {ContentID: "book-a"}, + {ContentID: "book-b"}, } - for _, fragment := range unexpectedFragments { - if strings.Contains(query, fragment) { - t.Fatalf("superseded progress query contains %q, got:\n%s", fragment, query) + ordered := appendUniqueContinueItems(nil, pageOne, seen, limit) + + // A progress write between pages shifts rows across the LIMIT/OFFSET + // boundary, so page two re-serves book-b. + pageTwo := []*models.MediaItem{ + {ContentID: "book-b"}, + {ContentID: "book-c"}, + } + ordered = appendUniqueContinueItems(ordered, pageTwo, seen, limit) + + got := contentIDs(ordered) + want := []string{"book-a", "book-b", "book-c"} + if len(got) != len(want) { + t.Fatalf("ordered IDs = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("ordered IDs = %v, want %v", got, want) } } } +func TestAppendUniqueContinueItemsRespectsLimit(t *testing.T) { + t.Parallel() + + seen := make(map[string]struct{}) + page := []*models.MediaItem{ + {ContentID: "a"}, + {ContentID: "b"}, + {ContentID: "c"}, + } + + ordered := appendUniqueContinueItems(nil, page, seen, 2) + if got := contentIDs(ordered); len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Fatalf("ordered IDs = %v, want [a b]", got) + } +} + func contentIDs(items []*models.MediaItem) []string { ids := make([]string, 0, len(items)) for _, item := range items { @@ -174,32 +230,3 @@ func contentIDs(items []*models.MediaItem) []string { func intPtr(v int) *int { return &v } - -type progressListCall struct { - profileID string - status string - limit int - offset int -} - -type stubProgressLister struct { - entries []userstore.WatchProgress - calls []progressListCall -} - -func (s *stubProgressLister) ListProgress(_ context.Context, profileID, status string, limit, offset int) ([]userstore.WatchProgress, error) { - s.calls = append(s.calls, progressListCall{ - profileID: profileID, - status: status, - limit: limit, - offset: offset, - }) - if offset >= len(s.entries) { - return nil, nil - } - end := offset + limit - if end > len(s.entries) { - end = len(s.entries) - } - return s.entries[offset:end], nil -} diff --git a/internal/sections/recipe_equivalence_test.go b/internal/sections/recipe_equivalence_test.go index 4979e50f..fcefe4e6 100644 --- a/internal/sections/recipe_equivalence_test.go +++ b/internal/sections/recipe_equivalence_test.go @@ -22,8 +22,8 @@ func TestRecipeDispatchEquivalence(t *testing.T) { } // Each type that has a no-DB-required short-circuit in FetchOne (continue_watching with no - // store, next_up with no repo) should resolve without error. - for _, typ := range []string{"continue_watching", "next_up"} { + // store, next_up / next_in_series with no repo) should resolve without error. + for _, typ := range []string{"continue_watching", "next_up", "next_in_series"} { rec, ok := recipes.Get(typ) if !ok { t.Fatalf("recipe %q missing", typ) diff --git a/internal/sections/recipes/library_staples.go b/internal/sections/recipes/library_staples.go index 9c1c44f3..d80e4919 100644 --- a/internal/sections/recipes/library_staples.go +++ b/internal/sections/recipes/library_staples.go @@ -2,6 +2,8 @@ package recipes import ( "encoding/json" + "fmt" + "strings" "time" ) @@ -15,18 +17,36 @@ type libStaple struct { icon string descShort string cacheTTL time.Duration + presets []GalleryPreset } -func (l *libStaple) Type() string { return l.typ } -func (l *libStaple) NewParams() any { return &libStapleParams{} } -func (l *libStaple) Validate(_ json.RawMessage) error { return nil } // any JSON is acceptable; missing fields ignored -func (l *libStaple) DefaultCacheTTL() time.Duration { return l.cacheTTL } +func (l *libStaple) Type() string { return l.typ } +func (l *libStaple) NewParams() any { return &libStapleParams{} } +func (l *libStaple) Validate(raw json.RawMessage) error { + if l.typ != "continue_watching" { + return nil // any JSON is acceptable; missing fields ignored + } + var params struct { + ContinueType string `json:"continue_type"` + } + if len(raw) > 0 { + if err := json.Unmarshal(raw, ¶ms); err != nil { + return err + } + } + switch strings.ToLower(strings.TrimSpace(params.ContinueType)) { + case "", "watching", "listening", "reading": + return nil + default: + return fmt.Errorf("continue_type must be 'watching', 'listening', or 'reading'") + } +} +func (l *libStaple) DefaultCacheTTL() time.Duration { return l.cacheTTL } func (l *libStaple) Definition() RecipeDefinition { - return RecipeDefinition{ - Type: l.typ, - Category: CategoryLibraryStaples, - Presets: []GalleryPreset{ + presets := l.presets + if presets == nil { + presets = []GalleryPreset{ { Key: l.typ + "_default", DisplayName: l.displayName, @@ -34,7 +54,12 @@ func (l *libStaple) Definition() RecipeDefinition { DescriptionShort: l.descShort, DefaultParams: json.RawMessage(`{}`), }, - }, + } + } + return RecipeDefinition{ + Type: l.typ, + Category: CategoryLibraryStaples, + Presets: presets, } } @@ -46,8 +71,31 @@ func (l *libStaple) Resolve(rc ResolverContext) (ResolvedItems, error) { func init() { Register(&libStaple{typ: "recently_added", displayName: "Recently Added", icon: "🆕", descShort: "Latest additions to your library.", cacheTTL: 5 * time.Minute}) Register(&libStaple{typ: "recently_released", displayName: "New Releases", icon: "🎬", descShort: "Recently released titles.", cacheTTL: 30 * time.Minute}) - Register(&libStaple{typ: "continue_watching", displayName: "Continue Watching", icon: "▶️", descShort: "Pick up where you left off.", cacheTTL: time.Minute}) + Register(&libStaple{ + typ: "continue_watching", + displayName: "Continue Watching", + icon: "▶️", + descShort: "Pick up where you left off.", + cacheTTL: time.Minute, + presets: []GalleryPreset{ + { + Key: "continue_watching_default", + DisplayName: "Continue Watching", + Icon: "▶️", + DescriptionShort: "Pick up movies and episodes where you left off.", + DefaultParams: json.RawMessage(`{"continue_type":"watching"}`), + }, + { + Key: "continue_listening_default", + DisplayName: "Continue Listening", + Icon: "🎧", + DescriptionShort: "Pick up audiobooks where you left off.", + DefaultParams: json.RawMessage(`{"continue_type":"listening"}`), + }, + }, + }) Register(&libStaple{typ: "next_up", displayName: "On Deck", icon: "📺", descShort: "Next episodes ready to watch.", cacheTTL: time.Minute}) + Register(&libStaple{typ: "next_in_series", displayName: "Next in Series", icon: "📚", descShort: "The next audiobook in series you've finished.", cacheTTL: time.Minute}) Register(&libStaple{typ: "watchlist", displayName: "Watchlist", icon: "🔖", descShort: "Items you've saved to watch.", cacheTTL: time.Minute}) Register(&libStaple{typ: "favorites", displayName: "Favorites", icon: "⭐", descShort: "Your favorites.", cacheTTL: time.Minute}) Register(&libStaple{typ: "random", displayName: "Surprise Me", icon: "🎲", descShort: "A random selection from your library.", cacheTTL: 5 * time.Minute}) diff --git a/internal/sections/recipes/library_staples_test.go b/internal/sections/recipes/library_staples_test.go index 1bc1cfe2..d9aed5ed 100644 --- a/internal/sections/recipes/library_staples_test.go +++ b/internal/sections/recipes/library_staples_test.go @@ -30,3 +30,27 @@ func TestLibraryStaplesAcceptEmptyParams(t *testing.T) { } } } + +func TestContinueWatchingRecipeExposesWatchingAndListeningPresets(t *testing.T) { + rec, ok := Get("continue_watching") + if !ok { + t.Fatal("continue_watching recipe not registered") + } + presets := rec.Definition().Presets + if len(presets) != 2 { + t.Fatalf("presets len = %d, want 2", len(presets)) + } + if presets[0].DisplayName != "Continue Watching" || string(presets[0].DefaultParams) != `{"continue_type":"watching"}` { + t.Fatalf("watching preset = %+v", presets[0]) + } + if presets[1].DisplayName != "Continue Listening" || string(presets[1].DefaultParams) != `{"continue_type":"listening"}` { + t.Fatalf("listening preset = %+v", presets[1]) + } +} + +func TestContinueWatchingRecipeRejectsUnknownContinueType(t *testing.T) { + rec, _ := Get("continue_watching") + if err := rec.Validate(json.RawMessage(`{"continue_type":"scrolling"}`)); err == nil { + t.Fatal("Validate accepted unknown continue_type") + } +} diff --git a/internal/sections/repo.go b/internal/sections/repo.go index a0db33c5..0792bbd4 100644 --- a/internal/sections/repo.go +++ b/internal/sections/repo.go @@ -448,6 +448,120 @@ func (r *Repository) CreateGeneratedHomeLibraryRecentSections(ctx context.Contex return created, nil } +func (r *Repository) EnsureHomeContinueListeningSection(ctx context.Context) (*PageSection, error) { + tx, err := r.pool.Begin(ctx) + if err != nil { + return nil, fmt.Errorf("beginning continue listening transaction: %w", err) + } + defer tx.Rollback(ctx) + + var existingID string + err = tx.QueryRow(ctx, ` + SELECT id + FROM page_sections + WHERE scope = 'home' + AND library_id IS NULL + AND section_type = $1 + AND ( + config->>'continue_type' = $2 + OR config->>'filter_type' = 'audiobook' + OR config->>'media_scope' = 'audiobook' + ) + ORDER BY position ASC + LIMIT 1 + `, SectionContinueWatching, ContinueTypeListening).Scan(&existingID) + if err == nil { + return nil, tx.Commit(ctx) + } + if !errors.Is(err, pgx.ErrNoRows) { + return nil, fmt.Errorf("checking home continue listening section: %w", err) + } + + insertPosition, err := continueListeningInsertPosition(ctx, tx) + if err != nil { + return nil, err + } + if _, err := tx.Exec(ctx, ` + UPDATE page_sections + SET position = position + 1, updated_at = NOW() + WHERE scope = 'home' + AND library_id IS NULL + AND position >= $1 + `, insertPosition); err != nil { + return nil, fmt.Errorf("shifting home sections: %w", err) + } + + id, err := idgen.NextID() + if err != nil { + return nil, fmt.Errorf("generate section id: %w", err) + } + query := fmt.Sprintf(`INSERT INTO page_sections (%s) VALUES ($1,'home',NULL,$2,$3,$4,false,20,$5,true,NOW(),NOW()) + RETURNING %s`, sectionColumns, sectionColumns) + created, err := scanSection(tx.QueryRow(ctx, query, + id, + insertPosition, + SectionContinueWatching, + "Continue Listening", + ContinueTypeConfig(ContinueTypeListening), + )) + if err != nil { + return nil, fmt.Errorf("creating home continue listening section: %w", err) + } + + if err := tx.Commit(ctx); err != nil { + return nil, fmt.Errorf("committing continue listening transaction: %w", err) + } + return created, nil +} + +type homeSectionPositionRow struct { + Position int + SectionType SectionType + Config json.RawMessage +} + +type queryer interface { + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) +} + +func continueListeningInsertPosition(ctx context.Context, q queryer) (int, error) { + rows, err := q.Query(ctx, ` + SELECT position, section_type, config + FROM page_sections + WHERE scope = 'home' + AND library_id IS NULL + ORDER BY position ASC + `) + if err != nil { + return 0, fmt.Errorf("listing home section positions: %w", err) + } + defer rows.Close() + + insertPosition := 0 + foundWatching := false + for rows.Next() { + var row homeSectionPositionRow + if err := rows.Scan(&row.Position, &row.SectionType, &row.Config); err != nil { + return 0, fmt.Errorf("scanning home section position: %w", err) + } + if foundWatching { + if row.SectionType != SectionContinueWatching { + break + } + insertPosition = row.Position + 1 + continue + } + insertPosition = row.Position + 1 + if row.SectionType == SectionContinueWatching && ContinueTypeFromConfig(row.Config) == ContinueTypeWatching { + foundWatching = true + } + } + if err := rows.Err(); err != nil { + return 0, fmt.Errorf("iterating home section positions: %w", err) + } + return insertPosition, nil +} + func (r *Repository) SyncGeneratedHomeLibraryRecentTitles(ctx context.Context, libraryID int, oldLibraryName, newLibraryName string) error { sections, err := r.listGeneratedHomeLibraryRecentSections(ctx, libraryID) if err != nil { diff --git a/internal/sections/types.go b/internal/sections/types.go index 6506f9b3..fc219b42 100644 --- a/internal/sections/types.go +++ b/internal/sections/types.go @@ -26,6 +26,7 @@ const ( SectionSimilarUsersLiked SectionType = "similar_users_liked" SectionTasteMatch SectionType = "taste_match" SectionNextUp SectionType = "next_up" + SectionNextInSeries SectionType = "next_in_series" SectionHiddenGems SectionType = "hidden_gems" SectionCriticallyAcclaimed SectionType = "critically_acclaimed" SectionAwardWinners SectionType = "award_winners" @@ -61,6 +62,7 @@ var ValidSectionTypes = map[SectionType]bool{ SectionSimilarUsersLiked: true, SectionTasteMatch: true, SectionNextUp: true, + SectionNextInSeries: true, SectionHiddenGems: true, SectionCriticallyAcclaimed: true, SectionAwardWinners: true, @@ -181,7 +183,7 @@ func ParseConfigFilters(config json.RawMessage) SectionConfigFilters { def, err := ParseQueryDefinition(config) if err == nil { switch def.MediaScope { - case "movie", "series": + case "movie", "series", "audiobook": f.FilterType = def.MediaScope } if len(def.LibraryIDs) > 0 { diff --git a/internal/server/frontend.go b/internal/server/frontend.go index 21df49bb..0e937f1e 100644 --- a/internal/server/frontend.go +++ b/internal/server/frontend.go @@ -10,6 +10,43 @@ import ( // When nil, FrontendHandler returns a placeholder response. var WebDistFS fs.FS +// frontendContentSecurityPolicy is served with every SPA HTML response. +// +// SECURITY: this policy is the primary mitigation for malicious ebook content. +// The in-app reader (foliate-js) renders book chapters in same-origin blob: +// iframes with allow-scripts (required to work around a WebKit bug), and +// blob:/srcdoc documents inherit the embedding document's CSP. With +// script-src 'self', scripts embedded in an EPUB (blob:/inline/data: sources) +// cannot execute, so a hostile book cannot read localStorage tokens or call +// the API. Do not add 'unsafe-inline', 'unsafe-eval', blob:, or data: to +// script-src without revisiting that threat model. +// +// Allowances beyond 'self' exist for concrete app needs: +// - script-src 'wasm-unsafe-eval': JASSUB (libass) subtitle rendering and +// node-unrar-js CBR extraction compile WebAssembly. +// - style-src blob: and 'unsafe-inline': foliate-js loads EPUB stylesheets +// via blob: URLs; the app uses inline style attributes. Google Fonts CSS +// is linked from index.html. +// - img-src/media-src http(s): artwork can come from TMDB/TVDB/S3 public +// URLs, and stream URLs may point at standalone proxy/transcode workers +// on another origin (proxy public_url, plain http on LANs). +// - connect-src http(s)/ws(s): realtime session hub WebSockets, browser-side +// Plex auth (plex.tv), and HLS fetches against standalone worker origins. +// - font-src blob: data: plus fonts.gstatic.com for Google Fonts; reader +// book fonts load from blob: URLs. +const frontendContentSecurityPolicy = "default-src 'self'; " + + "script-src 'self' 'wasm-unsafe-eval'; " + + "style-src 'self' 'unsafe-inline' blob: https://fonts.googleapis.com; " + + "img-src 'self' blob: data: http: https:; " + + "font-src 'self' blob: data: https://fonts.gstatic.com; " + + "media-src 'self' blob: http: https:; " + + "connect-src 'self' ws: wss: http: https:; " + + "worker-src 'self' blob:; " + + "frame-src 'self' blob:; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "form-action 'self'" + // FrontendHandler returns an http.Handler that serves the embedded SPA. // It serves static files from WebDistFS and falls back to index.html for // SPA routing (any path that doesn't match a file). @@ -17,6 +54,7 @@ func FrontendHandler() http.Handler { if WebDistFS == nil { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain") + w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("Frontend not built. Run: cd web && bun run build")) }) @@ -25,10 +63,12 @@ func FrontendHandler() http.Handler { fileServer := http.FileServer(http.FS(WebDistFS)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") path := r.URL.Path - // Try to serve the file directly - if path != "/" && !strings.HasSuffix(path, "/") { + // Try to serve the file directly. index.html is excluded so the SPA + // HTML always goes through the fallback below and carries the CSP. + if path != "/" && path != "/index.html" && !strings.HasSuffix(path, "/") { if f, err := WebDistFS.Open(strings.TrimPrefix(path, "/")); err == nil { f.Close() fileServer.ServeHTTP(w, r) @@ -43,6 +83,7 @@ func FrontendHandler() http.Handler { return } w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Content-Security-Policy", frontendContentSecurityPolicy) w.WriteHeader(http.StatusOK) _, _ = w.Write(indexBytes) }) diff --git a/internal/server/frontend_test.go b/internal/server/frontend_test.go new file mode 100644 index 00000000..fbb3570f --- /dev/null +++ b/internal/server/frontend_test.go @@ -0,0 +1,80 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "testing/fstest" +) + +func TestFrontendHandlerSetsSecurityHeadersOnSPAHTML(t *testing.T) { + prev := WebDistFS + WebDistFS = fstest.MapFS{ + "index.html": &fstest.MapFile{Data: []byte("
")}, + "assets/app.js": &fstest.MapFile{ + Data: []byte("console.log(1)"), + }, + } + t.Cleanup(func() { WebDistFS = prev }) + + handler := FrontendHandler() + + for _, path := range []string{"/", "/index.html", "/library/ebooks"} { + t.Run(path, func(t *testing.T) { + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil)) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d", rr.Code) + } + if got := rr.Header().Get("Content-Type"); got != "text/html; charset=utf-8" { + t.Fatalf("content-type = %q", got) + } + csp := rr.Header().Get("Content-Security-Policy") + if csp != frontendContentSecurityPolicy { + t.Fatalf("csp = %q", csp) + } + // The ebook-reader threat model depends on these directives; fail + // loudly if they are weakened. + for _, directive := range []string{ + "script-src 'self' 'wasm-unsafe-eval'", + "object-src 'none'", + "base-uri 'self'", + } { + if !strings.Contains(csp, directive) { + t.Fatalf("csp missing %q: %q", directive, csp) + } + } + if strings.Contains(csp, "script-src 'self' 'wasm-unsafe-eval' ") { + t.Fatalf("script-src must not carry extra sources: %q", csp) + } + if got := rr.Header().Get("X-Content-Type-Options"); got != "nosniff" { + t.Fatalf("x-content-type-options = %q", got) + } + }) + } +} + +func TestFrontendHandlerServesStaticAssetsWithoutCSP(t *testing.T) { + prev := WebDistFS + WebDistFS = fstest.MapFS{ + "index.html": &fstest.MapFile{Data: []byte("")}, + "assets/app.js": &fstest.MapFile{Data: []byte("console.log(1)")}, + } + t.Cleanup(func() { WebDistFS = prev }) + + handler := FrontendHandler() + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/assets/app.js", nil)) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d", rr.Code) + } + if got := rr.Header().Get("Content-Security-Policy"); got != "" { + t.Fatalf("static asset should not carry the SPA CSP, got %q", got) + } + if got := rr.Header().Get("X-Content-Type-Options"); got != "nosniff" { + t.Fatalf("x-content-type-options = %q", got) + } +} diff --git a/internal/taskmanager/manager.go b/internal/taskmanager/manager.go index bbc8bad0..4fbf1872 100644 --- a/internal/taskmanager/manager.go +++ b/internal/taskmanager/manager.go @@ -146,6 +146,16 @@ func (m *TaskManager) triggerLoop(ctx context.Context, w *taskWorker) { continue } + shouldRun, shouldRunErr := m.shouldRunScheduledTask(ctx, w) + if shouldRunErr != nil { + m.logger.Warn("scheduled task preflight failed; running task", + "task", w.task.Key(), "error", shouldRunErr) + } + if shouldRunErr == nil && !shouldRun { + m.rearmTriggersFromNow(w) + continue + } + result, err := w.run(ctx) if err != nil { // If the task is already running (e.g. via manual RunTask), don't @@ -168,12 +178,34 @@ func (m *TaskManager) triggerLoop(ctx context.Context, w *taskWorker) { } } +func (m *TaskManager) shouldRunScheduledTask(ctx context.Context, w *taskWorker) (bool, error) { + task, ok := w.task.(ScheduledConditionalTask) + if !ok { + return true, nil + } + return task.ShouldRun(ctx) +} + // rearmTriggers stops and restarts all triggers for a worker. func (m *TaskManager) rearmTriggers(w *taskWorker) { w.mu.Lock() + lastResult := w.lastResult for _, tr := range w.triggers { tr.Stop() - tr.Start(w.lastResult) + tr.Start(lastResult) + } + w.mu.Unlock() + w.notify() +} + +// rearmTriggersFromNow restarts triggers after a skipped conditional task. This +// intentionally ignores lastResult; otherwise an old completed_at can make an +// interval trigger fire immediately in a loop while there is no work. +func (m *TaskManager) rearmTriggersFromNow(w *taskWorker) { + w.mu.Lock() + for _, tr := range w.triggers { + tr.Stop() + tr.Start(nil) } w.mu.Unlock() w.notify() diff --git a/internal/taskmanager/manager_test.go b/internal/taskmanager/manager_test.go index 9f7ff7e9..84401172 100644 --- a/internal/taskmanager/manager_test.go +++ b/internal/taskmanager/manager_test.go @@ -50,6 +50,33 @@ func (fakeExecutionRepository) List(context.Context, string, int) ([]taskmanager return nil, nil } +type recordingExecutionRepository struct { + mu sync.Mutex + inserts []taskmanager.ExecutionResult + latest *taskmanager.ExecutionResult +} + +func (r *recordingExecutionRepository) Insert(_ context.Context, result taskmanager.ExecutionResult) error { + r.mu.Lock() + defer r.mu.Unlock() + r.inserts = append(r.inserts, result) + return nil +} + +func (r *recordingExecutionRepository) GetLatest(context.Context, string) (*taskmanager.ExecutionResult, error) { + return r.latest, nil +} + +func (r *recordingExecutionRepository) List(context.Context, string, int) ([]taskmanager.ExecutionResult, error) { + return nil, nil +} + +func (r *recordingExecutionRepository) insertCount() int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.inserts) +} + type fakeTrigger struct { cfg taskmanager.TriggerConfig ch chan struct{} @@ -117,6 +144,34 @@ func (t stubTask) DefaultTriggers() []taskmanager.TriggerConfig { return append([]taskmanager.TriggerConfig(nil), t.triggers...) } +type conditionalStubTask struct { + stubTask + shouldRunCalled chan struct{} + mu sync.Mutex + executeCalls int +} + +func (t *conditionalStubTask) ShouldRun(context.Context) (bool, error) { + select { + case t.shouldRunCalled <- struct{}{}: + default: + } + return false, nil +} + +func (t *conditionalStubTask) Execute(context.Context, taskmanager.ProgressReporter) error { + t.mu.Lock() + defer t.mu.Unlock() + t.executeCalls++ + return nil +} + +func (t *conditionalStubTask) executeCount() int { + t.mu.Lock() + defer t.mu.Unlock() + return t.executeCalls +} + func newFakeTrigger(cfg taskmanager.TriggerConfig) taskmanager.Trigger { return &fakeTrigger{ cfg: cfg, @@ -265,3 +320,70 @@ func TestTaskManagerRunTaskNotifiesAfterTriggerRearm(t *testing.T) { last.NextRunAt.Sub(last.LastExecution.CompletedAt)) } } + +func TestTaskManagerTriggerSkipsConditionalTaskWithoutHistory(t *testing.T) { + triggerRepo := &fakeTriggerRepository{ + triggers: map[string][]taskmanager.TriggerConfig{ + "conditional": { + {Type: taskmanager.TriggerTypeInterval, IntervalMs: int64(time.Hour / time.Millisecond)}, + }, + }, + } + historyRepo := &recordingExecutionRepository{ + latest: &taskmanager.ExecutionResult{ + TaskKey: "conditional", + CompletedAt: time.Now().Add(-24 * time.Hour), + }, + } + var triggers []*fakeTrigger + factory := func(cfg taskmanager.TriggerConfig) taskmanager.Trigger { + tr := &fakeTrigger{ + cfg: cfg, + ch: make(chan struct{}, 1), + stopCh: make(chan struct{}), + } + triggers = append(triggers, tr) + return tr + } + manager := taskmanager.New( + triggerRepo, + historyRepo, + factory, + slog.New(slog.NewTextHandler(io.Discard, nil)), + ) + task := &conditionalStubTask{ + stubTask: stubTask{key: "conditional"}, + shouldRunCalled: make(chan struct{}, 1), + } + manager.Register(task) + + ctx, cancel := context.WithCancel(context.Background()) + defer manager.Stop() + defer cancel() + manager.Start(ctx) + + if len(triggers) != 1 { + t.Fatalf("triggers = %d, want 1", len(triggers)) + } + beforeTrigger := time.Now() + triggers[0].ch <- struct{}{} + + select { + case <-task.shouldRunCalled: + case <-time.After(time.Second): + t.Fatal("scheduled preflight was not called") + } + + time.Sleep(25 * time.Millisecond) + if got := task.executeCount(); got != 0 { + t.Fatalf("Execute calls = %d, want 0", got) + } + if got := historyRepo.insertCount(); got != 0 { + t.Fatalf("history inserts = %d, want 0", got) + } + if !triggers[0].next.After(beforeTrigger) { + t.Fatalf("next run = %s, want rearmed after skip time %s", + triggers[0].next.Format(time.RFC3339Nano), + beforeTrigger.Format(time.RFC3339Nano)) + } +} diff --git a/internal/taskmanager/task.go b/internal/taskmanager/task.go index 8762e53e..bcb9857f 100644 --- a/internal/taskmanager/task.go +++ b/internal/taskmanager/task.go @@ -41,6 +41,13 @@ type Task interface { Execute(ctx context.Context, progress ProgressReporter) error } +// ScheduledConditionalTask lets a task suppress automatic trigger executions +// when there is no work to do. Manual RunTask calls still execute normally so +// admins can force a check and see a result. +type ScheduledConditionalTask interface { + ShouldRun(ctx context.Context) (bool, error) +} + // ProgressReporter allows tasks to report progress and result data during execution. type ProgressReporter interface { Report(percent float64, message string) diff --git a/internal/taskmanager/tasks/sync_audiobook_metadata.go b/internal/taskmanager/tasks/sync_audiobook_metadata.go index d8ce563b..b10ab95a 100644 --- a/internal/taskmanager/tasks/sync_audiobook_metadata.go +++ b/internal/taskmanager/tasks/sync_audiobook_metadata.go @@ -38,6 +38,13 @@ func (t *SyncAudiobookMetadataTask) DefaultTriggers() []taskmanager.TriggerConfi } } +func (t *SyncAudiobookMetadataTask) ShouldRun(ctx context.Context) (bool, error) { + if t == nil || t.enricher == nil { + return false, nil + } + return t.enricher.HasPendingItems(ctx) +} + func (t *SyncAudiobookMetadataTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error { progress.Report(0, "Scanning for unenriched audiobooks") diff --git a/internal/taskmanager/tasks/sync_ebook_metadata.go b/internal/taskmanager/tasks/sync_ebook_metadata.go new file mode 100644 index 00000000..8656ab7d --- /dev/null +++ b/internal/taskmanager/tasks/sync_ebook_metadata.go @@ -0,0 +1,56 @@ +package tasks + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/Silo-Server/silo-server/internal/taskmanager" +) + +type ebookMetadataEnricher interface { + Run(ctx context.Context) (int, error) +} + +// SyncEbookMetadataTask runs the periodic ebook enrichment sweep. +// It calls ebooks.Enricher.Run() which selects unenriched ebook media_items, +// resolves the per-folder metadata-provider chain at content_level='ebook', +// and writes results back to the database. +type SyncEbookMetadataTask struct { + enricher ebookMetadataEnricher +} + +// NewSyncEbookMetadataTask constructs the task. +func NewSyncEbookMetadataTask(enricher ebookMetadataEnricher) *SyncEbookMetadataTask { + return &SyncEbookMetadataTask{enricher: enricher} +} + +func (t *SyncEbookMetadataTask) Key() string { return "sync_ebook_metadata" } +func (t *SyncEbookMetadataTask) Name() string { return "Sync Ebook Metadata" } +func (t *SyncEbookMetadataTask) Description() string { + return "Fetches metadata (cover art, overview, authors) for ebooks that have not yet been enriched" +} +func (t *SyncEbookMetadataTask) Category() taskmanager.TaskCategory { + return taskmanager.TaskCategoryMetadata +} +func (t *SyncEbookMetadataTask) IsHidden() bool { return false } + +func (t *SyncEbookMetadataTask) DefaultTriggers() []taskmanager.TriggerConfig { + return []taskmanager.TriggerConfig{ + {Type: taskmanager.TriggerTypeInterval, IntervalMs: 5 * 60 * 1000}, + } +} + +func (t *SyncEbookMetadataTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error { + progress.Report(0, "Scanning for unenriched ebooks") + + enriched, err := t.enricher.Run(ctx) + if err != nil { + return fmt.Errorf("ebook metadata sync: %w", err) + } + + result, _ := json.Marshal(map[string]int{"items_enriched": enriched}) + progress.SetResultData(result) + progress.Report(100, fmt.Sprintf("Ebook metadata sync complete (%d items enriched)", enriched)) + return nil +} diff --git a/internal/taskmanager/tasks/sync_ebook_metadata_test.go b/internal/taskmanager/tasks/sync_ebook_metadata_test.go new file mode 100644 index 00000000..0adb8c9a --- /dev/null +++ b/internal/taskmanager/tasks/sync_ebook_metadata_test.go @@ -0,0 +1,92 @@ +package tasks + +import ( + "context" + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/Silo-Server/silo-server/internal/taskmanager" +) + +type fakeEbookMetadataEnricher struct { + enriched int + err error + called bool +} + +func (f *fakeEbookMetadataEnricher) Run(context.Context) (int, error) { + f.called = true + return f.enriched, f.err +} + +type ebookMetadataProgressReporter struct { + messages []string + result json.RawMessage +} + +func (p *ebookMetadataProgressReporter) Report(_ float64, message string) { + p.messages = append(p.messages, message) +} + +func (p *ebookMetadataProgressReporter) SetResultData(data json.RawMessage) { + p.result = data +} + +func TestSyncEbookMetadataTaskProperties(t *testing.T) { + task := NewSyncEbookMetadataTask(&fakeEbookMetadataEnricher{}) + + if task.Key() != "sync_ebook_metadata" { + t.Fatalf("Key() = %q, want sync_ebook_metadata", task.Key()) + } + if task.Name() != "Sync Ebook Metadata" { + t.Fatalf("Name() = %q, want Sync Ebook Metadata", task.Name()) + } + if task.Category() != taskmanager.TaskCategoryMetadata { + t.Fatalf("Category() = %q, want %q", task.Category(), taskmanager.TaskCategoryMetadata) + } + if task.IsHidden() { + t.Fatal("IsHidden() = true, want false") + } + triggers := task.DefaultTriggers() + if len(triggers) != 1 || triggers[0].Type != taskmanager.TriggerTypeInterval || triggers[0].IntervalMs != 5*60*1000 { + t.Fatalf("DefaultTriggers() = %#v, want one 5 minute interval", triggers) + } + if strings.Contains(strings.ToLower(task.Description()), "narrator") { + t.Fatalf("Description() mentions narrator: %q", task.Description()) + } +} + +func TestSyncEbookMetadataTaskExecuteReportsResult(t *testing.T) { + enricher := &fakeEbookMetadataEnricher{enriched: 3} + task := NewSyncEbookMetadataTask(enricher) + progress := &ebookMetadataProgressReporter{} + + if err := task.Execute(context.Background(), progress); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if !enricher.called { + t.Fatal("enricher was not called") + } + var result map[string]int + if err := json.Unmarshal(progress.result, &result); err != nil { + t.Fatalf("result data JSON error: %v", err) + } + if result["items_enriched"] != 3 { + t.Fatalf("items_enriched = %d, want 3", result["items_enriched"]) + } + if len(progress.messages) == 0 || !strings.Contains(progress.messages[len(progress.messages)-1], "3 items enriched") { + t.Fatalf("progress messages = %#v, want completion count", progress.messages) + } +} + +func TestSyncEbookMetadataTaskExecuteWrapsError(t *testing.T) { + enricher := &fakeEbookMetadataEnricher{err: errors.New("boom")} + task := NewSyncEbookMetadataTask(enricher) + + err := task.Execute(context.Background(), &ebookMetadataProgressReporter{}) + if err == nil || !strings.Contains(err.Error(), "ebook metadata sync") { + t.Fatalf("Execute() error = %v, want wrapped ebook metadata sync error", err) + } +} diff --git a/internal/taskmanager/tasks/sync_podcast_feeds.go b/internal/taskmanager/tasks/sync_podcast_feeds.go index 2589c6bd..ae56df83 100644 --- a/internal/taskmanager/tasks/sync_podcast_feeds.go +++ b/internal/taskmanager/tasks/sync_podcast_feeds.go @@ -39,6 +39,13 @@ func (t *SyncPodcastFeedsTask) DefaultTriggers() []taskmanager.TriggerConfig { } } +func (t *SyncPodcastFeedsTask) ShouldRun(ctx context.Context) (bool, error) { + if t == nil || t.refresher == nil || t.store == nil { + return false, nil + } + return t.refresher.HasDue(ctx, t.store) +} + func (t *SyncPodcastFeedsTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error { progress.Report(0, "Checking for due podcast feeds") diff --git a/internal/userdb/migrate.go b/internal/userdb/migrate.go index afaf8fd9..8e949723 100644 --- a/internal/userdb/migrate.go +++ b/internal/userdb/migrate.go @@ -5,7 +5,7 @@ import ( "fmt" ) -const schemaVersion = 10 +const schemaVersion = 11 func runMigrations(db *sql.DB) error { version, err := userVersion(db) @@ -106,9 +106,37 @@ func runMigrations(db *sql.DB) error { } } + if version < 11 { + if err := migrateToV11(tx); err != nil { + return err + } + if _, err := tx.Exec("PRAGMA user_version = 11"); err != nil { + return fmt.Errorf("setting sqlite user_version 11: %w", err) + } + } + return tx.Commit() } +// migrateToV11 resets legacy completed watch_progress rows to +// position_seconds = 0. The watch-progress model keeps `completed` as a +// one-way watched latch with no resume point; the Continue Watching +// predicate is position_seconds > 0, so legacy rows (position pinned to the +// duration) would otherwise surface as phantom resume entries. Running this +// as a versioned migration (rather than a predicate-only data fix) matters: +// re-running the UPDATE on every boot would wipe the resume point of any +// rewatch in flight. +func migrateToV11(tx *sql.Tx) error { + if _, err := tx.Exec(` + UPDATE watch_progress + SET position_seconds = 0 + WHERE completed = 1 + AND position_seconds <> 0`); err != nil { + return fmt.Errorf("resetting completed watch_progress positions: %w", err) + } + return nil +} + func migrateToV10(tx *sql.Tx) error { if columnExists(tx, "watch_history", "watch_identity") { return nil diff --git a/internal/userdb/progress.go b/internal/userdb/progress.go index 4f0096ba..f34d6aec 100644 --- a/internal/userdb/progress.go +++ b/internal/userdb/progress.go @@ -20,12 +20,20 @@ type WatchHistoryEntry = userstore.WatchHistoryEntry // The position is only updated if the new value is greater than the existing one. // The completed flag is set to true when position/duration exceeds the watched threshold. func UpdateProgress(db *sql.DB, profileID, mediaItemID string, position, duration float64, thresholds userstore.ProgressThresholds) error { + if duration > 0 && position > 0 && position/duration < userstore.MinResumeFraction(thresholds.MinResumePct) { + return nil + } now := nowUTC() + // Mirrors the Postgres pgstore UpdateProgress: `completed` is a one-way + // watched latch; position resets to 0 on completion so a rewatch + // heartbeat on a completed row re-enters Continue Watching through plain + // MAX while the watched flag survives. query := ` INSERT INTO watch_progress (profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(profile_id, media_item_id) DO UPDATE SET - position_seconds = MAX(excluded.position_seconds, watch_progress.position_seconds), + position_seconds = CASE WHEN excluded.completed = 1 THEN 0 + ELSE MAX(excluded.position_seconds, watch_progress.position_seconds) END, duration_seconds = excluded.duration_seconds, completed = CASE WHEN excluded.completed = 1 THEN 1 ELSE watch_progress.completed END, @@ -34,7 +42,7 @@ func UpdateProgress(db *sql.DB, profileID, mediaItemID string, position, duratio completed := false if duration > 0 && position/duration > userstore.WatchedFraction(thresholds.WatchedPct) { completed = true - position = duration // match MarkWatched() — reset so no stale resume point + position = 0 // match MarkWatched() — completed rows hold no resume point } _, err := db.Exec(query, profileID, mediaItemID, position, duration, completed, now) if err != nil { @@ -44,13 +52,15 @@ func UpdateProgress(db *sql.DB, profileID, mediaItemID string, position, duratio } // SetProgress bypasses the forward-only guard (for rewatches/explicit seek). -// It unconditionally sets the position to the given value. +// It unconditionally sets the position to the given value. The completed flag +// stays a one-way watched latch: only ClearProgress/ClearProgressBatch (mark +// unwatched) release it. func SetProgress(db *sql.DB, profileID, mediaItemID string, position, duration float64, thresholds userstore.ProgressThresholds) error { now := nowUTC() completed := false if duration > 0 && position/duration > userstore.WatchedFraction(thresholds.WatchedPct) { completed = true - position = duration // match MarkWatched() — reset so no stale resume point + position = 0 // match MarkWatched() — completed rows hold no resume point } query := ` INSERT INTO watch_progress (profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) @@ -58,7 +68,7 @@ func SetProgress(db *sql.DB, profileID, mediaItemID string, position, duration f ON CONFLICT(profile_id, media_item_id) DO UPDATE SET position_seconds = excluded.position_seconds, duration_seconds = excluded.duration_seconds, - completed = excluded.completed, + completed = watch_progress.completed OR excluded.completed, updated_at = excluded.updated_at ` _, err := db.Exec(query, profileID, mediaItemID, position, duration, completed, now) @@ -76,8 +86,8 @@ func SetProgressAt(db *sql.DB, profileID, mediaItemID string, position, duration if duration < 0 { duration = 0 } - if completed && duration > 0 { - position = duration + if completed { + position = 0 } if updatedAt.IsZero() { updatedAt = time.Now().UTC() @@ -96,7 +106,7 @@ func SetProgressAt(db *sql.DB, profileID, mediaItemID string, position, duration ON CONFLICT(profile_id, media_item_id) DO UPDATE SET position_seconds = excluded.position_seconds, duration_seconds = excluded.duration_seconds, - completed = excluded.completed, + completed = watch_progress.completed OR excluded.completed, updated_at = excluded.updated_at ` _, err = db.Exec(query, profileID, mediaItemID, position, duration, completed, updatedAtText) @@ -113,8 +123,8 @@ func SetProgressIfNewer(db *sql.DB, profileID, mediaItemID string, position, dur if duration < 0 { duration = 0 } - if completed && duration > 0 { - position = duration + if completed { + position = 0 } if updatedAt.IsZero() { updatedAt = time.Now().UTC() @@ -133,7 +143,7 @@ func SetProgressIfNewer(db *sql.DB, profileID, mediaItemID string, position, dur ON CONFLICT(profile_id, media_item_id) DO UPDATE SET position_seconds = excluded.position_seconds, duration_seconds = excluded.duration_seconds, - completed = excluded.completed, + completed = watch_progress.completed OR excluded.completed, updated_at = excluded.updated_at WHERE excluded.updated_at > watch_progress.updated_at `, profileID, mediaItemID, position, duration, completed, updatedAtText) @@ -156,14 +166,14 @@ func MarkWatched(db *sql.DB, profileID, mediaItemID string, duration float64) er now := nowUTC() query := ` INSERT INTO watch_progress (profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) - VALUES (?, ?, ?, ?, 1, ?) + VALUES (?, ?, 0, ?, 1, ?) ON CONFLICT(profile_id, media_item_id) DO UPDATE SET - position_seconds = excluded.position_seconds, + position_seconds = 0, duration_seconds = excluded.duration_seconds, completed = 1, updated_at = excluded.updated_at ` - _, err := db.Exec(query, profileID, mediaItemID, duration, duration, now) + _, err := db.Exec(query, profileID, mediaItemID, duration, now) if err != nil { return fmt.Errorf("marking watched: %w", err) } @@ -207,8 +217,10 @@ func MarkProgressBatch(db *sql.DB, profileID string, mediaItemIDs []string, upda VALUES (?, ?, 0, 0, 1, ?) ON CONFLICT(profile_id, media_item_id) DO UPDATE SET completed = 1, + position_seconds = 0, updated_at = excluded.updated_at - WHERE watch_progress.completed != 1 OR watch_progress.updated_at < excluded.updated_at + WHERE watch_progress.completed != 1 + OR watch_progress.updated_at < excluded.updated_at `, profileID, mediaItemID, updatedAtText); err != nil { return fmt.Errorf("mark progress batch row: %w", err) } @@ -309,11 +321,14 @@ func ListProgress(db *sql.DB, profileID string, status string, limit, offset int switch status { case "in_progress": + // position_seconds > 0 (not completed = 0): completed rows hold + // position 0, so a rewatch of a watched item has completed = 1 with + // a live resume point and belongs in Continue Watching. query = ` SELECT profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at, last_file_id, last_resolution, last_hdr, last_codec_video, last_edition_key FROM watch_progress - WHERE profile_id = ? AND completed = 0 + WHERE profile_id = ? AND position_seconds > 0 AND NOT EXISTS ( SELECT 1 FROM hidden_history_items hhi diff --git a/internal/userdb/progress_rewatch_test.go b/internal/userdb/progress_rewatch_test.go new file mode 100644 index 00000000..841a3cdb --- /dev/null +++ b/internal/userdb/progress_rewatch_test.go @@ -0,0 +1,298 @@ +package userdb + +import ( + "database/sql" + "testing" + "time" + + "github.com/Silo-Server/silo-server/internal/userstore" +) + +func newRewatchTestDB(t *testing.T) *sql.DB { + t.Helper() + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + t.Cleanup(func() { _ = db.Close() }) + if err := InitSchema(db); err != nil { + t.Fatalf("InitSchema: %v", err) + } + return db +} + +func rewatchGetProgress(t *testing.T, db *sql.DB, profileID, itemID string) *WatchProgress { + t.Helper() + wp, err := GetProgress(db, profileID, itemID) + if err != nil { + t.Fatalf("GetProgress: %v", err) + } + if wp == nil { + t.Fatalf("GetProgress(%s) = nil, want row", itemID) + } + return wp +} + +func rewatchInProgressIDs(t *testing.T, db *sql.DB, profileID string) []string { + t.Helper() + entries, err := ListProgress(db, profileID, "in_progress", 50, 0) + if err != nil { + t.Fatalf("ListProgress: %v", err) + } + ids := make([]string, 0, len(entries)) + for _, e := range entries { + ids = append(ids, e.MediaItemID) + } + return ids +} + +// TestProgressRewatchLifecycle pins the watch → complete → rewatch → complete +// cycle of the position-based Continue Watching model: completion resets the +// resume point to 0 and latches completed; a later heartbeat re-enters the +// in-progress set through plain MAX while completed stays true; finishing the +// rewatch resets the resume point again. +func TestProgressRewatchLifecycle(t *testing.T) { + db := newRewatchTestDB(t) + th := userstore.ProgressThresholds{WatchedPct: 90, MinResumePct: 5} + + // First watch in progress. + if err := UpdateProgress(db, "p", "m", 600, 7200, th); err != nil { + t.Fatalf("UpdateProgress(first watch): %v", err) + } + wp := rewatchGetProgress(t, db, "p", "m") + if wp.Completed || wp.PositionSeconds != 600 { + t.Fatalf("first watch: completed=%v pos=%v, want false/600", wp.Completed, wp.PositionSeconds) + } + if ids := rewatchInProgressIDs(t, db, "p"); len(ids) != 1 || ids[0] != "m" { + t.Fatalf("in_progress = %v, want [m]", ids) + } + + // Completion: watched latch set, resume point cleared, leaves Continue Watching. + if err := UpdateProgress(db, "p", "m", 7000, 7200, th); err != nil { + t.Fatalf("UpdateProgress(completion): %v", err) + } + wp = rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("completion: completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + if ids := rewatchInProgressIDs(t, db, "p"); len(ids) != 0 { + t.Fatalf("in_progress after completion = %v, want empty", ids) + } + + // Rewatch: re-enters Continue Watching, watched state survives. + if err := UpdateProgress(db, "p", "m", 900, 7200, th); err != nil { + t.Fatalf("UpdateProgress(rewatch): %v", err) + } + wp = rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 900 { + t.Fatalf("rewatch: completed=%v pos=%v, want true/900", wp.Completed, wp.PositionSeconds) + } + if ids := rewatchInProgressIDs(t, db, "p"); len(ids) != 1 || ids[0] != "m" { + t.Fatalf("in_progress during rewatch = %v, want [m]", ids) + } + + // Rewatch completes: resume point cleared again, still watched. + if err := UpdateProgress(db, "p", "m", 7100, 7200, th); err != nil { + t.Fatalf("UpdateProgress(rewatch completion): %v", err) + } + wp = rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("rewatch completion: completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + if ids := rewatchInProgressIDs(t, db, "p"); len(ids) != 0 { + t.Fatalf("in_progress after rewatch completion = %v, want empty", ids) + } +} + +func TestProgressRewatchGuards(t *testing.T) { + th := userstore.ProgressThresholds{WatchedPct: 90, MinResumePct: 5} + + t.Run("zero position heartbeat does not disturb watched item", func(t *testing.T) { + db := newRewatchTestDB(t) + if err := MarkWatched(db, "p", "m", 7200); err != nil { + t.Fatalf("MarkWatched: %v", err) + } + // Opening a watched item and backing out reports position 0. + if err := UpdateProgress(db, "p", "m", 0, 7200, th); err != nil { + t.Fatalf("UpdateProgress(zero): %v", err) + } + wp := rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("got completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + if ids := rewatchInProgressIDs(t, db, "p"); len(ids) != 0 { + t.Fatalf("in_progress = %v, want empty", ids) + } + }) + + t.Run("below min-resume heartbeat is discarded", func(t *testing.T) { + db := newRewatchTestDB(t) + if err := MarkWatched(db, "p", "m", 7200); err != nil { + t.Fatalf("MarkWatched: %v", err) + } + // 100/7200 = 1.4% < 5% floor: no write, no resume entry. + if err := UpdateProgress(db, "p", "m", 100, 7200, th); err != nil { + t.Fatalf("UpdateProgress(below floor): %v", err) + } + wp := rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("got completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + }) + + t.Run("mark watched cancels rewatch resume point", func(t *testing.T) { + db := newRewatchTestDB(t) + if err := MarkWatched(db, "p", "m", 7200); err != nil { + t.Fatalf("MarkWatched: %v", err) + } + if err := UpdateProgress(db, "p", "m", 900, 7200, th); err != nil { + t.Fatalf("UpdateProgress(rewatch): %v", err) + } + if err := MarkWatched(db, "p", "m", 7200); err != nil { + t.Fatalf("MarkWatched(again): %v", err) + } + wp := rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("got completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + }) + + t.Run("mark progress batch clears stale resume points", func(t *testing.T) { + db := newRewatchTestDB(t) + // In-progress row at 40%, then series-level mark played. + if err := UpdateProgress(db, "p", "ep", 2880, 7200, th); err != nil { + t.Fatalf("UpdateProgress: %v", err) + } + if err := MarkProgressBatch(db, "p", []string{"ep"}, time.Time{}); err != nil { + t.Fatalf("MarkProgressBatch: %v", err) + } + wp := rewatchGetProgress(t, db, "p", "ep") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("got completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + if ids := rewatchInProgressIDs(t, db, "p"); len(ids) != 0 { + t.Fatalf("in_progress = %v, want empty", ids) + } + }) + + t.Run("playback stop mid-rewatch keeps the watched latch", func(t *testing.T) { + db := newRewatchTestDB(t) + if err := MarkWatched(db, "p", "m", 7200); err != nil { + t.Fatalf("MarkWatched: %v", err) + } + // RecordPlaybackStop persists the final position via SetProgress with + // completed=false (below the watched threshold). The latch must hold. + if err := SetProgress(db, "p", "m", 2880, 7200, th); err != nil { + t.Fatalf("SetProgress: %v", err) + } + wp := rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 2880 { + t.Fatalf("got completed=%v pos=%v, want true/2880 (latch + resume point)", wp.Completed, wp.PositionSeconds) + } + }) + + t.Run("set progress if newer keeps the watched latch", func(t *testing.T) { + db := newRewatchTestDB(t) + if err := MarkWatched(db, "p", "m", 7200); err != nil { + t.Fatalf("MarkWatched: %v", err) + } + wrote, err := SetProgressIfNewer(db, "p", "m", 1800, 7200, false, time.Now().UTC().Add(time.Hour)) + if err != nil { + t.Fatalf("SetProgressIfNewer: %v", err) + } + if !wrote { + t.Fatalf("SetProgressIfNewer wrote = false, want true") + } + wp := rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 1800 { + t.Fatalf("got completed=%v pos=%v, want true/1800 (latch + resume point)", wp.Completed, wp.PositionSeconds) + } + }) + + t.Run("stale mark batch does not zero a newer rewatch", func(t *testing.T) { + db := newRewatchTestDB(t) + if err := MarkWatched(db, "p", "ep", 7200); err != nil { + t.Fatalf("MarkWatched: %v", err) + } + if err := UpdateProgress(db, "p", "ep", 900, 7200, th); err != nil { + t.Fatalf("UpdateProgress(rewatch): %v", err) + } + // A delayed batch mark carrying an old timestamp must not clobber the + // live rewatch resume point. + if err := MarkProgressBatch(db, "p", []string{"ep"}, time.Now().UTC().Add(-time.Hour)); err != nil { + t.Fatalf("MarkProgressBatch(stale): %v", err) + } + wp := rewatchGetProgress(t, db, "p", "ep") + if !wp.Completed || wp.PositionSeconds != 900 { + t.Fatalf("got completed=%v pos=%v, want true/900 (stale batch ignored)", wp.Completed, wp.PositionSeconds) + } + }) + + t.Run("set progress at completed normalizes position to zero", func(t *testing.T) { + db := newRewatchTestDB(t) + // Import path (trakt/jellyfin) reporting a completed watch with a + // raw end-of-file position. + if err := SetProgressAt(db, "p", "m", 7200, 7200, true, time.Time{}); err != nil { + t.Fatalf("SetProgressAt: %v", err) + } + wp := rewatchGetProgress(t, db, "p", "m") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("got completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + }) +} + +// TestMigrateToV11ResetsCompletedPositions verifies the v11 userdb migration: +// an existing DB (user_version 10) gets its legacy completed rows (position +// pinned to duration) reset to 0, and the version gate prevents re-runs from +// wiping rewatch state. +func TestMigrateToV11ResetsCompletedPositions(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + t.Cleanup(func() { _ = db.Close() }) + if err := InitSchema(db); err != nil { + t.Fatalf("InitSchema: %v", err) + } + + // Simulate an existing pre-v11 database: schema already at v10 with a + // legacy completed row written under the old model. + if _, err := db.Exec("PRAGMA user_version = 10"); err != nil { + t.Fatalf("set user_version: %v", err) + } + if _, err := db.Exec(` + INSERT INTO watch_progress (profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) + VALUES ('p', 'legacy', 7200, 7200, 1, '2026-01-01T00:00:00Z')`); err != nil { + t.Fatalf("seed legacy row: %v", err) + } + + if err := runMigrations(db); err != nil { + t.Fatalf("runMigrations: %v", err) + } + wp := rewatchGetProgress(t, db, "p", "legacy") + if !wp.Completed || wp.PositionSeconds != 0 { + t.Fatalf("after migration: completed=%v pos=%v, want true/0", wp.Completed, wp.PositionSeconds) + } + version, err := userVersion(db) + if err != nil { + t.Fatalf("userVersion: %v", err) + } + if version != schemaVersion { + t.Fatalf("user_version = %d, want %d", version, schemaVersion) + } + + // A rewatch in flight must survive subsequent runs (the version gate + // blocks re-runs). + th := userstore.ProgressThresholds{WatchedPct: 90, MinResumePct: 5} + if err := UpdateProgress(db, "p", "legacy", 900, 7200, th); err != nil { + t.Fatalf("UpdateProgress(rewatch): %v", err) + } + if err := runMigrations(db); err != nil { + t.Fatalf("runMigrations(again): %v", err) + } + wp = rewatchGetProgress(t, db, "p", "legacy") + if !wp.Completed || wp.PositionSeconds != 900 { + t.Fatalf("after re-run: completed=%v pos=%v, want true/900 (rewatch preserved)", wp.Completed, wp.PositionSeconds) + } +} diff --git a/internal/userstore/pgstore/progress.go b/internal/userstore/pgstore/progress.go index 7556c320..e372836f 100644 --- a/internal/userstore/pgstore/progress.go +++ b/internal/userstore/pgstore/progress.go @@ -58,13 +58,19 @@ func (s *PostgresUserStore) UpdateProgress(ctx context.Context, profileID, media completed := false if duration > 0 && position/duration > userstore.WatchedFraction(thresholds.WatchedPct) { completed = true - position = duration // match MarkWatched() — reset so no stale resume point + position = 0 // match MarkWatched() — completed rows hold no resume point } + // `completed` is a one-way watched latch; position resets to 0 on + // completion so `position_seconds > 0` means "has a resume point". A + // rewatch heartbeat on a completed row therefore re-enters Continue + // Watching through plain GREATEST (stored position is 0) while the + // watched flag survives. _, err := s.pool.Exec(ctx, ` INSERT INTO user_watch_progress (user_id, profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT(user_id, profile_id, media_item_id) DO UPDATE SET - position_seconds = GREATEST(excluded.position_seconds, user_watch_progress.position_seconds), + position_seconds = CASE WHEN excluded.completed THEN 0 + ELSE GREATEST(excluded.position_seconds, user_watch_progress.position_seconds) END, duration_seconds = excluded.duration_seconds, completed = CASE WHEN excluded.completed THEN TRUE ELSE user_watch_progress.completed END, @@ -85,7 +91,7 @@ func (s *PostgresUserStore) SetProgress(ctx context.Context, profileID, mediaIte completed := false if duration > 0 && position/duration > userstore.WatchedFraction(thresholds.WatchedPct) { completed = true - position = duration // match MarkWatched() — reset so no stale resume point + position = 0 // match MarkWatched() — completed rows hold no resume point } _, err := s.pool.Exec(ctx, ` INSERT INTO user_watch_progress (user_id, profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) @@ -93,7 +99,7 @@ func (s *PostgresUserStore) SetProgress(ctx context.Context, profileID, mediaIte ON CONFLICT(user_id, profile_id, media_item_id) DO UPDATE SET position_seconds = excluded.position_seconds, duration_seconds = excluded.duration_seconds, - completed = excluded.completed, + completed = user_watch_progress.completed OR excluded.completed, updated_at = excluded.updated_at`, s.userID, profileID, mediaItemID, position, duration, completed, now, ) @@ -110,8 +116,8 @@ func (s *PostgresUserStore) SetProgressAt(ctx context.Context, profileID, mediaI if duration < 0 { duration = 0 } - if completed && duration > 0 { - position = duration + if completed { + position = 0 } if updatedAt.IsZero() { updatedAt = time.Now().UTC() @@ -130,7 +136,7 @@ func (s *PostgresUserStore) SetProgressAt(ctx context.Context, profileID, mediaI ON CONFLICT(user_id, profile_id, media_item_id) DO UPDATE SET position_seconds = excluded.position_seconds, duration_seconds = excluded.duration_seconds, - completed = excluded.completed, + completed = user_watch_progress.completed OR excluded.completed, updated_at = excluded.updated_at`, s.userID, profileID, mediaItemID, position, duration, completed, updatedAt.UTC(), ) @@ -147,8 +153,8 @@ func (s *PostgresUserStore) SetProgressIfNewer(ctx context.Context, profileID, m if duration < 0 { duration = 0 } - if completed && duration > 0 { - position = duration + if completed { + position = 0 } if updatedAt.IsZero() { updatedAt = time.Now().UTC() @@ -186,13 +192,13 @@ func (s *PostgresUserStore) MarkWatched(ctx context.Context, profileID, mediaIte now := nowUTC() _, err := s.pool.Exec(ctx, ` INSERT INTO user_watch_progress (user_id, profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at) - VALUES ($1, $2, $3, $4, $5, TRUE, $6) + VALUES ($1, $2, $3, 0, $4, TRUE, $5) ON CONFLICT(user_id, profile_id, media_item_id) DO UPDATE SET - position_seconds = excluded.position_seconds, + position_seconds = 0, duration_seconds = excluded.duration_seconds, completed = TRUE, updated_at = excluded.updated_at`, - s.userID, profileID, mediaItemID, duration, duration, now, + s.userID, profileID, mediaItemID, duration, now, ) if err != nil { return fmt.Errorf("marking watched: %w", err) @@ -230,6 +236,7 @@ func (s *PostgresUserStore) MarkProgressBatch(ctx context.Context, profileID str FROM unnest($3::text[]) AS mid ON CONFLICT (user_id, profile_id, media_item_id) DO UPDATE SET completed = TRUE, + position_seconds = 0, updated_at = EXCLUDED.updated_at WHERE user_watch_progress.completed IS DISTINCT FROM TRUE OR user_watch_progress.updated_at < EXCLUDED.updated_at`, @@ -304,11 +311,14 @@ func (s *PostgresUserStore) ListProgress(ctx context.Context, profileID, status switch status { case "in_progress": + // position_seconds > 0 (not completed = FALSE): completed rows hold + // position 0, so a rewatch of a watched item has completed = TRUE with + // a live resume point and belongs in Continue Watching. query = ` SELECT profile_id, media_item_id, position_seconds, duration_seconds, completed, updated_at, last_file_id, last_resolution, last_hdr, last_codec_video, last_edition_key FROM user_watch_progress - WHERE user_id = $1 AND profile_id = $2 AND completed = FALSE + WHERE user_id = $1 AND profile_id = $2 AND position_seconds > 0 AND NOT EXISTS ( SELECT 1 FROM user_history_hidden_items hhi diff --git a/internal/userstore/storetest/suite.go b/internal/userstore/storetest/suite.go index c66bec53..e1bc0363 100644 --- a/internal/userstore/storetest/suite.go +++ b/internal/userstore/storetest/suite.go @@ -247,7 +247,7 @@ func testProgress(t *testing.T, newStore func(t *testing.T) userstore.UserStore) t.Errorf("PositionSeconds after SetProgress backward = %v, want 500", wp.PositionSeconds) } - // Completion flag: >90% + // Completion flag: >90%. Completed rows hold no resume point. if err := store.SetProgress(ctx, "p1", "movie-2", 6500, 7200, noThreshold); err != nil { t.Fatalf("SetProgress(near end): %v", err) } @@ -255,6 +255,9 @@ func testProgress(t *testing.T, newStore func(t *testing.T) userstore.UserStore) if !wp.Completed { t.Error("Expected Completed=true for >90% progress") } + if wp.PositionSeconds != 0 { + t.Errorf("PositionSeconds after completion = %v, want 0", wp.PositionSeconds) + } // Min-resume threshold: below 5% should be silently discarded if err := store.SetProgress(ctx, "p1", "movie-tiny", 10, 7200, noThreshold); err != nil { @@ -486,8 +489,8 @@ func testProgress(t *testing.T, newStore func(t *testing.T) userstore.UserStore) if !wp.Completed { t.Error("MarkWatched should create a completed progress row") } - if wp.PositionSeconds != 5400 || wp.DurationSeconds != 5400 { - t.Errorf("MarkWatched stored position=%v duration=%v, want 5400/5400", wp.PositionSeconds, wp.DurationSeconds) + if wp.PositionSeconds != 0 || wp.DurationSeconds != 5400 { + t.Errorf("MarkWatched stored position=%v duration=%v, want 0/5400", wp.PositionSeconds, wp.DurationSeconds) } if err := store.ClearProgress(ctx, "p1", "movie-3"); err != nil { diff --git a/migrations/sql/20260607131954_request_router_connections.sql b/migrations/sql/20260607131954_request_router_connections.sql new file mode 100644 index 00000000..b3977a79 --- /dev/null +++ b/migrations/sql/20260607131954_request_router_connections.sql @@ -0,0 +1,41 @@ +-- +goose Up +-- +goose StatementBegin +-- NOTE: existing rows get installation_id = NULL because the plugin install id is +-- not known at migration time. Such connections are treated as "not bound to a +-- plugin installation" and are skipped during fulfillment until an admin re-saves +-- the connection to bind it to an installed request_router plugin. +ALTER TABLE public.request_integrations + ADD COLUMN IF NOT EXISTS capability_id text NOT NULL DEFAULT 'request_router.v1', + ADD COLUMN IF NOT EXISTS installation_id integer, + ADD COLUMN IF NOT EXISTS supported_media_types text[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS plugin_config jsonb NOT NULL DEFAULT '{}'::jsonb; + +-- Fold the arr-specific typed columns into the generic plugin_config blob. +-- service_kind comes from the old `kind`; supported_media_types is derived from it. +UPDATE public.request_integrations +SET plugin_config = jsonb_strip_nulls( + coalesce(options, '{}'::jsonb) || jsonb_build_object( + 'service_kind', kind, + 'root_folder', root_folder, + 'quality_profile_id', quality_profile_id, + 'tags', to_jsonb(tags), + 'is_4k', is_4k, + 'is_default', is_default, + 'is_default_4k', is_default_4k, + 'anime_enabled', anime_enabled, + 'anime_quality_profile_id', anime_quality_profile_id, + 'anime_root_folder', anime_root_folder, + 'anime_tags', to_jsonb(anime_tags) + )), + supported_media_types = CASE WHEN kind = 'sonarr' THEN ARRAY['series'] ELSE ARRAY['movie'] END +WHERE plugin_config = '{}'::jsonb; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.request_integrations + DROP COLUMN IF EXISTS capability_id, + DROP COLUMN IF EXISTS installation_id, + DROP COLUMN IF EXISTS supported_media_types, + DROP COLUMN IF EXISTS plugin_config; +-- +goose StatementEnd diff --git a/migrations/sql/20260607161414_add_audiobookshelf_compat_enabled.sql b/migrations/sql/20260607161414_add_audiobookshelf_compat_enabled.sql index 1790584f..d99320ea 100644 --- a/migrations/sql/20260607161414_add_audiobookshelf_compat_enabled.sql +++ b/migrations/sql/20260607161414_add_audiobookshelf_compat_enabled.sql @@ -1,13 +1,7 @@ -- +goose Up -- +goose StatementBegin INSERT INTO server_settings (key, value) -VALUES ( - 'audiobookshelf_compat.enabled', - COALESCE( - (SELECT value FROM server_settings WHERE key = 'audiobooks.enabled'), - 'false' - ) -) +VALUES ('audiobookshelf_compat.enabled', 'true') ON CONFLICT (key) DO NOTHING; -- +goose StatementEnd diff --git a/migrations/sql/20260607211831_drop_legacy_request_integration_columns.sql b/migrations/sql/20260607211831_drop_legacy_request_integration_columns.sql new file mode 100644 index 00000000..423d4255 --- /dev/null +++ b/migrations/sql/20260607211831_drop_legacy_request_integration_columns.sql @@ -0,0 +1,46 @@ +-- +goose Up +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_request_integrations_default_per_kind; +DROP INDEX IF EXISTS idx_request_integrations_default4k_per_kind; +ALTER TABLE public.request_integrations + DROP CONSTRAINT IF EXISTS request_integrations_kind_check, + DROP COLUMN IF EXISTS kind, + DROP COLUMN IF EXISTS root_folder, + DROP COLUMN IF EXISTS quality_profile_id, + DROP COLUMN IF EXISTS tags, + DROP COLUMN IF EXISTS is_4k, + DROP COLUMN IF EXISTS is_default, + DROP COLUMN IF EXISTS is_default_4k, + DROP COLUMN IF EXISTS anime_enabled, + DROP COLUMN IF EXISTS anime_quality_profile_id, + DROP COLUMN IF EXISTS anime_root_folder, + DROP COLUMN IF EXISTS anime_tags, + DROP COLUMN IF EXISTS options; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.request_integrations + ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS root_folder text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS quality_profile_id integer, + ADD COLUMN IF NOT EXISTS tags integer[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS is_4k boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS is_default_4k boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS anime_enabled boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS anime_quality_profile_id integer, + ADD COLUMN IF NOT EXISTS anime_root_folder text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS anime_tags integer[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS options jsonb NOT NULL DEFAULT '{}'::jsonb; +-- Best-effort backfill from plugin_config (the source of truth). +UPDATE public.request_integrations SET + kind = COALESCE(plugin_config->>'service_kind', ''), + root_folder = COALESCE(plugin_config->>'root_folder', ''), + quality_profile_id = NULLIF(plugin_config->>'quality_profile_id','')::integer, + is_4k = COALESCE((plugin_config->>'is_4k')::boolean, false), + is_default = COALESCE((plugin_config->>'is_default')::boolean, false), + is_default_4k = COALESCE((plugin_config->>'is_default_4k')::boolean, false), + anime_enabled = COALESCE((plugin_config->>'anime_enabled')::boolean, false), + anime_root_folder = COALESCE(plugin_config->>'anime_root_folder', ''); +-- +goose StatementEnd diff --git a/migrations/sql/20260608000100_ebook_series.sql b/migrations/sql/20260608000100_ebook_series.sql new file mode 100644 index 00000000..461b680e --- /dev/null +++ b/migrations/sql/20260608000100_ebook_series.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE ebook_series ( + content_id TEXT PRIMARY KEY REFERENCES media_items(content_id) ON DELETE CASCADE, + series_name TEXT NOT NULL, + series_index NUMERIC, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX ebook_series_name_lower + ON ebook_series (LOWER(series_name), series_index NULLS LAST); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS ebook_series; +-- +goose StatementEnd diff --git a/migrations/sql/20260608000200_ebook_reader_progress.sql b/migrations/sql/20260608000200_ebook_reader_progress.sql new file mode 100644 index 00000000..d89b68e5 --- /dev/null +++ b/migrations/sql/20260608000200_ebook_reader_progress.sql @@ -0,0 +1,22 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE ebook_reader_progress ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id TEXT NOT NULL, + content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, + file_id INTEGER NOT NULL REFERENCES media_files(id) ON DELETE CASCADE, + location TEXT NOT NULL, + progress DOUBLE PRECISION NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, profile_id, content_id), + CHECK (progress >= 0 AND progress <= 1) +); + +CREATE INDEX ebook_reader_progress_profile_updated + ON ebook_reader_progress (user_id, profile_id, updated_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS ebook_reader_progress; +-- +goose StatementEnd diff --git a/migrations/sql/20260608000300_ebook_reader_state.sql b/migrations/sql/20260608000300_ebook_reader_state.sql new file mode 100644 index 00000000..a44ff0b2 --- /dev/null +++ b/migrations/sql/20260608000300_ebook_reader_state.sql @@ -0,0 +1,41 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE ebook_reader_config ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id TEXT NOT NULL, + content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, profile_id, content_id) +); + +CREATE INDEX ebook_reader_config_profile_updated + ON ebook_reader_config (user_id, profile_id, updated_at DESC); + +CREATE TABLE ebook_reader_annotations ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id TEXT NOT NULL, + content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, + kind TEXT NOT NULL CHECK (kind IN ('highlight', 'note', 'bookmark')), + cfi_range TEXT, + location TEXT, + selected_text TEXT NOT NULL DEFAULT '', + note TEXT NOT NULL DEFAULT '', + style TEXT NOT NULL DEFAULT 'highlight', + color TEXT NOT NULL DEFAULT '#facc15', + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CHECK ((kind = 'bookmark' AND location IS NOT NULL) OR (kind <> 'bookmark' AND cfi_range IS NOT NULL)) +); + +CREATE INDEX ebook_reader_annotations_book + ON ebook_reader_annotations (user_id, profile_id, content_id, updated_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS ebook_reader_annotations; +DROP TABLE IF EXISTS ebook_reader_config; +-- +goose StatementEnd diff --git a/migrations/sql/20260608131649_backfill_request_integration_capability_subid.sql b/migrations/sql/20260608131649_backfill_request_integration_capability_subid.sql new file mode 100644 index 00000000..df33f491 --- /dev/null +++ b/migrations/sql/20260608131649_backfill_request_integration_capability_subid.sql @@ -0,0 +1,51 @@ +-- +goose Up +-- +goose StatementBegin +-- request_integrations.capability_id must carry the capability SUB-ID +-- ("arr"/"seerr"), matching the value the host passes to +-- requireCapability("request_router.v1", id) when dispensing the plugin. Rows +-- created before the fix stored the capability TYPE "request_router.v1", which +-- resolves to no plugin capability (every save/options/fulfill call 500s with +-- "no fulfillment backend configured" / ErrCapabilityNotFound). +-- +-- Backfill the sub-id from each row's bound installation's request_router.v1 +-- capability. Rows with installation_id NULL (created under the pre-plugin +-- direct-to-arr system and never re-bound) are left untouched — an admin +-- re-saves them in Admin -> Requests to bind an installation and set the sub-id. +-- +-- A deterministic correlated subquery (ORDER BY ... LIMIT 1) is used instead of a +-- join so that, in the unusual case where one installation exposes more than one +-- request_router.v1 capability, the same sub-id is chosen for every row rather +-- than letting Postgres pick arbitrarily from the join cross-product. +UPDATE public.request_integrations ri +SET capability_id = ( + SELECT pc.capability_id + FROM public.plugin_capabilities pc + WHERE pc.plugin_installation_id = ri.installation_id + AND pc.capability_type = 'request_router.v1' + ORDER BY pc.capability_id + LIMIT 1 +) +WHERE ri.capability_id = 'request_router.v1' + AND ri.installation_id IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM public.plugin_capabilities pc + WHERE pc.plugin_installation_id = ri.installation_id + AND pc.capability_type = 'request_router.v1' + ); + +-- Drop the misleading column default (the capability type). The application now +-- always supplies the sub-id on insert; the default would silently reintroduce +-- the unresolvable type for any row that omitted capability_id. +ALTER TABLE public.request_integrations + ALTER COLUMN capability_id DROP DEFAULT; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Restore the prior column default. The data backfill is intentionally not +-- reverted: the sub-id is the correct value, and rewriting it back to the +-- capability type would re-break fulfillment. +ALTER TABLE public.request_integrations + ALTER COLUMN capability_id SET DEFAULT 'request_router.v1'; +-- +goose StatementEnd diff --git a/migrations/sql/20260608183000_enable_audiobookshelf_compat_default.sql b/migrations/sql/20260608183000_enable_audiobookshelf_compat_default.sql new file mode 100644 index 00000000..0f5ccc51 --- /dev/null +++ b/migrations/sql/20260608183000_enable_audiobookshelf_compat_default.sql @@ -0,0 +1,28 @@ +-- +goose Up +-- +goose StatementBegin +-- The original ABS compatibility setting copied the stale audiobooks.enabled +-- kill-switch, which defaulted to false. Docker deployments already publish +-- :13378, so keep the listener enabled unless an operator disables it later. +UPDATE server_settings +SET value = 'true' +WHERE key = 'audiobookshelf_compat.enabled' + AND trim(value) = ''; + +INSERT INTO server_settings (key, value) +SELECT 'audiobookshelf_compat.enabled', 'true' +WHERE NOT EXISTS ( + SELECT 1 + FROM server_settings + WHERE key = 'audiobookshelf_compat.enabled' +); + +DELETE FROM server_settings +WHERE key = 'audiobooks.enabled'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +INSERT INTO server_settings (key, value) +VALUES ('audiobooks.enabled', 'false') +ON CONFLICT (key) DO NOTHING; +-- +goose StatementEnd diff --git a/migrations/sql/20260608210000_audiobook_continue_listening_section_title.sql b/migrations/sql/20260608210000_audiobook_continue_listening_section_title.sql new file mode 100644 index 00000000..e2d029c1 --- /dev/null +++ b/migrations/sql/20260608210000_audiobook_continue_listening_section_title.sql @@ -0,0 +1,27 @@ +-- +goose Up +-- +goose StatementBegin +UPDATE page_sections ps +SET title = 'Continue Listening', + updated_at = NOW() +FROM media_folders mf +WHERE ps.scope = 'library' + AND ps.library_id = mf.id + AND lower(mf.type) IN ('audiobook', 'audiobooks') + AND ps.section_type = 'continue_watching' + AND ps.position = 0 + AND ps.title = 'Continue Watching'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE page_sections ps +SET title = 'Continue Watching', + updated_at = NOW() +FROM media_folders mf +WHERE ps.scope = 'library' + AND ps.library_id = mf.id + AND lower(mf.type) IN ('audiobook', 'audiobooks') + AND ps.section_type = 'continue_watching' + AND ps.position = 0 + AND ps.title = 'Continue Listening'; +-- +goose StatementEnd diff --git a/migrations/sql/20260608224625_home_continue_listening_split.sql b/migrations/sql/20260608224625_home_continue_listening_split.sql new file mode 100644 index 00000000..928e3714 --- /dev/null +++ b/migrations/sql/20260608224625_home_continue_listening_split.sql @@ -0,0 +1,102 @@ +-- +goose Up +-- +goose StatementBegin +UPDATE page_sections +SET config = config || '{"continue_type":"listening"}'::jsonb, + updated_at = NOW() +WHERE section_type = 'continue_watching' + AND NOT (config ? 'continue_type') + AND config->>'filter_type' = 'audiobook'; + +UPDATE page_sections ps +SET config = ps.config || '{"continue_type":"listening"}'::jsonb, + updated_at = NOW() +FROM media_folders mf +WHERE ps.scope = 'library' + AND ps.library_id = mf.id + AND lower(mf.type) IN ('audiobook', 'audiobooks') + AND ps.section_type = 'continue_watching' + AND NOT (ps.config ? 'continue_type'); + +UPDATE page_sections +SET config = config || '{"continue_type":"watching"}'::jsonb, + updated_at = NOW() +WHERE section_type = 'continue_watching' + AND NOT (config ? 'continue_type'); + +WITH insert_position AS ( + SELECT COALESCE( + ( + SELECT ps.position + 1 + FROM page_sections ps + WHERE ps.scope = 'home' + AND ps.library_id IS NULL + AND ps.section_type = 'continue_watching' + AND ps.config->>'continue_type' = 'watching' + ORDER BY ps.position ASC + LIMIT 1 + ), + ( + SELECT COALESCE(MAX(position) + 1, 0) + FROM page_sections + WHERE scope = 'home' + AND library_id IS NULL + ) + ) AS position + WHERE EXISTS ( + SELECT 1 + FROM media_folders + WHERE lower(type) IN ('audiobook', 'audiobooks') + ) + AND NOT EXISTS ( + SELECT 1 + FROM page_sections + WHERE scope = 'home' + AND library_id IS NULL + AND section_type = 'continue_watching' + AND config->>'continue_type' = 'listening' + ) +), +shifted AS ( + UPDATE page_sections + SET position = position + 1, + updated_at = NOW() + WHERE scope = 'home' + AND library_id IS NULL + AND position >= (SELECT position FROM insert_position) + RETURNING 1 +) +INSERT INTO page_sections ( + id, scope, library_id, position, section_type, title, featured, + item_limit, config, enabled, created_at, updated_at +) +SELECT + gen_random_uuid()::text, + 'home', + NULL, + position, + 'continue_watching', + 'Continue Listening', + false, + 20, + '{"continue_type":"listening"}'::jsonb, + true, + NOW(), + NOW() +FROM insert_position; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM page_sections +WHERE scope = 'home' + AND library_id IS NULL + AND section_type = 'continue_watching' + AND title = 'Continue Listening' + AND config->>'continue_type' = 'listening'; + +UPDATE page_sections +SET config = config - 'continue_type', + updated_at = NOW() +WHERE section_type = 'continue_watching' + AND config ? 'continue_type'; +-- +goose StatementEnd diff --git a/migrations/sql/20260609222830_audiobook_library_home_redesign.sql b/migrations/sql/20260609222830_audiobook_library_home_redesign.sql new file mode 100644 index 00000000..b8484d3c --- /dev/null +++ b/migrations/sql/20260609222830_audiobook_library_home_redesign.sql @@ -0,0 +1,122 @@ +-- +goose Up +-- +goose StatementBegin +-- Audiobook library Home redesign: the Continue Listening section becomes the +-- featured "Now Listening" resume hero, and a next_in_series row surfaces the +-- next unstarted book in series the profile has finished. +-- +-- Rows touched here carry a marker key in config so the Down migration can +-- revert exactly what this migration changed and nothing else. +-- +-- Mark the first continue-listening section of each audiobook library as +-- featured, but only when the admin has not already featured something else in +-- that library's layout. +WITH target AS ( + SELECT DISTINCT ON (ps.library_id) ps.id + FROM page_sections ps + JOIN media_folders mf ON mf.id = ps.library_id + WHERE ps.scope = 'library' + AND lower(mf.type) IN ('audiobook', 'audiobooks') + AND ps.section_type = 'continue_watching' + AND ps.config->>'continue_type' = 'listening' + AND NOT ps.featured + AND NOT EXISTS ( + SELECT 1 FROM page_sections f + WHERE f.scope = 'library' + AND f.library_id = ps.library_id + AND f.featured + ) + ORDER BY ps.library_id, ps.position ASC +) +UPDATE page_sections ps +SET featured = TRUE, + config = ps.config || '{"featured_by_migration":"20260609222830"}'::jsonb, + updated_at = NOW() +FROM target t +WHERE ps.id = t.id; +-- +goose StatementEnd + +-- +goose StatementBegin +-- Insert a next_in_series section into each audiobook library that lacks one, +-- directly after its continue-listening section (or at the end of the layout +-- when no continue-listening section exists). +WITH anchors AS ( + SELECT DISTINCT ON (ps.library_id) + ps.library_id, + ps.position + 1 AS insert_position + FROM page_sections ps + JOIN media_folders mf ON mf.id = ps.library_id + WHERE ps.scope = 'library' + AND lower(mf.type) IN ('audiobook', 'audiobooks') + AND ps.section_type = 'continue_watching' + AND ps.config->>'continue_type' = 'listening' + ORDER BY ps.library_id, ps.position ASC +), +targets AS ( + SELECT + mf.id AS library_id, + COALESCE( + a.insert_position, + ( + SELECT COALESCE(MAX(p2.position) + 1, 0) + FROM page_sections p2 + WHERE p2.scope = 'library' AND p2.library_id = mf.id + ) + ) AS insert_position + FROM media_folders mf + LEFT JOIN anchors a ON a.library_id = mf.id + WHERE lower(mf.type) IN ('audiobook', 'audiobooks') + AND NOT EXISTS ( + SELECT 1 FROM page_sections e + WHERE e.scope = 'library' + AND e.library_id = mf.id + AND e.section_type = 'next_in_series' + ) +), +shifted AS ( + UPDATE page_sections ps + SET position = ps.position + 1, + updated_at = NOW() + FROM targets t + WHERE ps.scope = 'library' + AND ps.library_id = t.library_id + AND ps.position >= t.insert_position + RETURNING 1 +) +INSERT INTO page_sections ( + id, scope, library_id, position, section_type, title, featured, + item_limit, config, enabled, created_at, updated_at +) +SELECT + gen_random_uuid()::text, + 'library', + t.library_id, + t.insert_position, + 'next_in_series', + 'Next in Your Series', + false, + 20, + '{"seeded_by_migration":"20260609222830"}'::jsonb, + true, + NOW(), + NOW() +FROM targets t; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Revert only the rows the Up migration touched, identified by the marker +-- keys it wrote. Admin-created next_in_series sections and sections featured +-- before (or after) the upgrade are left alone. +DELETE FROM page_sections +WHERE scope = 'library' + AND section_type = 'next_in_series' + AND config->>'seeded_by_migration' = '20260609222830'; + +UPDATE page_sections +SET featured = FALSE, + config = config - 'featured_by_migration', + updated_at = NOW() +WHERE scope = 'library' + AND section_type = 'continue_watching' + AND config->>'featured_by_migration' = '20260609222830'; +-- +goose StatementEnd diff --git a/migrations/sql/20260609224115_reset_completed_progress_position.sql b/migrations/sql/20260609224115_reset_completed_progress_position.sql new file mode 100644 index 00000000..12727a13 --- /dev/null +++ b/migrations/sql/20260609224115_reset_completed_progress_position.sql @@ -0,0 +1,20 @@ +-- +goose Up +-- Watch-progress model change: `completed` is a one-way watched latch and +-- completed rows hold no resume point (position_seconds = 0), mirroring +-- Jellyfin. The Continue Watching predicate becomes position_seconds > 0, so +-- a rewatch of a watched item re-enters the row naturally while keeping its +-- watched state. Legacy completed rows pinned position_seconds to +-- duration_seconds; reset them so they don't surface as phantom resume +-- entries under the new predicate. +UPDATE user_watch_progress +SET position_seconds = 0 +WHERE completed = TRUE + AND position_seconds <> 0; + +-- +goose Down +-- Restore the legacy convention (completed rows pinned to duration) so the +-- old `completed = FALSE` resume predicate doesn't surface watched items. +UPDATE user_watch_progress +SET position_seconds = duration_seconds +WHERE completed = TRUE + AND duration_seconds > 0; diff --git a/migrations/sql/20260610071424_add_ebook_reader_fk_indexes.sql b/migrations/sql/20260610071424_add_ebook_reader_fk_indexes.sql new file mode 100644 index 00000000..23a17218 --- /dev/null +++ b/migrations/sql/20260610071424_add_ebook_reader_fk_indexes.sql @@ -0,0 +1,26 @@ +-- +goose Up +-- +goose StatementBegin +-- The ebook reader tables carry ON DELETE CASCADE foreign keys on content_id +-- (and file_id for progress), but every existing index leads with user_id, so +-- media_items/media_files deletes had to seq-scan them. Plain btree indexes on +-- the FK columns let those cascades use index scans. +CREATE INDEX IF NOT EXISTS ebook_reader_progress_content_id + ON ebook_reader_progress (content_id); + +CREATE INDEX IF NOT EXISTS ebook_reader_progress_file_id + ON ebook_reader_progress (file_id); + +CREATE INDEX IF NOT EXISTS ebook_reader_config_content_id + ON ebook_reader_config (content_id); + +CREATE INDEX IF NOT EXISTS ebook_reader_annotations_content_id + ON ebook_reader_annotations (content_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS ebook_reader_annotations_content_id; +DROP INDEX IF EXISTS ebook_reader_config_content_id; +DROP INDEX IF EXISTS ebook_reader_progress_file_id; +DROP INDEX IF EXISTS ebook_reader_progress_content_id; +-- +goose StatementEnd diff --git a/migrations/sql/20260610080326_backfill_ebook_item_status.sql b/migrations/sql/20260610080326_backfill_ebook_item_status.sql new file mode 100644 index 00000000..9fd3ef18 --- /dev/null +++ b/migrations/sql/20260610080326_backfill_ebook_item_status.sql @@ -0,0 +1,18 @@ +-- +goose Up +-- Ebook items created by the scanner before status handling was fixed were +-- written with status = '' (the upsert bypasses the column default), which +-- disabled the pending -> matched promotion in enrichment and the curated- +-- metadata guard. Rows enrichment already processed become 'matched'; the +-- rest become 'pending' so the next enrichment sweep promotes them normally. +UPDATE media_items +SET status = CASE + WHEN matched_at IS NOT NULL OR last_refreshed IS NOT NULL THEN 'matched' + ELSE 'pending' + END +WHERE type = 'ebook' + AND status = ''; + +-- +goose Down +-- Irreversible data backfill: the pre-migration status ('') carried no +-- information, so there is nothing meaningful to restore. +SELECT 1; diff --git a/migrations/sql/20260610080327_ebook_enrichment_state.sql b/migrations/sql/20260610080327_ebook_enrichment_state.sql new file mode 100644 index 00000000..8f8b1aa3 --- /dev/null +++ b/migrations/sql/20260610080327_ebook_enrichment_state.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- Dedicated failure accounting for the ebook enrichment sweep. Enrichment +-- previously incremented media_items.refresh_failures, which the metadata +-- refresh-debt system also owns (increments on refresh failure, resets on +-- success, reads for debt scoring); the two systems fought over one counter. +CREATE TABLE ebook_enrichment_state ( + content_id text PRIMARY KEY REFERENCES media_items(content_id) ON DELETE CASCADE, + failures integer NOT NULL DEFAULT 0, + updated_at timestamptz NOT NULL DEFAULT now() +); + +-- +goose Down +DROP TABLE ebook_enrichment_state; diff --git a/web/package.json b/web/package.json index d450f60c..c811d9e0 100644 --- a/web/package.json +++ b/web/package.json @@ -23,15 +23,19 @@ "@fontsource/noto-sans-thai": "^5.2.8", "@tanstack/react-query": "^5.90.21", "@tanstack/react-virtual": "^3.13.19", + "@zip.js/zip.js": "^2.8.26", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "embla-carousel": "^8.6.0", "embla-carousel-react": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", + "foliate-js": "file:vendor/foliate-js", "framer-motion": "^12.38.0", "hls.js": "^1.6.15", "jassub": "^2.4.2", "lucide-react": "^0.576.0", + "node-unrar-js": "^2.0.2", + "pdfjs-dist": "^5.7.284", "radix-ui": "^1.4.3", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a6af9a64..563158f1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.19 version: 3.13.22(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@zip.js/zip.js': + specifier: ^2.8.26 + version: 2.8.26 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -52,6 +55,9 @@ importers: embla-carousel-wheel-gestures: specifier: ^8.1.0 version: 8.1.0(embla-carousel@8.6.0) + foliate-js: + specifier: file:vendor/foliate-js + version: file:vendor/foliate-js framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -64,6 +70,12 @@ importers: lucide-react: specifier: ^0.576.0 version: 0.576.0(react@19.2.4) + node-unrar-js: + specifier: ^2.0.2 + version: 2.0.2 + pdfjs-dist: + specifier: ^5.7.284 + version: 5.7.284 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -586,6 +598,81 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/canvas-android-arm64@0.1.100': + resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.100': + resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.100': + resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.100': + resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==} + engines: {node: '>= 10'} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1695,6 +1782,10 @@ packages: '@vitest/utils@4.1.0': resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@zip.js/zip.js@2.8.26': + resolution: {integrity: sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==} + engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} + abslink@1.1.6: resolution: {integrity: sha512-8fQgnUoVSgc1IhOrYzdDY+wTDPktbuYjds2LKf9kWYWKwDnHgXU168gdV3sPei7vevFf5m2fPZ6IQSKZkSjHVg==} @@ -1800,6 +1891,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + construct-style-sheets-polyfill@3.1.0: + resolution: {integrity: sha512-HBLKP0chz8BAY6rBdzda11c3wAZeCZ+kIG4weVC2NM3AXzxx09nhe8t0SQNdloAvg5GLuHwq/0SPOOSPvtCcKw==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2006,6 +2100,9 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + foliate-js@file:vendor/foliate-js: + resolution: {directory: vendor/foliate-js, type: directory} + framer-motion@12.38.0: resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} peerDependencies: @@ -2359,6 +2456,10 @@ packages: node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-unrar-js@2.0.2: + resolution: {integrity: sha512-hLNmoJzqaKJnod8yiTVGe9hnlNRHotUi0CreSv/8HtfRi/3JnRC8DvsmKfeGGguRjTEulhZK6zXX5PXoVuDZ2w==} + engines: {node: '>=10.0.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2396,6 +2497,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pdfjs-dist@5.7.284: + resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==} + engines: {node: '>=22.13.0 || >=24'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3271,6 +3376,54 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/canvas-android-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas@0.1.100': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.100 + '@napi-rs/canvas-darwin-arm64': 0.1.100 + '@napi-rs/canvas-darwin-x64': 0.1.100 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.100 + '@napi-rs/canvas-linux-arm64-musl': 0.1.100 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-musl': 0.1.100 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.100 + '@napi-rs/canvas-win32-x64-msvc': 0.1.100 + optional: true + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -4406,6 +4559,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@zip.js/zip.js@2.8.26': {} + abslink@1.1.6: {} acorn-jsx@5.3.2(acorn@8.16.0): @@ -4495,6 +4650,8 @@ snapshots: concat-map@0.0.1: {} + construct-style-sheets-polyfill@3.1.0: {} + convert-source-map@2.0.0: {} cookie@1.1.1: {} @@ -4721,6 +4878,10 @@ snapshots: flatted@3.4.2: {} + foliate-js@file:vendor/foliate-js: + dependencies: + construct-style-sheets-polyfill: 3.1.0 + framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: motion-dom: 12.38.0 @@ -4997,6 +5158,8 @@ snapshots: node-releases@2.0.36: {} + node-unrar-js@2.0.2: {} + object-assign@4.1.1: {} obug@2.1.1: {} @@ -5032,6 +5195,10 @@ snapshots: pathe@2.0.3: {} + pdfjs-dist@5.7.284: + optionalDependencies: + '@napi-rs/canvas': 0.1.100 + picocolors@1.1.1: {} picomatch@4.0.4: {} diff --git a/web/public/vendor/pdfjs/annotation_layer_builder.css b/web/public/vendor/pdfjs/annotation_layer_builder.css new file mode 100644 index 00000000..c1c2a8d5 --- /dev/null +++ b/web/public/vendor/pdfjs/annotation_layer_builder.css @@ -0,0 +1,407 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.annotationLayer { + color-scheme: only light; + + --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color: Highlight; + --input-focus-outline: 1px solid Canvas; + --input-unfocused-border-color: transparent; + --input-disabled-border-color: transparent; + --input-hover-border-color: black; + --link-outline: none; + + @media screen and (forced-colors: active) { + --input-focus-border-color: CanvasText; + --input-unfocused-border-color: ActiveText; + --input-disabled-border-color: GrayText; + --input-hover-border-color: Highlight; + --link-outline: 1.5px solid LinkText; + + .textWidgetAnnotation :is(input, textarea):required, + .choiceWidgetAnnotation select:required, + .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required { + outline: 1.5px solid selectedItem; + } + + .linkAnnotation { + outline: var(--link-outline); + + &:hover { + backdrop-filter: var(--hcm-highlight-filter); + } + + & > a:hover { + opacity: 0 !important; + background: none !important; + box-shadow: none; + } + } + + .popupAnnotation .popup { + outline: calc(1.5px * var(--total-scale-factor)) solid CanvasText !important; + background-color: ButtonFace !important; + color: ButtonText !important; + } + + .highlightArea:hover::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + backdrop-filter: var(--hcm-highlight-filter); + content: ""; + pointer-events: none; + } + + .popupAnnotation.focused .popup { + outline: calc(3px * var(--total-scale-factor)) solid Highlight !important; + } + } + + position: absolute; + top: 0; + left: 0; + pointer-events: none; + transform-origin: 0 0; + + &[data-main-rotation="90"] .norotate { + transform: rotate(270deg) translateX(-100%); + } + &[data-main-rotation="180"] .norotate { + transform: rotate(180deg) translate(-100%, -100%); + } + &[data-main-rotation="270"] .norotate { + transform: rotate(90deg) translateY(-100%); + } + + &.disabled { + section, + .popup { + pointer-events: none; + } + } + + .annotationContent { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + + &.freetext { + background: transparent; + border: none; + inset: 0; + overflow: visible; + white-space: nowrap; + font: 10px sans-serif; + line-height: 1.35; + } + } + + section { + position: absolute; + text-align: initial; + pointer-events: auto; + box-sizing: border-box; + transform-origin: 0 0; + user-select: none; + + &:has(div.annotationContent) { + canvas.annotationContent { + display: none; + } + } + + .overlaidText { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + display: inline-block; + overflow: hidden; + } + } + + .textLayer.selecting ~ & section { + pointer-events: none; + } + + :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a { + position: absolute; + font-size: 1em; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) + > a:hover { + opacity: 0.2; + background-color: rgb(255 255 0); + } + + .linkAnnotation.hasBorder:hover { + background-color: rgb(255 255 0 / 0.2); + } + + .hasBorder { + background-size: 100% 100%; + } + + .textAnnotation img { + position: absolute; + cursor: pointer; + width: 100%; + height: 100%; + top: 0; + left: 0; + } + + .textWidgetAnnotation :is(input, textarea), + .choiceWidgetAnnotation select, + .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { + background-image: var(--annotation-unfocused-field-background); + border: 2px solid var(--input-unfocused-border-color); + box-sizing: border-box; + font: calc(9px * var(--total-scale-factor)) sans-serif; + height: 100%; + margin: 0; + vertical-align: top; + width: 100%; + } + + .textWidgetAnnotation :is(input, textarea):required, + .choiceWidgetAnnotation select:required, + .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required { + outline: 1.5px solid red; + } + + .choiceWidgetAnnotation select option { + padding: 0; + } + + .buttonWidgetAnnotation.radioButton input { + border-radius: 50%; + } + + .textWidgetAnnotation textarea { + resize: none; + } + + .textWidgetAnnotation :is(input, textarea)[disabled], + .choiceWidgetAnnotation select[disabled], + .buttonWidgetAnnotation:is(.checkBox, .radioButton) input[disabled] { + background: none; + border: 2px solid var(--input-disabled-border-color); + cursor: not-allowed; + } + + .textWidgetAnnotation :is(input, textarea):hover, + .choiceWidgetAnnotation select:hover, + .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:hover { + border: 2px solid var(--input-hover-border-color); + } + .textWidgetAnnotation :is(input, textarea):hover, + .choiceWidgetAnnotation select:hover, + .buttonWidgetAnnotation.checkBox input:hover { + border-radius: 2px; + } + + .textWidgetAnnotation :is(input, textarea):focus, + .choiceWidgetAnnotation select:focus { + background: none; + border: 2px solid var(--input-focus-border-color); + border-radius: 2px; + outline: var(--input-focus-outline); + } + + .buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus { + background-image: none; + background-color: transparent; + } + + .buttonWidgetAnnotation.checkBox :focus { + border: 2px solid var(--input-focus-border-color); + border-radius: 2px; + outline: var(--input-focus-outline); + } + + .buttonWidgetAnnotation.radioButton :focus { + border: 2px solid var(--input-focus-border-color); + outline: var(--input-focus-outline); + } + + .buttonWidgetAnnotation.checkBox input:checked::before, + .buttonWidgetAnnotation.checkBox input:checked::after, + .buttonWidgetAnnotation.radioButton input:checked::before { + background-color: CanvasText; + content: ""; + display: block; + position: absolute; + } + + .buttonWidgetAnnotation.checkBox input:checked::before, + .buttonWidgetAnnotation.checkBox input:checked::after { + height: 80%; + left: 45%; + width: 1px; + } + + .buttonWidgetAnnotation.checkBox input:checked::before { + transform: rotate(45deg); + } + + .buttonWidgetAnnotation.checkBox input:checked::after { + transform: rotate(-45deg); + } + + .buttonWidgetAnnotation.radioButton input:checked::before { + border-radius: 50%; + height: 50%; + left: 25%; + top: 25%; + width: 50%; + } + + .textWidgetAnnotation input.comb { + font-family: monospace; + padding-left: 2px; + padding-right: 0; + } + + .textWidgetAnnotation input.comb:focus { + /* + * Letter spacing is placed on the right side of each character. Hence, the + * letter spacing of the last character may be placed outside the visible + * area, causing horizontal scrolling. We avoid this by extending the width + * when the element has focus and revert this when it loses focus. + */ + width: 103%; + } + + .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { + appearance: none; + } + + .fileAttachmentAnnotation .popupTriggerArea { + height: 100%; + width: 100%; + } + + .popupAnnotation { + position: absolute; + font-size: calc(9px * var(--total-scale-factor)); + pointer-events: none; + width: max-content; + max-width: 45%; + height: auto; + } + + .popup { + background-color: rgb(255 255 153); + color: black; + box-shadow: 0 calc(2px * var(--total-scale-factor)) + calc(5px * var(--total-scale-factor)) rgb(136 136 136); + border-radius: calc(2px * var(--total-scale-factor)); + outline: 1.5px solid rgb(255 255 74); + padding: calc(6px * var(--total-scale-factor)); + cursor: pointer; + font: message-box; + white-space: normal; + word-wrap: break-word; + pointer-events: auto; + user-select: text; + } + + .popupAnnotation.focused .popup { + outline-width: 3px; + } + + .popup * { + font-size: calc(9px * var(--total-scale-factor)); + } + + .popup > .header { + display: inline-block; + } + + .popup > .header > .title { + display: inline; + font-weight: bold; + } + + .popup > .header .popupDate { + display: inline-block; + margin-left: calc(5px * var(--total-scale-factor)); + width: fit-content; + } + + .popupContent { + border-top: 1px solid rgb(51 51 51); + margin-top: calc(2px * var(--total-scale-factor)); + padding-top: calc(2px * var(--total-scale-factor)); + } + + .richText > * { + white-space: pre-wrap; + font-size: calc(9px * var(--total-scale-factor)); + } + + .popupTriggerArea { + cursor: pointer; + + &:hover { + backdrop-filter: var(--hcm-highlight-filter); + } + } + + section svg { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + } + + .annotationTextContent { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + color: transparent; + user-select: none; + pointer-events: none; + + span { + width: 100%; + display: inline-block; + } + } + + svg.quadrilateralsContainer { + contain: strict; + width: 0; + height: 0; + position: absolute; + top: 0; + left: 0; + z-index: -1; + } +} diff --git a/web/public/vendor/pdfjs/cmaps/78-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/78-EUC-H.bcmap new file mode 100644 index 00000000..2655fc70 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/78-EUC-V.bcmap new file mode 100644 index 00000000..f1ed8538 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78-H.bcmap b/web/public/vendor/pdfjs/cmaps/78-H.bcmap new file mode 100644 index 00000000..39e89d33 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap new file mode 100644 index 00000000..e4167cb5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap new file mode 100644 index 00000000..50b1646e Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78-V.bcmap b/web/public/vendor/pdfjs/cmaps/78-V.bcmap new file mode 100644 index 00000000..d7af99b5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap new file mode 100644 index 00000000..37077d01 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap new file mode 100644 index 00000000..acf23231 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap new file mode 100644 index 00000000..2359bc52 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap new file mode 100644 index 00000000..af829382 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap new file mode 100644 index 00000000..780549de Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap new file mode 100644 index 00000000..bfd3119c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap new file mode 100644 index 00000000..25ef14ab Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap new file mode 100644 index 00000000..02f713bb Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap new file mode 100644 index 00000000..d08e0cc5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Add-H.bcmap b/web/public/vendor/pdfjs/cmaps/Add-H.bcmap new file mode 100644 index 00000000..59442aca Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Add-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap new file mode 100644 index 00000000..a3065e44 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap new file mode 100644 index 00000000..040014cf Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Add-V.bcmap b/web/public/vendor/pdfjs/cmaps/Add-V.bcmap new file mode 100644 index 00000000..2f816d32 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Add-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap new file mode 100644 index 00000000..88ec04af Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap new file mode 100644 index 00000000..03a50147 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap new file mode 100644 index 00000000..2aa95141 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap new file mode 100644 index 00000000..86d8b8c7 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap new file mode 100644 index 00000000..f50fc6c1 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap new file mode 100644 index 00000000..6caf4a83 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap new file mode 100644 index 00000000..b77fb070 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap new file mode 100644 index 00000000..69d79a2c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap new file mode 100644 index 00000000..36101083 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap new file mode 100644 index 00000000..707bb106 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap new file mode 100644 index 00000000..f7648cc3 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap new file mode 100644 index 00000000..85214589 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap new file mode 100644 index 00000000..e40c63ab Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap new file mode 100644 index 00000000..d7623b50 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap new file mode 100644 index 00000000..75865259 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap new file mode 100644 index 00000000..f0e94ec1 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap new file mode 100644 index 00000000..dad42c5a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap new file mode 100644 index 00000000..090819a0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap new file mode 100644 index 00000000..087dfc15 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap new file mode 100644 index 00000000..46aa9bff Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap new file mode 100644 index 00000000..5b4b65cc Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap new file mode 100644 index 00000000..e77d699a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap new file mode 100644 index 00000000..128a1410 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap new file mode 100644 index 00000000..cef1a998 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap new file mode 100644 index 00000000..11ffa36d Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap new file mode 100644 index 00000000..3172308c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap new file mode 100644 index 00000000..f3371c0c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/B5-H.bcmap new file mode 100644 index 00000000..beb4d228 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/B5-V.bcmap new file mode 100644 index 00000000..2d4f87d5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/B5pc-H.bcmap b/web/public/vendor/pdfjs/cmaps/B5pc-H.bcmap new file mode 100644 index 00000000..ce001316 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/B5pc-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/B5pc-V.bcmap b/web/public/vendor/pdfjs/cmaps/B5pc-V.bcmap new file mode 100644 index 00000000..73b99ff2 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/B5pc-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap new file mode 100644 index 00000000..61d1d0cb Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap new file mode 100644 index 00000000..1a393a51 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/CNS1-H.bcmap b/web/public/vendor/pdfjs/cmaps/CNS1-H.bcmap new file mode 100644 index 00000000..f738e218 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/CNS1-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/CNS1-V.bcmap b/web/public/vendor/pdfjs/cmaps/CNS1-V.bcmap new file mode 100644 index 00000000..9c3169f0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/CNS1-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/CNS2-H.bcmap b/web/public/vendor/pdfjs/cmaps/CNS2-H.bcmap new file mode 100644 index 00000000..c89b3527 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/CNS2-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/CNS2-V.bcmap b/web/public/vendor/pdfjs/cmaps/CNS2-V.bcmap new file mode 100644 index 00000000..7588cec8 --- /dev/null +++ b/web/public/vendor/pdfjs/cmaps/CNS2-V.bcmap @@ -0,0 +1,3 @@ +àRCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSEáCNS2-H \ No newline at end of file diff --git a/web/public/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap new file mode 100644 index 00000000..cb29415d Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap new file mode 100644 index 00000000..f09aec63 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/ETen-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/ETen-B5-H.bcmap new file mode 100644 index 00000000..c2d77462 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/ETen-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/ETen-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/ETen-B5-V.bcmap new file mode 100644 index 00000000..89bff159 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/ETen-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap new file mode 100644 index 00000000..a7d69db5 --- /dev/null +++ b/web/public/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap @@ -0,0 +1,3 @@ +àRCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSEá ETen-B5-H` ^ \ No newline at end of file diff --git a/web/public/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap new file mode 100644 index 00000000..adc5d618 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/EUC-H.bcmap new file mode 100644 index 00000000..e92ea5b3 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/EUC-V.bcmap new file mode 100644 index 00000000..7a7c1832 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Ext-H.bcmap b/web/public/vendor/pdfjs/cmaps/Ext-H.bcmap new file mode 100644 index 00000000..3b5cde44 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Ext-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap new file mode 100644 index 00000000..ea4d2d97 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap new file mode 100644 index 00000000..3457c277 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Ext-V.bcmap b/web/public/vendor/pdfjs/cmaps/Ext-V.bcmap new file mode 100644 index 00000000..4999ca40 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Ext-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GB-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/GB-EUC-H.bcmap new file mode 100644 index 00000000..e39908b9 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GB-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GB-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/GB-EUC-V.bcmap new file mode 100644 index 00000000..d5be5446 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GB-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GB-H.bcmap b/web/public/vendor/pdfjs/cmaps/GB-H.bcmap new file mode 100644 index 00000000..39189c54 --- /dev/null +++ b/web/public/vendor/pdfjs/cmaps/GB-H.bcmap @@ -0,0 +1,4 @@ +àRCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSE!!�º]aX!!]`�21�> �p �z�$]‚�"R‚d�-Uƒ7�*„ 4„%�+ „Z „{�/…%…<�9K…b�1]†.�"‡ ‰`]‡,�"]ˆ +�"]ˆh�"]‰F�"]Š$�"]‹�"]‹`�"]Œ>�"]��"]�z�"]ŽX�"]�6�"]��"]�r�"]‘P�"]’.�"]“ �"]“j�"]”H�"]•&�"]–�"]–b�"]—@�"]˜�"]˜|�"]™Z�"]š8�"]›�"]›t�"]œR�"]�0�"]ž�"]žl�"]ŸJ�"] (�"]¡�"]¡d�"]¢B�"]£ �"X£~�']¤W�"]¥5�"]¦�"]¦q�"]§O�"]¨-�"]© �"]©i�"]ªG�"]«%�"]¬�"]¬a�"]­?�"]®�"]®{�"]¯Y�"]°7�"]±�"]±s�"]²Q�"]³/�"]´ �"]´k�"]µI�"]¶'�"]·�"]·c�"]¸A�"]¹�"]¹}�"]º[�"]»9 \ No newline at end of file diff --git a/web/public/vendor/pdfjs/cmaps/GB-V.bcmap b/web/public/vendor/pdfjs/cmaps/GB-V.bcmap new file mode 100644 index 00000000..31083451 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GB-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap new file mode 100644 index 00000000..05fff7e8 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap new file mode 100644 index 00000000..0cdf6bed Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBK2K-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBK2K-H.bcmap new file mode 100644 index 00000000..46f6ba59 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBK2K-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBK2K-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBK2K-V.bcmap new file mode 100644 index 00000000..d9a94798 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBK2K-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap new file mode 100644 index 00000000..5cb0af68 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap new file mode 100644 index 00000000..bca93b8e Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap new file mode 100644 index 00000000..4b4e2d32 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap new file mode 100644 index 00000000..38f70669 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBT-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBT-H.bcmap new file mode 100644 index 00000000..8437ac33 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBT-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBT-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBT-V.bcmap new file mode 100644 index 00000000..697ab4a8 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBT-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap new file mode 100644 index 00000000..f6e50e89 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap new file mode 100644 index 00000000..6c0d71a2 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap new file mode 100644 index 00000000..c9edf67c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap new file mode 100644 index 00000000..31450c97 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/H.bcmap b/web/public/vendor/pdfjs/cmaps/H.bcmap new file mode 100644 index 00000000..7b24ea46 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap new file mode 100644 index 00000000..7d30c050 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap new file mode 100644 index 00000000..78946940 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap new file mode 100644 index 00000000..d829a231 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap new file mode 100644 index 00000000..2b572b50 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap new file mode 100644 index 00000000..971a4f23 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap new file mode 100644 index 00000000..d353ca25 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap new file mode 100644 index 00000000..576dc011 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap new file mode 100644 index 00000000..0e96d0e2 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap new file mode 100644 index 00000000..11d170c7 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap new file mode 100644 index 00000000..54959bf9 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap b/web/public/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap new file mode 100644 index 00000000..6ef7857a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap b/web/public/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap new file mode 100644 index 00000000..1fb2fa2a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Hankaku.bcmap b/web/public/vendor/pdfjs/cmaps/Hankaku.bcmap new file mode 100644 index 00000000..4b8ec7fc Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Hankaku.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Hiragana.bcmap b/web/public/vendor/pdfjs/cmaps/Hiragana.bcmap new file mode 100644 index 00000000..17e983e7 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Hiragana.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap new file mode 100644 index 00000000..a45c65f0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap new file mode 100644 index 00000000..0e7b21f0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSC-H.bcmap b/web/public/vendor/pdfjs/cmaps/KSC-H.bcmap new file mode 100644 index 00000000..b9b22b67 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap b/web/public/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap new file mode 100644 index 00000000..2531ffcf Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap b/web/public/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap new file mode 100644 index 00000000..367ceb22 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSC-V.bcmap b/web/public/vendor/pdfjs/cmaps/KSC-V.bcmap new file mode 100644 index 00000000..6ae2f0b6 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap new file mode 100644 index 00000000..a8d4240e Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap new file mode 100644 index 00000000..8b4ae18f Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap new file mode 100644 index 00000000..b655dbcf Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap new file mode 100644 index 00000000..21f97f65 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap b/web/public/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap new file mode 100644 index 00000000..e06f361e Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap b/web/public/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap new file mode 100644 index 00000000..f3c9113f Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Katakana.bcmap b/web/public/vendor/pdfjs/cmaps/Katakana.bcmap new file mode 100644 index 00000000..524303c4 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Katakana.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/LICENSE b/web/public/vendor/pdfjs/cmaps/LICENSE new file mode 100644 index 00000000..b1ad168a --- /dev/null +++ b/web/public/vendor/pdfjs/cmaps/LICENSE @@ -0,0 +1,36 @@ +%%Copyright: ----------------------------------------------------------- +%%Copyright: Copyright 1990-2009 Adobe Systems Incorporated. +%%Copyright: All rights reserved. +%%Copyright: +%%Copyright: Redistribution and use in source and binary forms, with or +%%Copyright: without modification, are permitted provided that the +%%Copyright: following conditions are met: +%%Copyright: +%%Copyright: Redistributions of source code must retain the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer. +%%Copyright: +%%Copyright: Redistributions in binary form must reproduce the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer in the documentation and/or other materials +%%Copyright: provided with the distribution. +%%Copyright: +%%Copyright: Neither the name of Adobe Systems Incorporated nor the names +%%Copyright: of its contributors may be used to endorse or promote +%%Copyright: products derived from this software without specific prior +%%Copyright: written permission. +%%Copyright: +%%Copyright: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +%%Copyright: CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +%%Copyright: INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +%%Copyright: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +%%Copyright: DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +%%Copyright: CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +%%Copyright: SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +%%Copyright: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +%%Copyright: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +%%Copyright: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +%%Copyright: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +%%Copyright: OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +%%Copyright: SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +%%Copyright: ----------------------------------------------------------- diff --git a/web/public/vendor/pdfjs/cmaps/NWP-H.bcmap b/web/public/vendor/pdfjs/cmaps/NWP-H.bcmap new file mode 100644 index 00000000..afc5e4b0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/NWP-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/NWP-V.bcmap b/web/public/vendor/pdfjs/cmaps/NWP-V.bcmap new file mode 100644 index 00000000..bb5785e3 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/NWP-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/RKSJ-H.bcmap b/web/public/vendor/pdfjs/cmaps/RKSJ-H.bcmap new file mode 100644 index 00000000..fb8d298e Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/RKSJ-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/RKSJ-V.bcmap b/web/public/vendor/pdfjs/cmaps/RKSJ-V.bcmap new file mode 100644 index 00000000..a2555a6c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/RKSJ-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/Roman.bcmap b/web/public/vendor/pdfjs/cmaps/Roman.bcmap new file mode 100644 index 00000000..f896dcf1 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/Roman.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap new file mode 100644 index 00000000..d5db27c5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap new file mode 100644 index 00000000..1dc9b7a2 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap new file mode 100644 index 00000000..961afefb Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap new file mode 100644 index 00000000..df0cffe8 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap new file mode 100644 index 00000000..1ab18a14 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap new file mode 100644 index 00000000..ad14662e Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap new file mode 100644 index 00000000..83c6bd7c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap new file mode 100644 index 00000000..22a27e4d Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap new file mode 100644 index 00000000..5bd6228c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap new file mode 100644 index 00000000..53c534b7 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap new file mode 100644 index 00000000..b95045b4 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap new file mode 100644 index 00000000..51f023e0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap new file mode 100644 index 00000000..f0dbd14f Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap new file mode 100644 index 00000000..ce9c30a9 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap new file mode 100644 index 00000000..982ca462 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap new file mode 100644 index 00000000..f78020dd Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap new file mode 100644 index 00000000..7daf56af Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap new file mode 100644 index 00000000..ac9975c5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap new file mode 100644 index 00000000..3da0a1c6 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap new file mode 100644 index 00000000..c50b9ddf Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap new file mode 100644 index 00000000..67613446 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap new file mode 100644 index 00000000..70bf90c0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap new file mode 100644 index 00000000..7a83d53a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap new file mode 100644 index 00000000..7a871353 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap new file mode 100644 index 00000000..9f0334ca Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap new file mode 100644 index 00000000..808a94f0 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap new file mode 100644 index 00000000..d768bf81 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap new file mode 100644 index 00000000..3d5bf6fb Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap new file mode 100644 index 00000000..09eee10d Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap new file mode 100644 index 00000000..6c546001 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap new file mode 100644 index 00000000..1b1a64f5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap new file mode 100644 index 00000000..994aa9ef Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap new file mode 100644 index 00000000..643f921b Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap new file mode 100644 index 00000000..c148f67f Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap new file mode 100644 index 00000000..1849d809 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap new file mode 100644 index 00000000..a83a677c Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap new file mode 100644 index 00000000..f527248a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap new file mode 100644 index 00000000..e1a988dc Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap new file mode 100644 index 00000000..47e054a9 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap new file mode 100644 index 00000000..b5b94852 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap new file mode 100644 index 00000000..026adcaa Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap new file mode 100644 index 00000000..fd4e66e8 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap new file mode 100644 index 00000000..075efb70 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap new file mode 100644 index 00000000..769d2142 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap new file mode 100644 index 00000000..bdab208b Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap new file mode 100644 index 00000000..6ff8674a Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap b/web/public/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap new file mode 100644 index 00000000..8dfa76a5 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/V.bcmap b/web/public/vendor/pdfjs/cmaps/V.bcmap new file mode 100644 index 00000000..fdec9906 Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/V.bcmap differ diff --git a/web/public/vendor/pdfjs/cmaps/WP-Symbol.bcmap b/web/public/vendor/pdfjs/cmaps/WP-Symbol.bcmap new file mode 100644 index 00000000..46729bbf Binary files /dev/null and b/web/public/vendor/pdfjs/cmaps/WP-Symbol.bcmap differ diff --git a/web/public/vendor/pdfjs/pdf.min.mjs b/web/public/vendor/pdfjs/pdf.min.mjs new file mode 100644 index 00000000..5822350b --- /dev/null +++ b/web/public/vendor/pdfjs/pdf.min.mjs @@ -0,0 +1,28 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ +/** + * pdfjsVersion = 5.7.284 + * pdfjsBuild = 7e5b36c2d + */const t=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),e=[1/0,1/0,-1/0,-1/0],i=new Float32Array(e),s=[.001,0,0,.001,0,0],n=1.35,r=1,a=2,o=4,l=16,h=32,d=64,c=128,u=256,p={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},g="pdfjs_internal_editor_",m={DISABLE:-1,NONE:0,FREETEXT:3,HIGHLIGHT:9,STAMP:13,INK:15,POPUP:16,SIGNATURE:101,COMMENT:102},f={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23,INK_COLOR_AND_OPACITY:24,HIGHLIGHT_COLOR:31,HIGHLIGHT_THICKNESS:32,HIGHLIGHT_FREE:33,HIGHLIGHT_SHOW_ALL:34,DRAW_STEP:41},b={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},y=0,A=1,v=2,w=3,x=3,C=4,E={GRAYSCALE_1BPP:1,RGB_24BPP:2,RGBA_32BPP:3},T={TEXT:1,LINK:2,FREETEXT:3,LINE:4,SQUARE:5,CIRCLE:6,POLYGON:7,POLYLINE:8,HIGHLIGHT:9,UNDERLINE:10,SQUIGGLY:11,STRIKEOUT:12,STAMP:13,CARET:14,INK:15,POPUP:16,FILEATTACHMENT:17,SOUND:18,MOVIE:19,WIDGET:20,SCREEN:21,PRINTERMARK:22,TRAPNET:23,WATERMARK:24,THREED:25,REDACT:26},S=1,_=2,P=3,D=4,M=5,k={ERRORS:0,WARNINGS:1,INFOS:5},I={dependency:1,setLineWidth:2,setLineCap:3,setLineJoin:4,setMiterLimit:5,setDash:6,setRenderingIntent:7,setFlatness:8,setGState:9,save:10,restore:11,transform:12,moveTo:13,lineTo:14,curveTo:15,curveTo2:16,curveTo3:17,closePath:18,rectangle:19,stroke:20,closeStroke:21,fill:22,eoFill:23,fillStroke:24,eoFillStroke:25,closeFillStroke:26,closeEOFillStroke:27,endPath:28,clip:29,eoClip:30,beginText:31,endText:32,setCharSpacing:33,setWordSpacing:34,setHScale:35,setLeading:36,setFont:37,setTextRenderingMode:38,setTextRise:39,moveText:40,setLeadingMoveText:41,setTextMatrix:42,nextLine:43,showText:44,showSpacedText:45,nextLineShowText:46,nextLineSetSpacingShowText:47,setCharWidth:48,setCharWidthAndBounds:49,setStrokeColorSpace:50,setFillColorSpace:51,setStrokeColor:52,setStrokeColorN:53,setFillColor:54,setFillColorN:55,setStrokeGray:56,setFillGray:57,setStrokeRGBColor:58,setFillRGBColor:59,setStrokeCMYKColor:60,setFillCMYKColor:61,shadingFill:62,beginInlineImage:63,beginImageData:64,endInlineImage:65,paintXObject:66,markPoint:67,markPointProps:68,beginMarkedContent:69,beginMarkedContentProps:70,endMarkedContent:71,beginCompat:72,endCompat:73,paintFormXObjectBegin:74,paintFormXObjectEnd:75,beginGroup:76,endGroup:77,beginAnnotation:80,endAnnotation:81,paintImageMaskXObject:83,paintImageMaskXObjectGroup:84,paintImageXObject:85,paintInlineImageXObject:86,paintInlineImageXObjectGroup:87,paintImageXObjectRepeat:88,paintImageMaskXObjectRepeat:89,paintSolidColorImageMask:90,constructPath:91,setStrokeTransparent:92,setFillTransparent:93,rawFillPath:94},F=0,B=1,R=2,O=3,L=4,N={NEED_PASSWORD:1,INCORRECT_PASSWORD:2};let U=k.WARNINGS;function setVerbosityLevel(t){Number.isInteger(t)&&(U=t)}function getVerbosityLevel(){return U}function info(t){U>=k.INFOS&&console.info(`Info: ${t}`)}function warn(t){U>=k.WARNINGS&&console.warn(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function assert(t,e){t||unreachable(e)}function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=function stringToUTF8String(t){return decodeURIComponent(escape(t))}(t)}catch{}}const s=e?URL.parse(t,e):URL.parse(t);return function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s)?s:null}function updateUrlHash(t,e,i=!1){const s=URL.parse(t);if(s){s.hash=e;return s.href}return i&&createValidAbsoluteUrl(t,"http://example.com")?t.split("#",1)[0]+""+(e?`#${e}`:""):""}function stripPath(t){return t.substring(t.lastIndexOf("/")+1)}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const H=function BaseExceptionClosure(){function BaseException(t,e){this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends H{constructor(t,e){super(t,"PasswordException");this.code=e}}class UnknownErrorException extends H{constructor(t,e){super(t,"UnknownErrorException");this.details=e}}class InvalidPDFException extends H{constructor(t){super(t,"InvalidPDFException")}}class ResponseException extends H{constructor(t,e,i){super(t,"ResponseException");this.status=e;this.missing=i}}class FormatError extends H{constructor(t){super(t,"FormatError")}}class AbortException extends H{constructor(t){super(t,"AbortException")}}function stringToBytes(t){"string"!=typeof t&&unreachable("Invalid argument for stringToBytes");const e=t.length,i=new Uint8Array(e);for(let s=0;s{if("undefined"==typeof document)return!1;const t=document.createElement("input");t.type="color";t.setAttribute("alpha","");t.value="#ff000080";return"#ff0000"!==t.value})())}}class Util{static get hexNums(){return shadow(this,"hexNums",Array.from(Array(256).keys(),t=>t.toString(16).padStart(2,"0")))}static makeHexColor(t,e,i){return`#${this.hexNums[t]}${this.hexNums[e]}${this.hexNums[i]}`}static domMatrixToTransform(t){return[t.a,t.b,t.c,t.d,t.e,t.f]}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[0];e[2]*=t[0];if(t[3]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[1];e[1]=i;i=e[2];e[2]=e[3];e[3]=i;if(t[1]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[2];e[2]*=t[2]}e[0]+=t[4];e[1]+=t[5];e[2]+=t[4];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static multiplyByDOMMatrix(t,e){return[t[0]*e.a+t[2]*e.b,t[1]*e.a+t[3]*e.b,t[0]*e.c+t[2]*e.d,t[1]*e.c+t[3]*e.d,t[0]*e.e+t[2]*e.f+t[4],t[1]*e.e+t[3]*e.f+t[5]]}static applyTransform(t,e,i=0){const s=t[i],n=t[i+1];t[i]=s*e[0]+n*e[2]+e[4];t[i+1]=s*e[1]+n*e[3]+e[5]}static applyTransformToBezier(t,e,i=0){const s=e[0],n=e[1],r=e[2],a=e[3],o=e[4],l=e[5];for(let e=0;e<6;e+=2){const h=t[i+e],d=t[i+e+1];t[i+e]=h*s+d*r+o;t[i+e+1]=h*n+d*a+l}}static applyInverseTransform(t,e){const i=t[0],s=t[1],n=e[0]*e[3]-e[1]*e[2];t[0]=(i*e[3]-s*e[2]+e[2]*e[5]-e[4]*e[3])/n;t[1]=(-i*e[1]+s*e[0]+e[4]*e[1]-e[5]*e[0])/n}static axialAlignedBoundingBox(t,e,i){const s=e[0],n=e[1],r=e[2],a=e[3],o=e[4],l=e[5],h=t[0],d=t[1],c=t[2],u=t[3];let p=s*h+o,g=p,m=s*c+o,f=m,b=a*d+l,y=b,A=a*u+l,v=A;if(0!==n||0!==r){const t=n*h,e=n*c,i=r*d,s=r*u;p+=i;f+=i;m+=s;g+=s;b+=t;v+=t;A+=e;y+=e}i[0]=Math.min(i[0],p,m,g,f);i[1]=Math.min(i[1],b,A,y,v);i[2]=Math.max(i[2],p,m,g,f);i[3]=Math.max(i[3],b,A,y,v)}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t,e){const i=t[0],s=t[1],n=t[2],r=t[3],a=i**2+s**2,o=i*n+s*r,l=n**2+r**2,h=(a+l)/2,d=Math.sqrt(h**2-(a*l-o**2));e[0]=Math.sqrt(h+d||1);e[1]=Math.sqrt(h-d||1)}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),r=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>r?null:[i,n,s,r]}static pointBoundingBox(t,e,i){i[0]=Math.min(i[0],t);i[1]=Math.min(i[1],e);i[2]=Math.max(i[2],t);i[3]=Math.max(i[3],e)}static rectBoundingBox(t,e,i,s,n){n[0]=Math.min(n[0],t,i);n[1]=Math.min(n[1],e,s);n[2]=Math.max(n[2],t,i);n[3]=Math.max(n[3],e,s)}static#t(t,e,i,s,n,r,a,o,l,h){if(l<=0||l>=1)return;const d=1-l,c=l*l,u=c*l,p=d*(d*(d*t+3*l*e)+3*c*i)+u*s,g=d*(d*(d*n+3*l*r)+3*c*a)+u*o;h[0]=Math.min(h[0],p);h[1]=Math.min(h[1],g);h[2]=Math.max(h[2],p);h[3]=Math.max(h[3],g)}static#e(t,e,i,s,n,r,a,o,l,h,d,c){if(Math.abs(l)<1e-12){Math.abs(h)>=1e-12&&this.#t(t,e,i,s,n,r,a,o,-d/h,c);return}const u=h**2-4*d*l;if(u<0)return;const p=Math.sqrt(u),g=2*l;this.#t(t,e,i,s,n,r,a,o,(-h+p)/g,c);this.#t(t,e,i,s,n,r,a,o,(-h-p)/g,c)}static bezierBoundingBox(t,e,i,s,n,r,a,o,l){l[0]=Math.min(l[0],t,a);l[1]=Math.min(l[1],e,o);l[2]=Math.max(l[2],t,a);l[3]=Math.max(l[3],e,o);this.#e(t,i,n,a,e,s,r,o,3*(3*(i-n)-t+a),6*(t-2*i+n),3*(i-t),l);this.#e(t,i,n,a,e,s,r,o,3*(3*(s-r)-e+o),6*(e-2*s+r),3*(s-e),l)}}let z=null,G=null;function normalizeUnicode(t){if(!z){z=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;G=new Map([["ſt","ſt"]])}return t.replaceAll(z,(t,e,i)=>e?e.normalize("NFKC"):G.get(i))}function getUuid(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);crypto.getRandomValues(t);return function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(e[],makeMap=()=>new Map,makeObj=()=>Object.create(null);function MathClamp(t,e,i){return Math.min(Math.max(t,e),i)}class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}class XfaLayer{static setupStorage(t,e,i,s,n){const r=s.getValue(e,{value:null});switch(i.name){case"textarea":null!==r.value&&(t.textContent=r.value);if("print"===n)break;t.addEventListener("input",t=>{s.setValue(e,{value:t.target.value})});break;case"input":if("radio"===i.attributes.type||"checkbox"===i.attributes.type){r.value===i.attributes.xfaOn?t.setAttribute("checked",!0):r.value===i.attributes.xfaOff&&t.removeAttribute("checked");if("print"===n)break;t.addEventListener("change",t=>{s.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})})}else{null!==r.value&&t.setAttribute("value",r.value);if("print"===n)break;t.addEventListener("input",t=>{s.setValue(e,{value:t.target.value})})}break;case"select":if(null!==r.value){t.setAttribute("value",r.value);for(const t of i.children)t.attributes.value===r.value?t.attributes.selected=!0:Object.hasOwn(t.attributes,"selected")&&delete t.attributes.selected}t.addEventListener("input",t=>{const i=t.target.options,n=-1===i.selectedIndex?"":i[i.selectedIndex].value;s.setValue(e,{value:n})})}}static setAttributes({html:t,element:e,storage:i=null,intent:s,linkService:n}){const{attributes:r}=e,a=t instanceof HTMLAnchorElement;"radio"===r.type&&(r.name=`${r.name}-${s}`);for(const[e,i]of Object.entries(r))if(null!=i)switch(e){case"class":i.length&&t.setAttribute(e,i.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",i);break;case"style":Object.assign(t.style,i);break;case"textContent":t.textContent=i;break;default:(!a||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,i)}a&&n.addLinkAttributes(t,r.href,r.newWindow);i&&r.dataId&&this.setupStorage(t,r.dataId,e,i)}static render(t){const e=t.annotationStorage,i=t.linkService,s=t.xfaHtml,n=t.intent||"display",r=document.createElement(s.name);s.attributes&&this.setAttributes({html:r,element:s,intent:n,linkService:i});const a="richText"!==n,o=t.div;o.append(r);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;o.style.transform=e}a&&o.setAttribute("class","xfaLayer xfaFont");const l=[];if(0===s.children.length){if(s.value){const t=document.createTextNode(s.value);r.append(t);a&&XfaText.shouldBuildText(s.name)&&l.push(t)}return{textDivs:l}}const h=[[s,-1,r]];for(;h.length>0;){const[t,s,r]=h.at(-1);if(s+1===t.children.length){h.pop();continue}const o=t.children[++h.at(-1)[1]];if(null===o)continue;const{name:d}=o;if("#text"===d){const t=document.createTextNode(o.value);l.push(t);r.append(t);continue}const c=o?.attributes?.xmlns?document.createElementNS(o.attributes.xmlns,d):document.createElement(d);r.append(c);o.attributes&&this.setAttributes({html:c,element:o,storage:e,intent:n,linkService:i});if(o.children?.length>0)h.push([o,-1,c]);else if(o.value){const t=document.createTextNode(o.value);a&&XfaText.shouldBuildText(d)&&l.push(t);c.append(t)}}for(const t of o.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:l}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}const V="http://www.w3.org/2000/svg";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}async function fetchData(t,e="text"){if(isValidFetchUrl(t,document.baseURI)){const i=await fetch(t);if(!i.ok)throw new Error(i.statusText);switch(e){case"blob":return i.blob();case"bytes":return i.bytes();case"json":return i.json()}return i.text()}return new Promise((i,s)=>{const n=new XMLHttpRequest;n.open("GET",t,!0);n.responseType="bytes"===e?"arraybuffer":e;n.onreadystatechange=()=>{if(n.readyState===XMLHttpRequest.DONE)if(200!==n.status&&0!==n.status)s(new Error(n.statusText));else{switch(e){case"bytes":i(new Uint8Array(n.response));return;case"blob":case"json":i(n.response);return}i(n.responseText)}};n.send(null)})}class PageViewport{constructor({viewBox:t,userUnit:e,scale:i,rotation:s,offsetX:n=0,offsetY:r=0,dontFlip:a=!1}){this.viewBox=t;this.userUnit=e;this.scale=i;this.rotation=s;this.offsetX=n;this.offsetY=r;i*=e;const o=(t[2]+t[0])/2,l=(t[3]+t[1])/2;let h,d,c,u,p,g,m,f;(s%=360)<0&&(s+=360);switch(s){case 180:h=-1;d=0;c=0;u=1;break;case 90:h=0;d=1;c=1;u=0;break;case 270:h=0;d=-1;c=-1;u=0;break;case 0:h=1;d=0;c=0;u=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(a){c=-c;u=-u}if(0===h){p=Math.abs(l-t[1])*i+n;g=Math.abs(o-t[0])*i+r;m=(t[3]-t[1])*i;f=(t[2]-t[0])*i}else{p=Math.abs(o-t[0])*i+n;g=Math.abs(l-t[1])*i+r;m=(t[2]-t[0])*i;f=(t[3]-t[1])*i}this.transform=[h*i,d*i,c*i,u*i,p-h*i*o-c*i*l,g-d*i*o-u*i*l];this.width=m;this.height=f}get rawDims(){const t=this.viewBox;return shadow(this,"rawDims",{pageWidth:t[2]-t[0],pageHeight:t[3]-t[1],pageX:t[0],pageY:t[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:i=this.offsetX,offsetY:s=this.offsetY,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}convertToViewportPoint(t,e){const i=[t,e];Util.applyTransform(i,this.transform);return i}convertToViewportRectangle(t){const e=[t[0],t[1]];Util.applyTransform(e,this.transform);const i=[t[2],t[3]];Util.applyTransform(i,this.transform);return[e[0],e[1],i[0],i[1]]}convertToPdfPoint(t,e){const i=[t,e];Util.applyInverseTransform(i,this.transform);return i}}class RenderingCancelledException extends H{constructor(t,e=0){super(t,"RenderingCancelledException");this.extraDelay=e}}function isDataScheme(t){const e=t.length;let i=0;for(;i{try{return new URL(t)}catch{try{return new URL(decodeURIComponent(t))}catch{try{return new URL(t,"https://foo.bar")}catch{try{return new URL(decodeURIComponent(t),"https://foo.bar")}catch{return null}}}}})(t);if(!i)return e;const decode=t=>{try{let e=decodeURIComponent(t);if(e.includes("/")){e=stripPath(e);if(/^\.pdf$/i.test(e))return t}return e}catch{return t}},s=/\.pdf$/i,n=stripPath(i.pathname);if(s.test(n))return decode(n);if(i.searchParams.size>0){const getLast=t=>[...t].findLast(t=>s.test(t)),t=getLast(i.searchParams.values())??getLast(i.searchParams.keys());if(t)return decode(t)}if(i.hash){const t=/[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i.exec(i.hash);if(t)return decode(t[0])}return e}class StatTimer{#i=new Map;times=[];time(t){this.#i.has(t)&&warn(`Timer is already running for ${t}`);this.#i.set(t,Date.now())}timeEnd(t){this.#i.has(t)||warn(`Timer has not been started for ${t}`);this.times.push({name:t,start:this.#i.get(t),end:Date.now()});this.#i.delete(t)}toString(){const t=Math.max(...this.times.map(t=>t.name.length));return this.times.map(e=>`${e.name.padEnd(t)} ${e.end-e.start}ms\n`).join("")}}function isValidFetchUrl(t,e){const i=e?URL.parse(t,e):URL.parse(t);return/https?:/.test(i?.protocol??"")}function noContextMenu(t){t.preventDefault()}function stopEvent(t){t.preventDefault();t.stopPropagation()}class PDFDateString{static#s;static toDateObject(t){if(t instanceof Date)return t;if(!t||"string"!=typeof t)return null;this.#s||=new RegExp("^D:(\\d{4})(\\d{2})?(\\d{2})?(\\d{2})?(\\d{2})?(\\d{2})?([Z|+|-])?(\\d{2})?'?(\\d{2})?'?");const e=this.#s.exec(t);if(!e)return null;const i=parseInt(e[1],10);let s=parseInt(e[2],10);s=s>=1&&s<=12?s-1:0;let n=parseInt(e[3],10);n=n>=1&&n<=31?n:1;let r=parseInt(e[4],10);r=r>=0&&r<=23?r:0;let a=parseInt(e[5],10);a=a>=0&&a<=59?a:0;let o=parseInt(e[6],10);o=o>=0&&o<=59?o:0;const l=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===l){r+=h;a+=d}else if("+"===l){r-=h;a-=d}return new Date(Date.UTC(i,s,n,r,a,o))}}function getXfaPageViewport(t,{scale:e=1,rotation:i=0}){const{width:s,height:n}=t.attributes.style,r=[0,0,parseInt(s,10),parseInt(n,10)];return new PageViewport({viewBox:r,userUnit:1,scale:e,rotation:i})}function getRGBA(t){if(t.startsWith("#")){const e=t.slice(1);return[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16),e.length>=8?parseInt(e.slice(6,8),16)/255:1]}if(t.startsWith("rgb(")){const[e,i,s]=t.slice(4,-1).split(",").map(t=>parseInt(t,10));return[e,i,s,1]}if(t.startsWith("rgba(")){const e=t.slice(5,-1).split(",");return[parseInt(e[0],10),parseInt(e[1],10),parseInt(e[2],10),parseFloat(e[3])]}const e=t.match(/^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+|none))?\)$/);return e?[Math.round(255*parseFloat(e[1])),Math.round(255*parseFloat(e[2])),Math.round(255*parseFloat(e[3])),void 0!==e[4]&&"none"!==e[4]?parseFloat(e[4]):1]:null}function getRGB(t){const e=getRGBA(t);if(!e){warn(`Not a valid color format: "${t}"`);return[0,0,0]}return e.slice(0,3)}function getCurrentTransform(t){const{a:e,b:i,c:s,d:n,e:r,f:a}=t.getTransform();return[e,i,s,n,r,a]}function getCurrentTransformInverse(t){const{a:e,b:i,c:s,d:n,e:r,f:a}=t.getTransform().invertSelf();return[e,i,s,n,r,a]}function setLayerDimensions(t,e,i=!1,s=!0){if(e instanceof PageViewport){const{pageWidth:s,pageHeight:n}=e.rawDims,{style:r}=t,a=FeatureTest.isCSSRoundSupported,o=`var(--total-scale-factor) * ${s}px`,l=`var(--total-scale-factor) * ${n}px`,h=a?`round(down, ${o}, var(--scale-round-x))`:`calc(${o})`,d=a?`round(down, ${l}, var(--scale-round-y))`:`calc(${l})`;if(i&&e.rotation%180!=0){r.width=d;r.height=h}else{r.width=h;r.height=d}}s&&t.setAttribute("data-main-rotation",e.rotation)}class OutputScale{constructor(){const{pixelRatio:t}=OutputScale;this.sx=t;this.sy=t}get scaled(){return 1!==this.sx||1!==this.sy}get symmetric(){return this.sx===this.sy}limitCanvas(t,e,i,s,n=-1){let r=1/0,a=1/0,o=1/0;(i=OutputScale.capPixels(i,n))>0&&(r=Math.sqrt(i/(t*e)));if(-1!==s){a=s/t;o=s/e}const l=Math.min(r,a,o);if(this.sx>l||this.sy>l){this.sx=l;this.sy=l;return!0}return!1}static get pixelRatio(){return globalThis.devicePixelRatio||1}static capPixels(t,e){if(e>=0){const i=Math.ceil(window.screen.availWidth*window.screen.availHeight*this.pixelRatio**2*(1+e/100));return t>0?Math.min(t,i):i}return t}}const j=["image/apng","image/avif","image/bmp","image/gif","image/jpeg","image/png","image/svg+xml","image/webp","image/x-icon"];class ColorScheme{static get isDarkMode(){return shadow(this,"isDarkMode",!!window?.matchMedia?.("(prefers-color-scheme: dark)").matches)}}class CSSConstants{static get commentForegroundColor(){const t=document.createElement("span");t.classList.add("comment","sidebar");const{style:e}=t;e.width=e.height="0";e.display="none";e.color="var(--comment-fg-color)";document.body.append(t);const{color:i}=window.getComputedStyle(t);t.remove();return shadow(this,"commentForegroundColor",getRGB(i))}}function applyOpacity(t,e){const i=255*(1-(e=MathClamp(e??1,0,1)));return t.map(t=>Math.round(t*e+i))}function RGBToHSL(t,e){const i=t[0]/255,s=t[1]/255,n=t[2]/255,r=Math.max(i,s,n),a=Math.min(i,s,n),o=(r+a)/2;if(r===a)e[0]=e[1]=0;else{const t=r-a;e[1]=o<.5?t/(r+a):t/(2-r-a);switch(r){case i:e[0]=60*((s-n)/t+(sn?(s+.05)/(n+.05):(n+.05)/(s+.05)}const $=new Map;function findContrastColor(t,e){const i=t[0]+256*t[1]+65536*t[2]+16777216*e[0]+4294967296*e[1]+1099511627776*e[2];let s=$.get(i);if(s)return s;const n=new Float32Array(9),r=n.subarray(0,3),a=n.subarray(3,6);RGBToHSL(t,a);const o=n.subarray(6,9);RGBToHSL(e,o);const l=o[2]<.5,h=l?12:4.5;a[2]=l?Math.sqrt(a[2]):1-Math.sqrt(1-a[2]);if(contrastRatio(a,o,r)i;){const i=a[2]=(t+e)/2;l===contrastRatio(a,o,r){e.delete()},{signal:e._signal});this.#o.append(i)}get#b(){const t=document.createElement("div");t.className="divider";return t}async addAltText(t){const e=await t.render();this.#f(e);this.#o.append(e,this.#b);this.#l=t}addComment(t,e=null){if(this.#h)return;const i=t.renderForToolbar();if(!i)return;this.#f(i);const s=this.#d=this.#b;if(e){this.#o.insertBefore(i,e);this.#o.insertBefore(s,e)}else this.#o.append(i,s);this.#h=t;t.toolbar=this}addColorPicker(t){if(this.#r)return;this.#r=t;const e=t.renderButton();this.#f(e);this.#o.append(e,this.#b)}async addEditSignatureButton(t){const e=this.#c=await t.renderEditButton(this.#a);this.#f(e);this.#o.append(e,this.#b)}removeButton(t){if("comment"===t){this.#h?.removeToolbarCommentButton();this.#h=null;this.#d?.remove();this.#d=null}}async addButton(t,e){switch(t){case"colorPicker":e&&this.addColorPicker(e);break;case"altText":e&&await this.addAltText(e);break;case"editSignature":e&&await this.addEditSignatureButton(e);break;case"delete":this.addDeleteButton();break;case"comment":e&&this.addComment(e)}}async addButtonBefore(t,e,i){if(!e&&"comment"===t)return;const s=this.#o.querySelector(i);s&&"comment"===t&&this.addComment(e,s)}updateEditSignatureButton(t){this.#c&&(this.#c.title=t)}remove(){this.#n.remove();this.#r?.destroy();this.#r=null}}class FloatingToolbar{#o=null;#n=null;#y;constructor(t){this.#y=t}#A(){const t=this.#n=document.createElement("div");t.className="editToolbar";t.setAttribute("role","toolbar");const e=this.#y._signal;e instanceof AbortSignal&&!e.aborted&&t.addEventListener("contextmenu",noContextMenu,{signal:e});const i=this.#o=document.createElement("div");i.className="buttons";t.append(i);this.#y.hasCommentManager()&&this.#v("commentButton","pdfjs-comment-floating-button","pdfjs-comment-floating-button-label",()=>{this.#y.commentSelection("floating_button")});this.#v("highlightButton","pdfjs-highlight-floating-button1","pdfjs-highlight-floating-button-label",()=>{this.#y.highlightSelection("floating_button")});return t}#w(t,e){let i=0,s=0;for(const n of t){const t=n.y+n.height;if(ti){s=r;i=t}else e?r>s&&(s=r):r=1}static clearPointerType(){CurrentPointers.#T=null}static clearPointerIds(){CurrentPointers.#x=NaN;CurrentPointers.#C=null}static clearTimeStamp(){CurrentPointers.#E=NaN}}class IdManager{#S=0;get id(){return`${g}${this.#S++}`}}class ImageManager{#_=getUuid();#S=0;#P=null;static get _isSVGFittingCanvas(){const t=new OffscreenCanvas(1,3).getContext("2d",{willReadFrequently:!0}),e=new Image;e.src='data:image/svg+xml;charset=UTF-8,';return shadow(this,"_isSVGFittingCanvas",e.decode().then(()=>{t.drawImage(e,0,0,1,1,0,0,1,3);return 0===new Uint32Array(t.getImageData(0,0,1,1).data.buffer)[0]}))}async#D(t,e){this.#P||=new Map;let i=this.#P.get(t);if(null===i)return null;if(i?.bitmap){i.refCounter+=1;return i}try{i||={bitmap:null,id:`image_${this.#_}_${this.#S++}`,refCounter:0,isSvg:!1};let t;if("string"==typeof e){i.url=e;t=await fetchData(e,"blob")}else e instanceof File?t=i.file=e:e instanceof Blob&&(t=e);if("image/svg+xml"===t.type){const e=ImageManager._isSVGFittingCanvas,s=new FileReader,n=new Image,r=new Promise((t,r)=>{n.onload=()=>{i.bitmap=n;i.isSvg=!0;t()};s.onload=async()=>{const t=i.svgUrl=s.result;n.src=await e?`${t}#svgView(preserveAspectRatio(none))`:t};n.onerror=s.onerror=r});s.readAsDataURL(t);await r}else i.bitmap=await createImageBitmap(t);i.refCounter=1}catch(t){warn(t);i=null}this.#P.set(t,i);i&&this.#P.set(i.id,i);return i}async getFromFile(t){const{lastModified:e,name:i,size:s,type:n}=t;return this.#D(`${e}_${i}_${s}_${n}`,t)}async getFromUrl(t){return this.#D(t,t)}async getFromBlob(t,e){const i=await e;return this.#D(t,i)}async getFromId(t){this.#P||=new Map;const e=this.#P.get(t);if(!e)return null;if(e.bitmap){e.refCounter+=1;return e}if(e.file)return this.getFromFile(e.file);if(e.blobPromise){const{blobPromise:t}=e;delete e.blobPromise;return this.getFromBlob(e.id,t)}return this.getFromUrl(e.url)}getFromCanvas(t,e){this.#P||=new Map;let i=this.#P.get(t);if(i?.bitmap){i.refCounter+=1;return i}const s=new OffscreenCanvas(e.width,e.height);s.getContext("2d").drawImage(e,0,0);i={bitmap:s.transferToImageBitmap(),id:`image_${this.#_}_${this.#S++}`,refCounter:1,isSvg:!1};this.#P.set(t,i);this.#P.set(i.id,i);return i}getSvgUrl(t){const e=this.#P.get(t);return e?.isSvg?e.svgUrl:null}deleteId(t){this.#P||=new Map;const e=this.#P.get(t);if(!e)return;e.refCounter-=1;if(0!==e.refCounter)return;const{bitmap:i}=e;if(!e.url&&!e.file){const t=new OffscreenCanvas(i.width,i.height);t.getContext("bitmaprenderer").transferFromImageBitmap(i);e.blobPromise=t.convertToBlob()}i.close?.();e.bitmap=null}isValidId(t){return t.startsWith(`image_${this.#_}_`)}}class CommandManager{#M=[];#k=!1;#I;#F=-1;constructor(t=128){this.#I=t}add({cmd:t,undo:e,post:i,mustExec:s,type:n=NaN,overwriteIfSameType:r=!1,keepUndo:a=!1}){s&&t();if(this.#k)return;const o={cmd:t,undo:e,post:i,type:n};if(-1===this.#F){this.#M.length>0&&(this.#M.length=0);this.#F=0;this.#M.push(o);return}if(r&&this.#M[this.#F].type===n){a&&(o.undo=this.#M[this.#F].undo);this.#M[this.#F]=o;return}const l=this.#F+1;if(l===this.#I)this.#M.splice(0,1);else{this.#F=l;l=0;e--)if(this.#M[e].type!==t){this.#M.splice(e+1,this.#F-e);this.#F=e;return}this.#M.length=0;this.#F=-1}}destroy(){this.#M=null}}class KeyboardManager{constructor(t){this.buffer=[];this.callbacks=new Map;this.allKeys=new Set;const{isMac:e}=FeatureTest.platform;for(const[i,s,n={}]of t)for(const t of i){const i=t.startsWith("mac+");if(e&&i){this.callbacks.set(t.slice(4),{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}else if(!e&&!i){this.callbacks.set(t,{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}}}#B(t){t.altKey&&this.buffer.push("alt");t.ctrlKey&&this.buffer.push("ctrl");t.metaKey&&this.buffer.push("meta");t.shiftKey&&this.buffer.push("shift");this.buffer.push(t.key);const e=this.buffer.join("+");this.buffer.length=0;return e}exec(t,e){if(!this.allKeys.has(e.key))return;const i=this.callbacks.get(this.#B(e));if(!i)return;const{callback:s,options:{bubbles:n=!1,args:r=[],checker:a=null}}=i;if(!a||a(t,e)){s.bind(t,...r,e)();n||stopEvent(e)}}}class ColorManager{static _colorsMapping=new Map([["CanvasText",[0,0,0]],["Canvas",[255,255,255]]]);get _colors(){const t=new Map([["CanvasText",null],["Canvas",null]]);!function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";e.style.colorScheme="only light";document.body.append(e);for(const i of t.keys()){e.style.color=i;const s=window.getComputedStyle(e).color;t.set(i,getRGB(s))}e.remove()}(t);return shadow(this,"_colors",t)}convert(t){const e=getRGB(t);if(!window.matchMedia("(forced-colors: active)").matches)return e;for(const[t,i]of this._colors)if(i.every((t,i)=>t===e[i]))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?Util.makeHexColor(...e):t}}class AnnotationEditorUIManager{#R=new AbortController;#O=null;#L=null;#N=new Map;#U=new Map;#H=null;#z=null;#G=null;#W=null;#V=new CommandManager;#j=null;#$=null;#X=null;#q=0;#K=new Set;#Y=null;#Q=null;#J=new Set;_editorUndoBar=null;#Z=!1;#tt=!1;#et=!1;#it=null;#st=null;#nt=null;#rt=null;#at=!1;#ot=null;#lt=new IdManager;#ht=!1;#dt=!1;#ct=!1;#ut=null;#pt=null;#gt=null;#mt=null;#ft=null;#bt=m.NONE;#yt=new Set;#At=null;#vt=null;#wt=null;#xt=null;#Ct=null;#Et={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1,hasSelectedText:!1};#Tt=[0,0];#St=null;#_t=null;#Pt=null;#Dt=null;#Mt=null;static TRANSLATE_SMALL=1;static TRANSLATE_BIG=10;static get _keyboardManager(){const t=AnnotationEditorUIManager.prototype,arrowChecker=t=>t.#_t.contains(document.activeElement)&&"BUTTON"!==document.activeElement.tagName&&t.hasSomethingToControl(),textInputChecker=(t,{target:e})=>{if(e instanceof HTMLInputElement){const{type:t}=e;return"text"!==t&&"number"!==t}return!0},e=this.TRANSLATE_SMALL,i=this.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+a","mac+meta+a"],t.selectAll,{checker:textInputChecker}],[["ctrl+z","mac+meta+z"],t.undo,{checker:textInputChecker}],[["ctrl+y","ctrl+shift+z","mac+meta+shift+z","ctrl+shift+Z","mac+meta+shift+Z"],t.redo,{checker:textInputChecker}],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete","mac+Delete"],t.delete,{checker:textInputChecker}],[["Enter","mac+Enter"],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#_t.contains(e)&&!t.isEnterHandled}],[[" ","mac+ "],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#_t.contains(document.activeElement)}],[["Escape","mac+Escape"],t.unselectAll],[["ArrowLeft","mac+ArrowLeft"],t.translateSelectedEditors,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t.translateSelectedEditors,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t.translateSelectedEditors,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t.translateSelectedEditors,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t.translateSelectedEditors,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t.translateSelectedEditors,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t.translateSelectedEditors,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t.translateSelectedEditors,{args:[0,i],checker:arrowChecker}]]))}constructor(t,e,i,s,n,r,a,o,l,h,d,c,u,p,g,m){const f=this._signal=this.#R.signal;this.#_t=t;this.#Pt=e;this.#Dt=i;this.#z=s;this.#j=n;this.#vt=r;this.#Ct=o;this._eventBus=a;a._on("editingaction",this.onEditingAction.bind(this),{signal:f});a._on("pagechanging",this.onPageChanging.bind(this),{signal:f});a._on("scalechanging",this.onScaleChanging.bind(this),{signal:f});a._on("rotationchanging",this.onRotationChanging.bind(this),{signal:f});a._on("setpreference",this.onSetPreference.bind(this),{signal:f});a._on("switchannotationeditorparams",t=>this.updateParams(t.type,t.value),{signal:f});window.addEventListener("pointerdown",()=>{this.#dt=!0},{capture:!0,signal:f});window.addEventListener("pointerup",()=>{this.#dt=!1},{capture:!0,signal:f});window.addEventListener("beforeunload",this.#kt.bind(this),{capture:!0,signal:f});this.#It();this.#Ft();this.#Bt();this.#G=o.annotationStorage;this.#it=o.filterFactory;this.#wt=l;this.#rt=h||null;this.#Z=d;this.#tt=c;this.#et=u;this.#ft=p||null;this.viewParameters={realScale:PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0};this.isShiftKeyDown=!1;this._editorUndoBar=g||null;this._supportsPinchToZoom=!1!==m;n?.setSidebarUiManager(this)}destroy(){this.#Mt?.resolve();this.#Mt=null;this.#R?.abort();this.#R=null;this._signal=null;for(const t of this.#U.values())t.destroy();this.#U.clear();this.#N.clear();this.#J.clear();this.#mt?.clear();this.#O=null;this.#yt.clear();this.#V.destroy();this.#z?.destroy();this.#j?.destroy();this.#vt?.destroy();this.#ot?.hide();this.#ot=null;this.#gt?.destroy();this.#gt=null;this.#L=null;if(this.#st){clearTimeout(this.#st);this.#st=null}if(this.#St){clearTimeout(this.#St);this.#St=null}this._editorUndoBar?.destroy();this.#Ct=null}combinedSignal(t){return AbortSignal.any([this._signal,t.signal])}get mlManager(){return this.#ft}get useNewAltTextFlow(){return this.#tt}get useNewAltTextWhenAddingImage(){return this.#et}get hcmFilter(){return shadow(this,"hcmFilter",this.#wt?this.#it.addHCMFilter(this.#wt.foreground,this.#wt.background):"none")}get direction(){return shadow(this,"direction",getComputedStyle(this.#_t).direction)}get _highlightColors(){return shadow(this,"_highlightColors",this.#rt?new Map(this.#rt.split(",").map(t=>{(t=t.split("=").map(t=>t.trim()))[1]=t[1].toUpperCase();return t})):null)}get highlightColors(){const{_highlightColors:t}=this;if(!t)return shadow(this,"highlightColors",null);const e=new Map,i=!!this.#wt;for(const[s,n]of t){const t=s.endsWith("_HCM");i&&t?e.set(s.replace("_HCM",""),n):i||t||e.set(s,n)}return shadow(this,"highlightColors",e)}get highlightColorNames(){return shadow(this,"highlightColorNames",this.highlightColors?new Map(Array.from(this.highlightColors,t=>t.reverse())):null)}getNonHCMColor(t){if(!this._highlightColors)return t;const e=this.highlightColorNames.get(t);return this._highlightColors.get(e)||t}getNonHCMColorName(t){return this.highlightColorNames.get(t)||t}setCurrentDrawingSession(t){if(t){this.unselectAll();this.disableUserSelect(!0)}else this.disableUserSelect(!1);this.#X=t}setMainHighlightColorPicker(t){this.#gt=t}editAltText(t,e=!1){this.#z?.editAltText(this,t,e)}hasCommentManager(){return!!this.#j}editComment(t,e,i,s){this.#j?.showDialog(this,t,e,i,s)}selectComment(t,e){const i=this.#U.get(t),s=i?.getEditorByUID(e);s?.toggleComment(!0,!0)}updateComment(t){this.#j?.updateComment(t.getData())}updatePopupColor(t){this.#j?.updatePopupColor(t)}removeComment(t){this.#j?.removeComments([t.uid])}deleteComment(t,e){const undo=()=>{t.comment=e};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,"comment");this.toggleComment(null);t.comment=null},undo,mustExec:!0})}toggleComment(t,e,i=void 0){this.#j?.toggleCommentPopup(t,e,i)}makeCommentColor(t,e){return t&&this.#j?.makeCommentColor(t,e)||null}getCommentDialogElement(){return this.#j?.dialogElement||null}async waitForEditorsRendered(t){if(this.#U.has(t-1))return;const{resolve:e,promise:i}=Promise.withResolvers(),onEditorsRendered=i=>{if(i.pageNumber===t){this._eventBus._off("editorsrendered",onEditorsRendered);e()}};this._eventBus.on("editorsrendered",onEditorsRendered);await i}getSignature(t){this.#vt?.getSignature({uiManager:this,editor:t})}get signatureManager(){return this.#vt}switchToMode(t,e){this._eventBus.on("annotationeditormodechanged",e,{once:!0,signal:this._signal});this._eventBus.dispatch("showannotationeditorui",{source:this,mode:t})}setPreference(t,e){this._eventBus.dispatch("setpreference",{source:this,name:t,value:e})}onSetPreference({name:t,value:e}){if("enableNewAltTextWhenAddingImage"===t)this.#et=e}onPageChanging({pageNumber:t}){this.#q=t-1}deletePage(t){for(const e of this.getEditors(t))e.remove();this.#U.delete(t);this.#q===t&&(this.#q=0)}focusMainContainer(){this.#_t.focus()}findParent(t,e){for(const i of this.#U.values()){const{x:s,y:n,width:r,height:a}=i.div.getBoundingClientRect();if(t>=s&&t<=s+r&&e>=n&&e<=n+a)return i}return null}disableUserSelect(t=!1){this.#Pt.classList.toggle("noUserSelect",t)}addShouldRescale(t){this.#J.add(t)}removeShouldRescale(t){this.#J.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#J)t.onScaleChanging();this.#X?.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}#Rt({anchorNode:t}){return t.nodeType===Node.TEXT_NODE?t.parentElement:t}#Ot(t){const{currentLayer:e}=this;if(e.hasTextLayer(t))return e;for(const e of this.#U.values())if(e.hasTextLayer(t))return e;return null}highlightSelection(t="",e=!1){const i=document.getSelection();if(!i||i.isCollapsed)return;const{anchorNode:s,anchorOffset:n,focusNode:r,focusOffset:a}=i,o=i.toString(),l=this.#Rt(i).closest(".textLayer"),h=this.getSelectionBoxes(l);if(!h)return;i.empty();const d=this.#Ot(l),c=this.#bt===m.NONE,callback=()=>{const i=d?.createAndAddNewEditor({x:0,y:0},!1,{methodOfCreation:t,boxes:h,anchorNode:s,anchorOffset:n,focusNode:r,focusOffset:a,text:o});c&&this.showAllEditors("highlight",!0,!0);e&&i?.editComment()};c?this.switchToMode(m.HIGHLIGHT,callback):callback()}commentSelection(t=""){this.highlightSelection(t,!0)}#kt(t){this.commitOrRemove();this.currentLayer?.endDrawingSession(!1)}#Lt(){const t=document.getSelection();if(!t||t.isCollapsed)return;const e=this.#Rt(t).closest(".textLayer"),i=this.getSelectionBoxes(e);if(i){this.#ot||=new FloatingToolbar(this);this.#ot.show(e,i,"ltr"===this.direction)}}getAndRemoveDataFromAnnotationStorage(t){if(!this.#G)return null;const e=`${g}${t}`,i=this.#G.getRawValue(e);i&&this.#G.remove(e);return i}addToAnnotationStorage(t){t.isEmpty()||!this.#G||this.#G.has(t.id)||this.#G.setValue(t.id,t)}a11yAlert(t,e=null){const i=this.#Dt;if(i){i.setAttribute("data-l10n-id",t);e?i.setAttribute("data-l10n-args",JSON.stringify(e)):i.removeAttribute("data-l10n-args")}}#Nt(){const t=document.getSelection();if(!t||t.isCollapsed){if(this.#At){this.#ot?.hide();this.#At=null;this.#Ut({hasSelectedText:!1})}return}const{anchorNode:e}=t;if(e===this.#At)return;const i=this.#Rt(t).closest(".textLayer");if(i){this.#ot?.hide();this.#At=e;this.#Ut({hasSelectedText:!0});if(this.#bt===m.HIGHLIGHT||this.#bt===m.NONE){this.#bt===m.HIGHLIGHT&&this.showAllEditors("highlight",!0,!0);this.#at=this.isShiftKeyDown;if(!this.isShiftKeyDown){const t=this.#bt===m.HIGHLIGHT?this.#Ot(i):null;t?.toggleDrawing();if(this.#dt){const e=new AbortController,i=this.combinedSignal(e),pointerup=i=>{if("pointerup"!==i.type||0===i.button){e.abort();t?.toggleDrawing(!0);"pointerup"===i.type&&this.#Ht("main_toolbar")}};window.addEventListener("pointerup",pointerup,{signal:i});window.addEventListener("blur",pointerup,{signal:i})}else{t?.toggleDrawing(!0);this.#Ht("main_toolbar")}}}}else if(this.#At){this.#ot?.hide();this.#At=null;this.#Ut({hasSelectedText:!1})}}#Ht(t=""){this.#bt===m.HIGHLIGHT?this.highlightSelection(t):this.#Z&&this.#Lt()}#It(){document.addEventListener("selectionchange",this.#Nt.bind(this),{signal:this._signal})}#zt(){if(this.#nt)return;this.#nt=new AbortController;const t=this.combinedSignal(this.#nt);window.addEventListener("focus",this.focus.bind(this),{signal:t});window.addEventListener("blur",this.blur.bind(this),{signal:t})}#Gt(){this.#nt?.abort();this.#nt=null}blur(){this.isShiftKeyDown=!1;if(this.#at){this.#at=!1;this.#Ht("main_toolbar")}if(!this.hasSelection)return;const{activeElement:t}=document;for(const e of this.#yt)if(e.div.contains(t)){this.#pt=[e,t];e._focusEventsAllowed=!1;break}}focus(){if(!this.#pt)return;const[t,e]=this.#pt;this.#pt=null;e.addEventListener("focusin",()=>{t._focusEventsAllowed=!0},{once:!0,signal:this._signal});e.focus()}#Bt(){if(this.#ut)return;this.#ut=new AbortController;const t=this.combinedSignal(this.#ut);window.addEventListener("keydown",this.keydown.bind(this),{signal:t});window.addEventListener("keyup",this.keyup.bind(this),{signal:t})}#Wt(){this.#ut?.abort();this.#ut=null}#Vt(){if(this.#$)return;this.#$=new AbortController;const t=this.combinedSignal(this.#$);document.addEventListener("copy",this.copy.bind(this),{signal:t});document.addEventListener("cut",this.cut.bind(this),{signal:t});document.addEventListener("paste",this.paste.bind(this),{signal:t})}#jt(){this.#$?.abort();this.#$=null}#Ft(){const t=this._signal;document.addEventListener("dragover",this.dragOver.bind(this),{signal:t});document.addEventListener("drop",this.drop.bind(this),{signal:t})}addEditListeners(){this.#Bt();this.setEditingState(!0)}removeEditListeners(){this.#Wt();this.setEditingState(!1)}dragOver(t){for(const{type:e}of t.dataTransfer.items)for(const i of this.#Q)if(i.isHandlingMimeForPasting(e)){t.dataTransfer.dropEffect="copy";t.preventDefault();return}}drop(t){for(const e of t.dataTransfer.items)for(const i of this.#Q)if(i.isHandlingMimeForPasting(e.type)){i.paste(e,this.currentLayer);t.preventDefault();return}}copy(t){t.preventDefault();this.#O?.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#yt){const i=t.serialize(!0);i&&e.push(i)}0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}async paste(t){t.preventDefault();const{clipboardData:e}=t;for(const t of e.items)for(const e of this.#Q)if(e.isHandlingMimeForPasting(t.type)){e.paste(t,this.currentLayer);return}let i=e.getData("application/pdfjs");if(!i)return;try{i=JSON.parse(i)}catch(t){warn(`paste: "${t.message}".`);return}if(!Array.isArray(i))return;this.unselectAll();const s=this.currentLayer;try{const t=[];for(const e of i){const i=await s.deserialize(e);if(!i)return;t.push(i)}const cmd=()=>{for(const e of t)this.#$t(e);this.#Xt(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd,undo,mustExec:!0})}catch(t){warn(`paste: "${t.message}".`)}}keydown(t){this.isShiftKeyDown||"Shift"!==t.key||(this.isShiftKeyDown=!0);this.#bt===m.NONE||this.isEditorHandlingKeyboard||AnnotationEditorUIManager._keyboardManager.exec(this,t)}keyup(t){if(this.isShiftKeyDown&&"Shift"===t.key){this.isShiftKeyDown=!1;if(this.#at){this.#at=!1;this.#Ht("main_toolbar")}}}onEditingAction({name:t}){switch(t){case"undo":case"redo":case"delete":case"selectAll":this[t]();break;case"highlightSelection":this.highlightSelection("context_menu");break;case"commentSelection":this.commentSelection("context_menu")}}updatePageIndex(t,e){for(const i of this.getEditors(t))i.pageIndex=e;const i=this.#H.get(t);if(i){i.pageIndex=e;this.#U.set(e,i);this.#ht?i.enable():i.disable()}}startUpdatePages(){this.#H=new Map(this.#U);this.#U.clear()}endUpdatePages(){this.#H=null}clonePage(t,e){for(const i of this.getEditors(t)){const t=i.serialize(i.mode!==m.HIGHLIGHT);if(t){t.pageIndex=e;t.id=this.getId();t.isClone=!0;delete t.popupRef;this.#G.setValue(t.id,t)}}}findClonesForPage(t){const e=[],{pageIndex:i}=t;for(const[s,n]of this.#G)if(n.pageIndex===i&&n.isClone){this.#G.remove(s);e.push(t.deserialize(n).then(e=>{if(e){e.isClone=!0;t.addOrRebuild(e)}}))}return Promise.all(e)}#Ut(t){if(Object.entries(t).some(([t,e])=>this.#Et[t]!==e)){this._eventBus.dispatch("editingstateschanged",{source:this,details:Object.assign(this.#Et,t)});this.#bt===m.HIGHLIGHT&&!1===t.hasSelectedEditor&&this.#qt([[f.HIGHLIGHT_FREE,!0]])}}#qt(t){this._eventBus.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#zt();this.#Vt();this.#Ut({isEditing:this.#bt!==m.NONE,isEmpty:this.#Kt(),hasSomethingToUndo:this.#V.hasSomethingToUndo(),hasSomethingToRedo:this.#V.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Gt();this.#jt();this.#Ut({isEditing:!1});this.disableUserSelect(!1)}}registerEditorTypes(t){if(!this.#Q){this.#Q=t;for(const t of this.#Q)this.#qt(t.defaultPropertiesToUpdate)}}getId(){return this.#lt.id}get currentLayer(){return this.#U.get(this.#q)}getLayer(t){return this.#U.get(t)}get currentPageIndex(){return this.#q}addLayer(t){this.#U.set(t.pageIndex,t);this.#ht?t.enable():t.disable()}removeLayer(t){this.#U.delete(t.pageIndex)}async updateMode(t,e=null,i=!1,s=!1,n=!1,r=!1){if(this.#bt!==t){if(this.#Mt){await this.#Mt.promise;if(!this.#Mt)return}this.#Mt=Promise.withResolvers();this.#X?.commitOrRemove();this.#bt===m.POPUP&&this.#j?.hideSidebar();this.#j?.destroyPopup();this.#bt=t;if(t!==m.NONE){for(const t of this.#N.values())t.addStandaloneCommentButton();t===m.SIGNATURE&&await(this.#vt?.loadSignatures());i&&CurrentPointers.clearPointerType();this.setEditingState(!0);await this.#Yt();this.unselectAll();for(const e of this.#U.values())e.updateMode(t);if(t===m.POPUP){this.#L||=await this.#Ct.getAnnotationsByType(new Set(this.#Q.map(t=>t._editorType)));const t=new Set,e=[];for(const i of this.#N.values()){const{annotationElementId:s,hasComment:n,deleted:r}=i;s&&t.add(s);n&&!r&&e.push(i.getData())}for(const i of this.#L){const{id:s,popupRef:n,contentsObj:r}=i;n&&r?.str&&!t.has(s)&&!this.#K.has(s)&&e.push(i)}this.#j?.showSidebar(e)}if(e){for(const t of this.#N.values())if(t.uid===e){this.setSelected(t);r?t.editComment():n?t.enterInEditMode():t.focus()}else t.unselect();this.#Mt.resolve()}else{s&&this.addNewEditorFromKeyboard();this.#Mt.resolve()}}else{this.setEditingState(!1);this.#Qt();for(const t of this.#N.values())t.hideStandaloneCommentButton();this._editorUndoBar?.hide();this.toggleComment(null);this.#Mt.resolve()}}}addNewEditorFromKeyboard(){this.currentLayer.canCreateNewEmptyEditor()&&this.currentLayer.addNewEditor()}updateToolbar(t){t.mode!==this.#bt&&this._eventBus.dispatch("switchannotationeditormode",{source:this,...t})}updateParams(t,e){if(this.#Q){switch(t){case f.CREATE:this.currentLayer.addNewEditor(e);return;case f.HIGHLIGHT_SHOW_ALL:this._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:{type:"highlight",action:"toggle_visibility"}}});(this.#xt||=new Map).set(t,e);this.showAllEditors("highlight",e)}if(this.hasSelection)for(const i of this.#yt)i.updateParams(t,e);else for(const i of this.#Q)i.updateDefaultParams(t,e)}}showAllEditors(t,e,i=!1){for(const i of this.#N.values())i.editorType===t&&i.show(e);(this.#xt?.get(f.HIGHLIGHT_SHOW_ALL)??!0)!==e&&this.#qt([[f.HIGHLIGHT_SHOW_ALL,e]])}enableWaiting(t=!1){if(this.#ct!==t){this.#ct=t;for(const e of this.#U.values()){t?e.disableClick():e.enableClick();e.div.classList.toggle("waiting",t)}}}async#Yt(){if(!this.#ht){this.#ht=!0;const t=[];for(const e of this.#U.values())t.push(e.enable());await Promise.all(t);for(const t of this.#N.values())t.enable()}}#Qt(){this.unselectAll();if(this.#ht){this.#ht=!1;for(const t of this.#U.values())t.disable();for(const t of this.#N.values())t.disable()}}*getEditors(t){for(const e of this.#N.values())e.pageIndex===t&&(yield e)}getEditor(t){return this.#N.get(t)}addEditor(t){this.#N.set(t.id,t)}removeEditor(t){if(t.div.contains(document.activeElement)){this.#st&&clearTimeout(this.#st);this.#st=setTimeout(()=>{this.focusMainContainer();this.#st=null},0)}this.#N.delete(t.id);t.annotationElementId&&this.#mt?.delete(t.annotationElementId);this.unselect(t);t.annotationElementId&&this.#K.has(t.annotationElementId)||this.#G?.remove(t.id)}addDeletedAnnotationElement(t){this.#K.add(t.annotationElementId);this.addChangedExistingAnnotation(t);t.deleted=!0}isDeletedAnnotationElement(t){return this.#K.has(t)}removeDeletedAnnotationElement(t){this.#K.delete(t.annotationElementId);this.removeChangedExistingAnnotation(t);t.deleted=!1}#$t(t){const e=this.#U.get(t.pageIndex);if(e)e.addOrRebuild(t);else{this.addEditor(t);this.addToAnnotationStorage(t)}}setActiveEditor(t){if(this.#O!==t){this.#O=t;t&&this.#qt(t.propertiesToUpdate)}}get#Jt(){let t=null;for(t of this.#yt);return t}updateUI(t){this.#Jt===t&&this.#qt(t.propertiesToUpdate)}updateUIForDefaultProperties(t){this.#qt(t.defaultPropertiesToUpdate)}toggleSelected(t){if(this.#yt.has(t)){this.#yt.delete(t);t.unselect();this.#Ut({hasSelectedEditor:this.hasSelection})}else{this.#yt.add(t);t.select();this.#qt(t.propertiesToUpdate);this.#Ut({hasSelectedEditor:!0})}}setSelected(t){this.updateToolbar({mode:t.mode,editId:t.uid});this.#X?.commitOrRemove();for(const e of this.#yt)e!==t&&e.unselect();this.#j?.destroyPopup();this.#yt.clear();this.#yt.add(t);t.select();this.#qt(t.propertiesToUpdate);this.#Ut({hasSelectedEditor:!0})}isSelected(t){return this.#yt.has(t)}get firstSelectedEditor(){return this.#yt.values().next().value}unselect(t){t.unselect();this.#yt.delete(t);this.#Ut({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#yt.size}get isEnterHandled(){return 1===this.#yt.size&&this.firstSelectedEditor.isEnterHandled}undo(){this.#V.undo();this.#Ut({hasSomethingToUndo:this.#V.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#Kt()});this._editorUndoBar?.hide()}redo(){this.#V.redo();this.#Ut({hasSomethingToUndo:!0,hasSomethingToRedo:this.#V.hasSomethingToRedo(),isEmpty:this.#Kt()})}addCommands(t){this.#V.add(t);this.#Ut({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#Kt()})}cleanUndoStack(t){this.#V.cleanType(t)}#Kt(){if(0===this.#N.size)return!0;if(1===this.#N.size)for(const t of this.#N.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();const t=this.currentLayer?.endDrawingSession(!0);if(!this.hasSelection&&!t)return;const e=t?[t]:[...this.#yt],undo=()=>{for(const t of e)this.#$t(t)};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,1===e.length?e[0].editorType:e.length);for(const t of e)t.remove()},undo,mustExec:!0})}commitOrRemove(){this.#O?.commitOrRemove()}hasSomethingToControl(){return this.#O||this.hasSelection}#Xt(t){for(const t of this.#yt)t.unselect();this.#yt.clear();for(const e of t)if(!e.isEmpty()){this.#yt.add(e);e.select()}this.#Ut({hasSelectedEditor:this.hasSelection})}selectAll(){for(const t of this.#yt)t.commit();this.#Xt(this.#N.values())}unselectAll(){if(this.#O){this.#O.commitOrRemove();if(this.#bt!==m.NONE)return}if(!this.#X?.commitOrRemove()){this.#j?.destroyPopup();if(this.hasSelection){for(const t of this.#yt)t.unselect();this.#yt.clear();this.#Ut({hasSelectedEditor:!1})}}}translateSelectedEditors(t,e,i=!1){i||this.commitOrRemove();if(!this.hasSelection)return;this.#Tt[0]+=t;this.#Tt[1]+=e;const[s,n]=this.#Tt,r=[...this.#yt];this.#St&&clearTimeout(this.#St);this.#St=setTimeout(()=>{this.#St=null;this.#Tt[0]=this.#Tt[1]=0;this.addCommands({cmd:()=>{for(const t of r)if(this.#N.has(t.id)){t.translateInPage(s,n);t.translationDone()}},undo:()=>{for(const t of r)if(this.#N.has(t.id)){t.translateInPage(-s,-n);t.translationDone()}},mustExec:!1})},1e3);for(const i of r){i.translateInPage(t,e);i.translationDone()}}setUpDragSession(){if(this.hasSelection){this.disableUserSelect(!0);this.#Y=new Map;for(const t of this.#yt)this.#Y.set(t,{savedX:t.x,savedY:t.y,savedPageIndex:t.pageIndex,newX:0,newY:0,newPageIndex:-1})}}endDragSession(){if(!this.#Y)return!1;this.disableUserSelect(!1);const t=this.#Y;this.#Y=null;let e=!1;for(const[{x:i,y:s,pageIndex:n},r]of t){r.newX=i;r.newY=s;r.newPageIndex=n;e||=i!==r.savedX||s!==r.savedY||n!==r.savedPageIndex}if(!e)return!1;const move=(t,e,i,s)=>{if(this.#N.has(t.id)){const n=this.#U.get(s);if(n)t._setParentAndPosition(n,e,i);else{t.pageIndex=s;t.x=e;t.y=i}}};this.addCommands({cmd:()=>{for(const[e,{newX:i,newY:s,newPageIndex:n}]of t)move(e,i,s,n)},undo:()=>{for(const[e,{savedX:i,savedY:s,savedPageIndex:n}]of t)move(e,i,s,n)},mustExec:!0});return!0}dragSelectedEditors(t,e){if(this.#Y)for(const i of this.#Y.keys())i.drag(t,e)}rebuild(t){if(null===t.parent){const e=this.getLayer(t.pageIndex);if(e){e.changeParent(t);e.addOrRebuild(t)}else{this.addEditor(t);this.addToAnnotationStorage(t);t.rebuild()}}else t.parent.addOrRebuild(t)}get isEditorHandlingKeyboard(){return this.getActive()?.shouldGetKeyboardEvents()||1===this.#yt.size&&this.firstSelectedEditor.shouldGetKeyboardEvents()}isActive(t){return this.#O===t}getActive(){return this.#O}getMode(){return this.#bt}isEditingMode(){return this.#bt!==m.NONE}get imageManager(){return shadow(this,"imageManager",new ImageManager)}getSelectionBoxes(t){if(!t)return null;const e=document.getSelection();for(let i=0,s=e.rangeCount;i({x:(e-s)/r,y:1-(t+a-i)/n,width:o/r,height:a/n});break;case"180":a=(t,e,a,o)=>({x:1-(t+a-i)/n,y:1-(e+o-s)/r,width:a/n,height:o/r});break;case"270":a=(t,e,a,o)=>({x:1-(e+o-s)/r,y:(t-i)/n,width:o/r,height:a/n});break;default:a=(t,e,a,o)=>({x:(t-i)/n,y:(e-s)/r,width:a/n,height:o/r})}const o=[];for(let t=0,i=e.rangeCount;tt.stopPropagation(),{signal:i});const onClick=t=>{t.preventDefault();this.#a._uiManager.editAltText(this.#a);this.#le&&this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_clicked",data:{label:this.#de}})};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",e=>{if(e.target===t&&"Enter"===e.key){this.#ne=!0;onClick(e)}},{signal:i});await this.#ce();return t}get#de(){return(this.#l?"added":null===this.#l&&this.guessedText&&"review")||"missing"}finish(){if(this.#te){this.#te.focus({focusVisible:this.#ne});this.#ne=!1}}isEmpty(){return this.#le?null===this.#l:!this.#l&&!this.#Zt}hasData(){return this.#le?null!==this.#l||!!this.#ae:this.isEmpty()}get guessedText(){return this.#ae}async setGuessedText(t){if(null===this.#l){this.#ae=t;this.#oe=await AltText._l10n.get("pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",{generatedAltText:t});this.#ce()}}toggleAltTextBadge(t=!1){if(this.#le&&!this.#l){if(!this.#re){const t=this.#re=document.createElement("div");t.className="noAltTextBadge";this.#a.div.append(t)}this.#re.classList.toggle("hidden",!t)}else{this.#re?.remove();this.#re=null}}serialize(t){let e=this.#l;t||this.#ae!==e||(e=this.#oe);return{altText:e,decorative:this.#Zt,guessedText:this.#ae,textWithDisclaimer:this.#oe}}get data(){return{altText:this.#l,decorative:this.#Zt}}set data({altText:t,decorative:e,guessedText:i,textWithDisclaimer:s,cancel:n=!1}){if(i){this.#ae=i;this.#oe=s}if(this.#l!==t||this.#Zt!==e){if(!n){this.#l=t;this.#Zt=e}this.#ce()}}toggle(t=!1){if(this.#te){if(!t&&this.#se){clearTimeout(this.#se);this.#se=null}this.#te.disabled=!t}}shown(){this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_displayed",data:{label:this.#de}})}destroy(){this.#te?.remove();this.#te=null;this.#ee=null;this.#ie=null;this.#re?.remove();this.#re=null}async#ce(){const t=this.#te;if(!t)return;if(this.#le){t.classList.toggle("done",!!this.#l);t.setAttribute("data-l10n-id",AltText.#he[this.#de]);this.#ee?.setAttribute("data-l10n-id",AltText.#he[`${this.#de}-label`]);if(!this.#l){this.#ie?.remove();return}}else{if(!this.#l&&!this.#Zt){t.classList.remove("done");this.#ie?.remove();return}t.classList.add("done");t.setAttribute("data-l10n-id","pdfjs-editor-alt-text-edit-button")}let e=this.#ie;if(!e){this.#ie=e=document.createElement("span");e.className="tooltip";e.setAttribute("role","tooltip");e.id=`alt-text-tooltip-${this.#a.id}`;const i=100,s=this.#a._uiManager._signal;s.addEventListener("abort",()=>{clearTimeout(this.#se);this.#se=null},{once:!0});t.addEventListener("mouseenter",()=>{this.#se=setTimeout(()=>{this.#se=null;this.#ie.classList.add("show");this.#a._reportTelemetry({action:"alt_text_tooltip"})},i)},{signal:s});t.addEventListener("mouseleave",()=>{if(this.#se){clearTimeout(this.#se);this.#se=null}this.#ie?.classList.remove("show")},{signal:s})}if(this.#Zt)e.setAttribute("data-l10n-id","pdfjs-editor-alt-text-decorative-tooltip");else{e.removeAttribute("data-l10n-id");e.textContent=this.#l}e.parentNode||t.append(e);const i=this.#a.getElementForAltText();i?.setAttribute("aria-describedby",e.id)}}class Comment{#ue=null;#pe=null;#ge=!1;#a=null;#me=null;#fe=null;#be=null;#ye=null;#Ae=!1;#ve=null;constructor(t){this.#a=t}renderForToolbar(){const t=this.#pe=document.createElement("button");t.className="comment";return this.#A(t,!1)}renderForStandalone(){const t=this.#ue=document.createElement("button");t.className="annotationCommentButton";const e=this.#a.commentButtonPosition;if(e){const{style:i}=t;i.insetInlineEnd=`calc(${100*("ltr"===this.#a._uiManager.direction?1-e[0]:e[0])}% - var(--comment-button-dim))`;i.top=`calc(${100*e[1]}% - var(--comment-button-dim))`;const s=this.#a.commentButtonColor;s&&(i.backgroundColor=s)}return this.#A(t,!0)}focusButton(){setTimeout(()=>{(this.#ue??this.#pe)?.focus()},0)}onUpdatedColor(){if(!this.#ue)return;const t=this.#a.commentButtonColor;t&&(this.#ue.style.backgroundColor=t);this.#a._uiManager.updatePopupColor(this.#a)}get commentButtonWidth(){return(this.#ue?.getBoundingClientRect().width??0)/this.#a.parent.boundingClientRect.width}get commentPopupPositionInLayer(){if(this.#ve)return this.#ve;if(!this.#ue)return null;const{x:t,y:e,height:i}=this.#ue.getBoundingClientRect(),{x:s,y:n,width:r,height:a}=this.#a.parent.boundingClientRect;return[(t-s)/r,(e+i-n)/a]}set commentPopupPositionInLayer(t){this.#ve=t}hasDefaultPopupPosition(){return null===this.#ve}removeStandaloneCommentButton(){this.#ue?.remove();this.#ue=null}removeToolbarCommentButton(){this.#pe?.remove();this.#pe=null}setCommentButtonStates({selected:t,hasPopup:e}){if(this.#ue){this.#ue.classList.toggle("selected",t);this.#ue.ariaExpanded=e}}#A(t,e){if(!this.#a._uiManager.hasCommentManager())return null;t.tabIndex="0";t.ariaHasPopup="dialog";if(e){t.ariaControls="commentPopup";t.setAttribute("data-l10n-id","pdfjs-show-comment-button")}else{t.ariaControlsElements=[this.#a._uiManager.getCommentDialogElement()];t.setAttribute("data-l10n-id","pdfjs-editor-add-comment-button")}const i=this.#a._uiManager._signal;if(!(i instanceof AbortSignal)||i.aborted)return t;t.addEventListener("contextmenu",noContextMenu,{signal:i});if(e){t.addEventListener("focusin",t=>{this.#a._focusEventsAllowed=!1;stopEvent(t)},{capture:!0,signal:i});t.addEventListener("focusout",t=>{this.#a._focusEventsAllowed=!0;stopEvent(t)},{capture:!0,signal:i})}t.addEventListener("pointerdown",t=>t.stopPropagation(),{signal:i});const onClick=e=>{e.preventDefault();t===this.#pe?this.edit():this.#a.toggleComment(!0)};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",e=>{if(e.target===t&&"Enter"===e.key){this.#ge=!0;onClick(e)}},{signal:i});t.addEventListener("pointerenter",()=>{this.#a.toggleComment(!1,!0)},{signal:i});t.addEventListener("pointerleave",()=>{this.#a.toggleComment(!1,!1)},{signal:i});return t}edit(t){const e=this.commentPopupPositionInLayer;let i,s;if(e)[i,s]=e;else{[i,s]=this.#a.commentButtonPosition;const{width:t,height:e,x:n,y:r}=this.#a;i=n+i*t;s=r+s*e}const n=this.#a.parent.boundingClientRect,{x:r,y:a,width:o,height:l}=n;this.#a._uiManager.editComment(this.#a,r+i*o,a+s*l,{...t,parentDimensions:n})}finish(){if(this.#pe){this.#pe.focus({focusVisible:this.#ge});this.#ge=!1}}isDeleted(){return this.#Ae||""===this.#be}isEmpty(){return null===this.#be}hasBeenEdited(){return this.isDeleted()||this.#be!==this.#me}serialize(){return this.data}get data(){return{text:this.#be,richText:this.#fe,date:this.#ye,deleted:this.isDeleted()}}set data(t){t!==this.#be&&(this.#fe=null);if(null!==t){this.#be=t;this.#ye=new Date;this.#Ae=!1}else{this.#be="";this.#Ae=!0}}restoreData({text:t,richText:e,date:i}){this.#be=t;this.#fe=e;this.#ye=i;this.#Ae=!1}setInitialText(t,e=null){this.#me=t;this.data=t;this.#ye=null;this.#fe=e}shown(){}destroy(){this.#pe?.remove();this.#pe=null;this.#ue?.remove();this.#ue=null;this.#be="";this.#fe=null;this.#ye=null;this.#a=null;this.#ge=!1;this.#Ae=!1}}class TouchManager{#_t;#we=!1;#xe=null;#Ce;#Ee;#Te;#Se;#_e=null;#Pe;#De=null;#Me;#ke=null;constructor({container:t,isPinchingDisabled:e=null,isPinchingStopped:i=null,onPinchStart:s=null,onPinching:n=null,onPinchEnd:r=null,signal:a}){this.#_t=t;this.#xe=i;this.#Ce=e;this.#Ee=s;this.#Te=n;this.#Se=r;this.#Me=new AbortController;this.#Pe=AbortSignal.any([a,this.#Me.signal]);t.addEventListener("touchstart",this.#Ie.bind(this),{passive:!1,signal:this.#Pe})}get MIN_TOUCH_DISTANCE_TO_PINCH(){return 35/OutputScale.pixelRatio}#Ie(t){if(this.#Ce?.())return;if(1===t.touches.length){if(this.#_e)return;const t=this.#_e=new AbortController,e=AbortSignal.any([this.#Pe,t.signal]),i=this.#_t,s={capture:!0,signal:e,passive:!1},cancelPointerDown=t=>{if("touch"===t.pointerType){this.#_e?.abort();this.#_e=null}};i.addEventListener("pointerdown",t=>{if("touch"===t.pointerType){stopEvent(t);cancelPointerDown(t)}},s);i.addEventListener("pointerup",cancelPointerDown,s);i.addEventListener("pointercancel",cancelPointerDown,s);return}if(!this.#ke){this.#ke=new AbortController;const t=AbortSignal.any([this.#Pe,this.#ke.signal]),e=this.#_t,i={signal:t,capture:!1,passive:!1};e.addEventListener("touchmove",this.#Fe.bind(this),i);const s=this.#Be.bind(this);e.addEventListener("touchend",s,i);e.addEventListener("touchcancel",s,i);i.capture=!0;e.addEventListener("pointerdown",stopEvent,i);e.addEventListener("pointermove",stopEvent,i);e.addEventListener("pointercancel",stopEvent,i);e.addEventListener("pointerup",stopEvent,i);this.#Ee?.()}stopEvent(t);if(2!==t.touches.length||this.#xe?.()){this.#De=null;return}let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);this.#De={touch0X:e.screenX,touch0Y:e.screenY,touch1X:i.screenX,touch1Y:i.screenY}}#Fe(t){if(!this.#De||2!==t.touches.length)return;stopEvent(t);let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);const{screenX:s,screenY:n}=e,{screenX:r,screenY:a}=i,o=this.#De,{touch0X:l,touch0Y:h,touch1X:d,touch1Y:c}=o,u=d-l,p=c-h,g=r-s,m=a-n,f=Math.hypot(g,m)||1,b=Math.hypot(u,p)||1;if(!this.#we&&Math.abs(b-f)<=TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH)return;o.touch0X=s;o.touch0Y=n;o.touch1X=r;o.touch1Y=a;if(!this.#we){this.#we=!0;return}const y=[(s+r)/2,(n+a)/2];this.#Te?.(y,b,f)}#Be(t){if(!(t.touches.length>=2)){if(this.#ke){this.#ke.abort();this.#ke=null;this.#Se?.()}if(this.#De){stopEvent(t);this.#De=null;this.#we=!1}}}destroy(){this.#Me?.abort();this.#Me=null;this.#_e?.abort();this.#_e=null}}class AnnotationEditor{#Re=null;#Oe=null;#l=null;#h=null;#ue=null;#Le=!1;#Ne=null;#Ue="";#He=null;#ze=null;#Ge=null;#We=null;#Ve=null;#je="";#$e=!1;#Xe=null;#qe=!1;#Ke=!1;#Ye=!1;#Qe=null;#Je=0;#Ze=0;#ti=null;#ei=null;isSelected=!1;_isCopy=!1;_editToolbar=null;_initialOptions=Object.create(null);_initialData=null;_isVisible=!0;_uiManager=null;_focusEventsAllowed=!0;static _l10n=null;static _l10nResizer=null;#ii=!1;#si=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new ColorManager;static _zIndex=1;static _telemetryTimeout=1e3;static get _resizerKeyboardManager(){const t=AnnotationEditor.prototype._resizeWithKeyboard,e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_resizerKeyboardManager",new KeyboardManager([[["ArrowLeft","mac+ArrowLeft"],t,{args:[-e,0]}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t,{args:[-i,0]}],[["ArrowRight","mac+ArrowRight"],t,{args:[e,0]}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t,{args:[i,0]}],[["ArrowUp","mac+ArrowUp"],t,{args:[0,-e]}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t,{args:[0,-i]}],[["ArrowDown","mac+ArrowDown"],t,{args:[0,e]}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t,{args:[0,i]}],[["Escape","mac+Escape"],AnnotationEditor.prototype._stopResizingWithKeyboard]]))}constructor(t){this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;this.annotationElementId=t.annotationElementId||null;this.creationDate=t.creationDate||new Date;this.modificationDate=t.modificationDate||null;this.canAddComment=!0;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:r}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[n,r];const[a,o]=this.parentDimensions;this.x=t.x/a;this.y=t.y/o;this.isAttachedToDOM=!1;this.deleted=!1}updatePageIndex(t){this.pageIndex=t}get editorType(){return Object.getPrototypeOf(this).constructor._type}get mode(){return Object.getPrototypeOf(this).constructor._editorType}static get isDrawer(){return!1}static get _defaultLineColor(){return shadow(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t._uiManager.getId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e){AnnotationEditor._l10n??=t;AnnotationEditor._l10nResizer||=Object.freeze({topLeft:"pdfjs-editor-resizer-top-left",topMiddle:"pdfjs-editor-resizer-top-middle",topRight:"pdfjs-editor-resizer-top-right",middleRight:"pdfjs-editor-resizer-middle-right",bottomRight:"pdfjs-editor-resizer-bottom-right",bottomMiddle:"pdfjs-editor-resizer-bottom-middle",bottomLeft:"pdfjs-editor-resizer-bottom-left",middleLeft:"pdfjs-editor-resizer-middle-left"});if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){unreachable("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#ii}set _isDraggable(t){this.#ii=t;this.div?.classList.toggle("draggable",t)}get uid(){return this.annotationElementId||this.id}get isEnterHandled(){return!0}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#si}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}else{this.#ni();this.#We?.remove();this.#We=null}this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#$e?this.#$e=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.isInEditMode()&&this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,r]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/r;this.fixAndSetPosition()}_moveAfterPaste(t,e){if(this.isClone){delete this.isClone;return}const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s);this._onTranslated()}#ri([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this._onTranslating(this.x,this.y);this.fixAndSetPosition()}translate(t,e){this.#ri(this.parentDimensions,t,e)}translateInPage(t,e){this.#Xe||=[this.x,this.y,this.width,this.height];this.#ri(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}translationDone(){this._onTranslated(this.x,this.y)}drag(t,e){this.#Xe||=[this.x,this.y,this.width,this.height];const{div:i,parentDimensions:[s,n]}=this;this.x+=t/s;this.y+=e/n;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:r,y:a}=this;const[o,l]=this.getBaseTranslation();r+=o;a+=l;const{style:h}=i;h.left=`${(100*r).toFixed(2)}%`;h.top=`${(100*a).toFixed(2)}%`;this._onTranslating(r,a);i.scrollIntoView({block:"nearest"})}_onTranslating(t,e){}_onTranslated(t,e){}get _hasBeenMoved(){return!!this.#Xe&&(this.#Xe[0]!==this.x||this.#Xe[1]!==this.y)}get _hasBeenResized(){return!!this.#Xe&&(this.#Xe[2]!==this.width||this.#Xe[3]!==this.height)}getBaseTranslation(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}get _mustFixPosition(){return!0}fixAndSetPosition(t=this.rotation){const{div:{style:e},pageDimensions:[i,s]}=this;let{x:n,y:r,width:a,height:o}=this;a*=i;o*=s;n*=i;r*=s;if(this._mustFixPosition)switch(t){case 0:n=MathClamp(n,0,i-a);r=MathClamp(r,0,s-o);break;case 90:n=MathClamp(n,0,i-o);r=MathClamp(r,a,s);break;case 180:n=MathClamp(n,a,i);r=MathClamp(r,o,s);break;case 270:n=MathClamp(n,o,i);r=MathClamp(r,0,s-a)}this.x=n/=i;this.y=r/=s;const[l,h]=this.getBaseTranslation();n+=l;r+=h;e.left=`${(100*n).toFixed(2)}%`;e.top=`${(100*r).toFixed(2)}%`;this.moveInDOM()}static#ai(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#ai(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#ai(t,e,360-this.parentRotation)}#oi(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this;return[e*t,i*t]}setDims(){const{div:{style:t},width:e,height:i}=this;t.width=`${(100*e).toFixed(2)}%`;t.height=`${(100*i).toFixed(2)}%`}getInitialTranslation(){return[0,0]}#li(){if(this.#He)return;this.#He=document.createElement("div");this.#He.classList.add("resizers");const t=this._willKeepAspectRatio?["topLeft","topRight","bottomRight","bottomLeft"]:["topLeft","topMiddle","topRight","middleRight","bottomRight","bottomMiddle","bottomLeft","middleLeft"],e=this._uiManager._signal;for(const i of t){const t=document.createElement("div");this.#He.append(t);t.classList.add("resizer",i);t.setAttribute("data-resizer-name",i);t.addEventListener("pointerdown",this.#hi.bind(this,i),{signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e});t.tabIndex=-1}this.div.prepend(this.#He)}#hi(t,e){e.preventDefault();const{isMac:i}=FeatureTest.platform;if(0!==e.button||e.ctrlKey&&i)return;this.#l?.toggle(!1);const s=this._isDraggable;this._isDraggable=!1;this.#ze=[e.screenX,e.screenY];const n=new AbortController,r=this._uiManager.combinedSignal(n);this.parent.togglePointerEvents(!1);window.addEventListener("pointermove",this.#di.bind(this,t),{passive:!0,capture:!0,signal:r});window.addEventListener("touchmove",stopEvent,{passive:!1,signal:r});window.addEventListener("contextmenu",noContextMenu,{signal:r});this.#Ge={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const a=this.parent.div.style.cursor,o=this.div.style.cursor;this.div.style.cursor=this.parent.div.style.cursor=window.getComputedStyle(e.target).cursor;const pointerUpCallback=()=>{n.abort();this.parent.togglePointerEvents(!0);this.#l?.toggle(!0);this._isDraggable=s;this.parent.div.style.cursor=a;this.div.style.cursor=o;this.#ci()};window.addEventListener("pointerup",pointerUpCallback,{signal:r});window.addEventListener("blur",pointerUpCallback,{signal:r})}#ui(t,e,i,s){this.width=i;this.height=s;this.x=t;this.y=e;this.setDims();this.fixAndSetPosition();this._onResized()}_onResized(){}#ci(){if(!this.#Ge)return;const{savedX:t,savedY:e,savedWidth:i,savedHeight:s}=this.#Ge;this.#Ge=null;const n=this.x,r=this.y,a=this.width,o=this.height;n===t&&r===e&&a===i&&o===s||this.addCommands({cmd:this.#ui.bind(this,n,r,a,o),undo:this.#ui.bind(this,t,e,i,s),mustExec:!0})}static _round(t){return Math.round(1e4*t)/1e4}#di(t,e){const[i,s]=this.parentDimensions,n=this.x,r=this.y,a=this.width,o=this.height,l=AnnotationEditor.MIN_SIZE/i,h=AnnotationEditor.MIN_SIZE/s,d=this.#oi(this.rotation),transf=(t,e)=>[d[0]*t+d[2]*e,d[1]*t+d[3]*e],c=this.#oi(360-this.rotation);let u,p,g=!1,m=!1;switch(t){case"topLeft":g=!0;u=(t,e)=>[0,0];p=(t,e)=>[t,e];break;case"topMiddle":u=(t,e)=>[t/2,0];p=(t,e)=>[t/2,e];break;case"topRight":g=!0;u=(t,e)=>[t,0];p=(t,e)=>[0,e];break;case"middleRight":m=!0;u=(t,e)=>[t,e/2];p=(t,e)=>[0,e/2];break;case"bottomRight":g=!0;u=(t,e)=>[t,e];p=(t,e)=>[0,0];break;case"bottomMiddle":u=(t,e)=>[t/2,e];p=(t,e)=>[t/2,0];break;case"bottomLeft":g=!0;u=(t,e)=>[0,e];p=(t,e)=>[t,0];break;case"middleLeft":m=!0;u=(t,e)=>[0,e/2];p=(t,e)=>[t,e/2]}const f=u(a,o),b=p(a,o);let y=transf(...b);const A=AnnotationEditor._round(n+y[0]),v=AnnotationEditor._round(r+y[1]);let w,x,C=1,E=1;if(e.fromKeyboard)({deltaX:w,deltaY:x}=e);else{const{screenX:t,screenY:i}=e,[s,n]=this.#ze;[w,x]=this.screenToPageTranslation(t-s,i-n);this.#ze[0]=t;this.#ze[1]=i}[w,x]=(T=w/i,S=x/s,[c[0]*T+c[2]*S,c[1]*T+c[3]*S]);var T,S;if(g){const t=Math.hypot(a,o);C=E=Math.max(Math.min(Math.hypot(b[0]-f[0]-w,b[1]-f[1]-x)/t,1/a,1/o),l/a,h/o)}else m?C=MathClamp(Math.abs(b[0]-f[0]-w),l,1)/a:E=MathClamp(Math.abs(b[1]-f[1]-x),h,1)/o;const _=AnnotationEditor._round(a*C),P=AnnotationEditor._round(o*E);y=transf(...p(_,P));const D=A-y[0],M=v-y[1];this.#Xe||=[this.x,this.y,this.width,this.height];this.width=_;this.height=P;this.x=D;this.y=M;this.setDims();this.fixAndSetPosition();this._onResizing()}_onResizing(){}altTextFinish(){this.#l?.finish()}get toolbarButtons(){return null}async addEditToolbar(){if(this._editToolbar||this.#Ke)return this._editToolbar;this._editToolbar=new EditorToolbar(this);this.div.append(this._editToolbar.render());const{toolbarButtons:t}=this;if(t)for(const[e,i]of t)await this._editToolbar.addButton(e,i);this.hasComment||this._editToolbar.addButton("comment",this.addCommentButton());this._editToolbar.addButton("delete");return this._editToolbar}addCommentButtonInToolbar(){this._editToolbar?.addButtonBefore("comment",this.addCommentButton(),".deleteButton")}removeCommentButtonFromToolbar(){this._editToolbar?.removeButton("comment")}removeEditToolbar(){this._editToolbar?.remove();this._editToolbar=null;this.#l?.destroy()}addContainer(t){const e=this._editToolbar?.div;e?e.before(t):this.div.append(t)}getClientDimensions(){return this.div.getBoundingClientRect()}createAltText(){if(!this.#l){AltText.initialize(AnnotationEditor._l10n);this.#l=new AltText(this);if(this.#Re){this.#l.data=this.#Re;this.#Re=null}}return this.#l}get altTextData(){return this.#l?.data}set altTextData(t){this.#l&&(this.#l.data=t)}get guessedAltText(){return this.#l?.guessedText}async setGuessedAltText(t){await(this.#l?.setGuessedText(t))}serializeAltText(t){return this.#l?.serialize(t)}hasAltText(){return!!this.#l&&!this.#l.isEmpty()}hasAltTextData(){return this.#l?.hasData()??!1}focusCommentButton(){this.#h?.focusButton()}addCommentButton(){return this.canAddComment?this.#h||=new Comment(this):null}addStandaloneCommentButton(){if(this._uiManager.hasCommentManager())if(this.#ue)this._uiManager.isEditingMode()&&this.#ue.classList.remove("hidden");else if(this.hasComment){this.#ue=this.#h.renderForStandalone();this.div.append(this.#ue)}}removeStandaloneCommentButton(){this.#h.removeStandaloneCommentButton();this.#ue=null}hideStandaloneCommentButton(){this.#ue?.classList.add("hidden")}get comment(){if(!this.#h)return null;const{data:{richText:t,text:e,date:i,deleted:s}}=this.#h;return{text:e,richText:t,date:i,deleted:s,color:this.getNonHCMColor(),opacity:this.opacity??1}}set comment(t){this.#h||=new Comment(this);"object"==typeof t&&null!==t?this.#h.restoreData(t):this.#h.data=t;if(this.hasComment){this.removeCommentButtonFromToolbar();this.addStandaloneCommentButton();this._uiManager.updateComment(this)}else{this.addCommentButtonInToolbar();this.removeStandaloneCommentButton();this._uiManager.removeComment(this)}}setCommentData({comment:t,popupRef:e,richText:i}){if(!e)return;this.#h||=new Comment(this);this.#h.setInitialText(t,i);if(!this.annotationElementId)return;const s=this._uiManager.getAndRemoveDataFromAnnotationStorage(this.annotationElementId);s&&this.updateFromAnnotationLayer(s)}get hasEditedComment(){return this.#h?.hasBeenEdited()}get hasDeletedComment(){return this.#h?.isDeleted()}get hasComment(){return!!this.#h&&!this.#h.isEmpty()&&!this.#h.isDeleted()}async editComment(t){this.#h||=new Comment(this);this.#h.edit(t)}toggleComment(t,e=void 0){this.hasComment&&this._uiManager.toggleComment(this,t,e)}setSelectedCommentButton(t){this.#h.setSelectedButton(t)}addComment(t){if(this.hasEditedComment){const e=180,i=100,[,,,s]=t.rect,[n]=this.pageDimensions,[r]=this.pageTranslation,a=r+n+1,o=s-i,l=a+e;t.popup={contents:this.comment.text,deleted:this.comment.deleted,rect:[a,o,l,s]}}}updateFromAnnotationLayer({popup:{contents:t,deleted:e}}){this.#h.data=e?null:t}get parentBoundingClientRect(){return this.parent.boundingClientRect}render(){const t=this.div=document.createElement("div");t.setAttribute("data-editor-rotation",(360-this.rotation)%360);t.className=this.name;t.setAttribute("id",this.id);t.tabIndex=this.#Le?-1:0;t.setAttribute("role","application");this.defaultL10nId&&t.setAttribute("data-l10n-id",this.defaultL10nId);this._isVisible||t.classList.add("hidden");this.setInForeground();this.#pi();const[e,i]=this.parentDimensions;if(this.parentRotation%180!=0){t.style.maxWidth=`${(100*i/e).toFixed(2)}%`;t.style.maxHeight=`${(100*e/i).toFixed(2)}%`}const[s,n]=this.getInitialTranslation();this.translate(s,n);bindEvents(this,t,["keydown","pointerdown","dblclick"]);this.isResizable&&this._uiManager._supportsPinchToZoom&&(this.#ei||=new TouchManager({container:t,isPinchingDisabled:()=>!this.isSelected,onPinchStart:this.#gi.bind(this),onPinching:this.#mi.bind(this),onPinchEnd:this.#fi.bind(this),signal:this._uiManager._signal}));this.addStandaloneCommentButton();this._uiManager._editorUndoBar?.hide();return t}#gi(){this.#Ge={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};this.#l?.toggle(!1);this.parent.togglePointerEvents(!1)}#mi(t,e,i){let s=i/e*.7+1-.7;if(1===s)return;const n=this.#oi(this.rotation),transf=(t,e)=>[n[0]*t+n[2]*e,n[1]*t+n[3]*e],[r,a]=this.parentDimensions,o=this.x,l=this.y,h=this.width,d=this.height,c=AnnotationEditor.MIN_SIZE/r,u=AnnotationEditor.MIN_SIZE/a;s=Math.max(Math.min(s,1/h,1/d),c/h,u/d);const p=AnnotationEditor._round(h*s),g=AnnotationEditor._round(d*s);if(p===h&&g===d)return;this.#Xe||=[o,l,h,d];const m=transf(h/2,d/2),f=AnnotationEditor._round(o+m[0]),b=AnnotationEditor._round(l+m[1]),y=transf(p/2,g/2);this.x=f-y[0];this.y=b-y[1];this.width=p;this.height=g;this.setDims();this.fixAndSetPosition();this._onResizing()}#fi(){this.#l?.toggle(!0);this.parent.togglePointerEvents(!0);this.#ci()}pointerdown(t){const{isMac:e}=FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{this.#$e=!0;this._isDraggable?this.#bi(t):this.#yi(t)}}#yi(t){const{isMac:e}=FeatureTest.platform;t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this)}#bi(t){const{isSelected:e}=this;this._uiManager.setUpDragSession();let i=!1;const s=new AbortController,n=this._uiManager.combinedSignal(s),r={capture:!0,passive:!1,signal:n},cancelDrag=t=>{s.abort();this.#Ne=null;this.#$e=!1;this._uiManager.endDragSession()||this.#yi(t);i&&this._onStopDragging()};if(e){this.#Je=t.clientX;this.#Ze=t.clientY;this.#Ne=t.pointerId;this.#Ue=t.pointerType;window.addEventListener("pointermove",t=>{if(!i){i=!0;this._uiManager.toggleComment(this,!0,!1);this._onStartDragging()}const{clientX:e,clientY:s,pointerId:n}=t;if(n!==this.#Ne){stopEvent(t);return}const[r,a]=this.screenToPageTranslation(e-this.#Je,s-this.#Ze);this.#Je=e;this.#Ze=s;this._uiManager.dragSelectedEditors(r,a)},r);window.addEventListener("touchmove",stopEvent,r);window.addEventListener("pointerdown",t=>{t.pointerType===this.#Ue&&(this.#ei||t.isPrimary)&&cancelDrag(t);stopEvent(t)},r)}const pointerUpCallback=t=>{this.#Ne&&this.#Ne!==t.pointerId?stopEvent(t):cancelDrag(t)};window.addEventListener("pointerup",pointerUpCallback,{signal:n});window.addEventListener("blur",pointerUpCallback,{signal:n})}_onStartDragging(){}_onStopDragging(){}moveInDOM(){this.#Qe&&clearTimeout(this.#Qe);this.#Qe=setTimeout(()=>{this.#Qe=null;this.parent?.moveEditorInDOM(this)},0)}_setParentAndPosition(t,e,i){t.changeParent(this);this.x=e;this.y=i;this.fixAndSetPosition();this._onTranslated()}getRect(t,e,i=this.rotation){const s=this.parentScale,[n,r]=this.pageDimensions,[a,o]=this.pageTranslation,l=t/s,h=e/s,d=this.x*n,c=this.y*r,u=this.width*n,p=this.height*r;switch(i){case 0:return[d+l+a,r-c-h-p+o,d+l+u+a,r-c-h+o];case 90:return[d+h+a,r-c+l+o,d+h+p+a,r-c+l+u+o];case 180:return[d-l-u+a,r-c+h+o,d-l+a,r-c+h+p+o];case 270:return[d-h-p+a,r-c-l-u+o,d-h+a,r-c-l+o];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[i,s,n,r]=t,a=n-i,o=r-s;switch(this.rotation){case 0:return[i,e-r,a,o];case 90:return[i,e-s,o,a];case 180:return[n,e-s,a,o];case 270:return[n,e-r,o,a];default:throw new Error("Invalid rotation")}}getPDFRect(){return this.getRect(0,0)}getNonHCMColor(){return this.color&&AnnotationEditor._colorManager.convert(this._uiManager.getNonHCMColor(this.color))}onUpdatedColor(){this.#h?.onUpdatedColor()}getData(){const{comment:{text:t,color:e,date:i,opacity:s,deleted:n,richText:r},uid:a,pageIndex:o,creationDate:l,modificationDate:h}=this;return{id:a,pageIndex:o,rect:this.getPDFRect(),richText:r,contentsObj:{str:t},creationDate:l,modificationDate:i||h,popupRef:!n,color:e,opacity:s}}onceAdded(t){}isEmpty(){return!1}enableEditMode(){if(this.isInEditMode())return!1;this.parent.setEditingState(!1);this.#Ke=!0;return!0}disableEditMode(){if(!this.isInEditMode())return!1;this.parent.setEditingState(!0);this.#Ke=!1;return!0}isInEditMode(){return this.#Ke}shouldGetKeyboardEvents(){return this.#Ye}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}get isOnScreen(){const{top:t,left:e,bottom:i,right:s}=this.getClientDimensions(),{innerHeight:n,innerWidth:r}=window;return e0&&t0}#pi(){if(this.#Ve||!this.div)return;this.#Ve=new AbortController;const t=this._uiManager.combinedSignal(this.#Ve);this.div.addEventListener("focusin",this.focusin.bind(this),{signal:t});this.div.addEventListener("focusout",this.focusout.bind(this),{signal:t})}rebuild(){this.#pi()}rotate(t){}resize(){}serializeDeleted(){return{id:this.annotationElementId,deleted:!0,pageIndex:this.pageIndex,popupRef:this._initialData?.popupRef||""}}serialize(t=!1,e=null){return{annotationType:this.mode,pageIndex:this.pageIndex,rect:this.getPDFRect(),rotation:this.rotation,structTreeParentId:this._structTreeParentId,popupRef:this._initialData?.popupRef||""}}static async deserialize(t,e,i){const s=new this.prototype.constructor({parent:e,id:i.getId(),uiManager:i,annotationElementId:t.annotationElementId,creationDate:t.creationDate,modificationDate:t.modificationDate});s.rotation=t.rotation;s.#Re=t.accessibilityData;s._isCopy=t.isCopy||!1;const[n,r]=s.pageDimensions,[a,o,l,h]=s.getRectInCurrentCoords(t.rect,r);s.x=a/n;s.y=o/r;s.width=l/n;s.height=h/r;return s}get hasBeenModified(){return!!this.annotationElementId&&(this.deleted||null!==this.serialize())}remove(){this.#Ve?.abort();this.#Ve=null;this.isEmpty()||this.commit();this.parent?this.parent.remove(this):this._uiManager.removeEditor(this);this.hideCommentPopup();if(this.#Qe){clearTimeout(this.#Qe);this.#Qe=null}this.#ni();this.removeEditToolbar();if(this.#ti){for(const t of this.#ti.values())clearTimeout(t);this.#ti=null}this.parent=null;this.#ei?.destroy();this.#ei=null;this.#We?.remove();this.#We=null}get isResizable(){return!1}makeResizable(){if(this.isResizable){this.#li();this.#He.classList.remove("hidden")}}get toolbarPosition(){return null}get commentButtonPosition(){return"ltr"===this._uiManager.direction?[1,0]:[0,0]}get commentButtonPositionInPage(){const{commentButtonPosition:[t,e]}=this,[i,s,n,r]=this.getPDFRect();return[AnnotationEditor._round(i+(n-i)*t),AnnotationEditor._round(s+(r-s)*(1-e))]}get commentButtonColor(){return this._uiManager.makeCommentColor(this.getNonHCMColor(),this.opacity)}get commentPopupPosition(){return this.#h.commentPopupPositionInLayer}set commentPopupPosition(t){this.#h.commentPopupPositionInLayer=t}hasDefaultPopupPosition(){return this.#h.hasDefaultPopupPosition()}get commentButtonWidth(){return this.#h.commentButtonWidth}get elementBeforePopup(){return this.div}setCommentButtonStates(t){this.#h?.setCommentButtonStates(t)}keydown(t){if(!this.isResizable||t.target!==this.div||"Enter"!==t.key)return;this._uiManager.setSelected(this);this.#Ge={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const e=this.#He.children;if(!this.#Oe){this.#Oe=Array.from(e);const t=this.#Ai.bind(this),i=this.#vi.bind(this),s=this._uiManager._signal;for(const e of this.#Oe){const n=e.getAttribute("data-resizer-name");e.setAttribute("role","spinbutton");e.addEventListener("keydown",t,{signal:s});e.addEventListener("blur",i,{signal:s});e.addEventListener("focus",this.#wi.bind(this,n),{signal:s});e.setAttribute("data-l10n-id",AnnotationEditor._l10nResizer[n])}}const i=this.#Oe[0];let s=0;for(const t of e){if(t===i)break;s++}const n=(360-this.rotation+this.parentRotation)%360/90*(this.#Oe.length/4);if(n!==s){if(ns)for(let t=0;t{this.div?.classList.contains("selectedEditor")&&this._editToolbar?.show()})}}focus(){this.div&&!this.div.contains(document.activeElement)&&setTimeout(()=>this.div?.focus({preventScroll:!0}),0)}unselect(){if(this.isSelected){this.isSelected=!1;this.#He?.classList.add("hidden");this.div?.classList.remove("selectedEditor");this.div?.contains(document.activeElement)&&this._uiManager.currentLayer.div.focus({preventScroll:!0});this._editToolbar?.hide();this.#l?.toggleAltTextBadge(!0);this.hideCommentPopup()}}hideCommentPopup(){this.hasComment&&this._uiManager.toggleComment(null)}updateParams(t,e){}disableEditing(){}enableEditing(){}get canChangeContent(){return!1}enterInEditMode(){if(this.canChangeContent){this.enableEditMode();this.div.focus()}}dblclick(t){if("BUTTON"!==t.target.nodeName){this.enterInEditMode();this.parent.updateToolbar({mode:this.constructor._editorType,editId:this.uid})}}getElementForAltText(){return this.div}get contentDiv(){return this.div}get isEditing(){return this.#qe}set isEditing(t){this.#qe=t;if(this.parent)if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}static get MIN_SIZE(){return 16}static canCreateNewEmptyEditor(){return!0}get telemetryInitialData(){return{action:"added"}}get telemetryFinalData(){return null}_reportTelemetry(t,e=!1){if(e){this.#ti||=new Map;const{action:e}=t;let i=this.#ti.get(e);i&&clearTimeout(i);i=setTimeout(()=>{this._reportTelemetry(t);this.#ti.delete(e);0===this.#ti.size&&(this.#ti=null)},AnnotationEditor._telemetryTimeout);this.#ti.set(e,i);return}t.type||=this.editorType;this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:t}})}show(t=this._isVisible){this.div.classList.toggle("hidden",!t);this._isVisible=t}enable(){this.div&&(this.div.tabIndex=0);this.#Le=!1}disable(){this.div&&(this.div.tabIndex=-1);this.#Le=!0}updateFakeAnnotationElement(t){if(this.#We||this.deleted)if(this.deleted){this.#We.remove();this.#We=null}else(this.hasEditedComment||this._hasBeenMoved||this._hasBeenResized)&&this.#We.updateEdited({rect:this.getPDFRect(),popup:this.comment});else this.#We=t.addFakeAnnotation(this)}renderAnnotationElement(t){if(this.deleted){t.hide();return null}let e=t.container.querySelector(".annotationContent");if(e){if("CANVAS"===e.nodeName){const t=e;e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.before(e)}}else{e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.container.prepend(e)}return e}resetAnnotationElement(t){const{firstElementChild:e}=t.container;"DIV"===e?.nodeName&&e.classList.contains("annotationContent")&&e.remove()}}class FakeEditor extends AnnotationEditor{constructor(t){super(t);this.annotationElementId=t.annotationElementId;this.deleted=!0}serialize(){return this.serializeDeleted()}}const X=3285377520,q=4294901760,K=65535;class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:X;this.h2=t?4294967295&t:X}update(t){let e,i;if("string"==typeof t){e=new Uint8Array(2*t.length);i=0;for(let s=0,n=t.length;s>>8;e[i++]=255&n}}}else{if(!ArrayBuffer.isView(t))throw new Error("Invalid data format, must be a string or TypedArray.");e=t.slice();i=e.byteLength}const s=i>>2,n=i-4*s,r=new Uint32Array(e.buffer,0,s);let a=0,o=0,l=this.h1,h=this.h2;const d=3432918353,c=461845907,u=11601,p=13715;for(let t=0;t>>17;a=a*c&q|a*p&K;l^=a;l=l<<13|l>>>19;l=5*l+3864292196}else{o=r[t];o=o*d&q|o*u&K;o=o<<15|o>>>17;o=o*c&q|o*p&K;h^=o;h=h<<13|h>>>19;h=5*h+3864292196}a=0;switch(n){case 3:a^=e[4*s+2]<<16;case 2:a^=e[4*s+1]<<8;case 1:a^=e[4*s];a=a*d&q|a*u&K;a=a<<15|a>>>17;a=a*c&q|a*p&K;1&s?l^=a:h^=a}this.h1=l;this.h2=h}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&q|36045*t&K;e=4283543511*e&q|(2950163797*(e<<16|t>>>16)&q)>>>16;t^=e>>>1;t=444984403*t&q|60499*t&K;e=3301882366*e&q|(3120437893*(e<<16|t>>>16)&q)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}const Y=Object.freeze({map:null,hash:"",transfer:void 0});class AnnotationStorage{#Ci=!1;#Ei=null;#Ti=null;#Si=new Map;onSetModified=null;onResetModified=null;onAnnotationEditor=null;getValue(t,e){const i=this.#Si.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#Si.get(t)}remove(t){const e=this.#Si.get(t);if(void 0!==e){e instanceof AnnotationEditor&&this.#Ti.delete(e.annotationElementId);this.#Si.delete(t);0===this.#Si.size&&this.resetModified();this.#Si.values().some(t=>t instanceof AnnotationEditor)||this.onAnnotationEditor?.(null)}}setValue(t,e){const i=this.#Si.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#Si.set(t,e)}s&&this.#_i();if(e instanceof AnnotationEditor){(this.#Ti||=new Map).set(e.annotationElementId,e);this.onAnnotationEditor?.(e.constructor._type)}}has(t){return this.#Si.has(t)}get size(){return this.#Si.size}#_i(){if(!this.#Ci){this.#Ci=!0;this.onSetModified?.()}}resetModified(){if(this.#Ci){this.#Ci=!1;this.onResetModified?.()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#Si.size)return Y;const t=new Map,e=new MurmurHash3_64,i=[],s=Object.create(null);let n=!1;for(const[i,r]of this.#Si){const a=r instanceof AnnotationEditor?r.serialize(!1,s):r;if(r.page){r.pageIndex=r.page._pageIndex;delete r.page}if(a){t.set(i,a);e.update(`${i}:${JSON.stringify(a)}`);n||=!!a.bitmap}}if(n)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfer:i}:Y}get editorStats(){let t=null;const e=new Map;let i=0,s=0;for(const n of this.#Si.values()){if(!(n instanceof AnnotationEditor)){n.popup&&(n.popup.deleted?s+=1:i+=1);continue}n.isCommentDeleted?s+=1:n.hasEditedComment&&(i+=1);const r=n.telemetryFinalData;if(!r)continue;const{type:a}=r;e.has(a)||e.set(a,Object.getPrototypeOf(n).constructor);t||=Object.create(null);const o=t[a]||=new Map;for(const[t,e]of Object.entries(r)){if("type"===t)continue;const i=o.getOrInsertComputed(t,makeMap);i.set(e,(i.get(e)??0)+1)}}if(s>0||i>0){t||=Object.create(null);t.comments={deleted:s,edited:i}}if(!t)return null;for(const[i,s]of e)t[i]=s.computeTelemetryFinalData(t[i]);return t}resetModifiedIds(){this.#Ei=null}updateEditor(t,e){const i=this.#Ti?.get(t);if(i){i.updateFromAnnotationLayer(e);return!0}return!1}getEditor(t){return this.#Ti?.get(t)||null}get modifiedIds(){if(this.#Ei)return this.#Ei;const t=[];if(this.#Ti)for(const e of this.#Ti.values())e.serialize()&&t.push(e.annotationElementId);let e="";if(t.length){const i=new MurmurHash3_64;i.update(t.join(","));e=i.hexdigest()}return this.#Ei={ids:new Set(t),hash:e}}[Symbol.iterator](){return this.#Si.entries()}}class PrintAnnotationStorage extends AnnotationStorage{#Pi=Y;constructor(t){super();const{serializable:e}=t;if(e===Y)return;const{map:i,hash:s,transfer:n}=e,r=structuredClone(i,n?{transfer:n}:null);this.#Pi={map:r,hash:s,transfer:[]}}get print(){unreachable("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#Pi}get modifiedIds(){return shadow(this,"modifiedIds",{ids:new Set,hash:""})}}const Q="__forcedDependency",{floor:J,ceil:Z}=Math;function expandBBox(t,e,i,s,n,r){t[4*e+0]=Math.min(t[4*e+0],i);t[4*e+1]=Math.min(t[4*e+1],s);t[4*e+2]=Math.max(t[4*e+2],n);t[4*e+3]=Math.max(t[4*e+3],r)}const tt=new Uint32Array(new Uint8Array([255,255,0,0]).buffer)[0];class BBoxReader{#Di;#Mi;constructor(t,e){this.#Di=t;this.#Mi=e}get length(){return this.#Di.length}isEmpty(t){return this.#Di[t]===tt}minX(t){return this.#Mi[4*t+0]/256}minY(t){return this.#Mi[4*t+1]/256}maxX(t){return(this.#Mi[4*t+2]+1)/256}maxY(t){return(this.#Mi[4*t+3]+1)/256}}const ensureDebugMetadata=(t,e)=>t?.getOrInsertComputed(e,()=>({dependencies:new Set,isRenderingOperation:!1}));class CanvasBBoxTracker{#ki=[[1,0,0,1,0,0]];#Ii=[-1/0,-1/0,1/0,1/0];#Fi=new Float64Array(e);_pendingBBoxIdx=-1;#Bi;#Ri;#Oi;#Di;_savesStack=[];_markedContentStack=[];constructor(t,e){this.#Bi=t.width;this.#Ri=t.height;this.#Li(e)}growOperationsCount(t){t>=this.#Di.length&&this.#Li(t,this.#Di)}#Li(t,e){const i=new ArrayBuffer(4*t);this.#Oi=new Uint8ClampedArray(i);this.#Di=new Uint32Array(i);if(e&&e.length>0){this.#Di.set(e);this.#Di.fill(tt,e.length)}else this.#Di.fill(tt)}get clipBox(){return this.#Ii}save(t){this.#Ii={__proto__:this.#Ii};this._savesStack.push(t);return this}restore(t,e){const i=Object.getPrototypeOf(this.#Ii);if(null===i)return this;this.#Ii=i;const s=this._savesStack.pop();if(void 0!==s){e?.(s,t);this.#Di[t]=this.#Di[s]}return this}recordOpenMarker(t){this._savesStack.push(t);return this}getOpenMarker(){return 0===this._savesStack.length?null:this._savesStack.at(-1)}recordCloseMarker(t,e){const i=this._savesStack.pop();if(void 0!==i){e?.(i,t);this.#Di[t]=this.#Di[i]}return this}beginMarkedContent(t){this._markedContentStack.push(t);return this}endMarkedContent(t,e){const i=this._markedContentStack.pop();if(void 0!==i){e?.(i,t);this.#Di[t]=this.#Di[i]}return this}pushBaseTransform(t){this.#ki.push(Util.multiplyByDOMMatrix(this.#ki.at(-1),t.getTransform()));return this}popBaseTransform(){this.#ki.length>1&&this.#ki.pop();return this}resetBBox(t){if(this._pendingBBoxIdx!==t){this._pendingBBoxIdx=t;this.#Fi.set(e,0)}return this}recordClipBox(t,i,s,n,r,a){const o=Util.multiplyByDOMMatrix(this.#ki.at(-1),i.getTransform()),l=e.slice();Util.axialAlignedBoundingBox([s,r,n,a],o,l);const h=Util.intersect(this.#Ii,l);if(h){this.#Ii[0]=h[0];this.#Ii[1]=h[1];this.#Ii[2]=h[2];this.#Ii[3]=h[3]}else{this.#Ii[0]=this.#Ii[1]=1/0;this.#Ii[2]=this.#Ii[3]=-1/0}return this}recordBBox(t,i,s,n,r,a){const o=this.#Ii;if(o[0]===1/0)return this;const l=Util.multiplyByDOMMatrix(this.#ki.at(-1),i.getTransform());if(o[0]===-1/0){Util.axialAlignedBoundingBox([s,r,n,a],l,this.#Fi);return this}const h=e.slice();Util.axialAlignedBoundingBox([s,r,n,a],l,h);this.#Fi[0]=MathClamp(h[0],o[0],this.#Fi[0]);this.#Fi[1]=MathClamp(h[1],o[1],this.#Fi[1]);this.#Fi[2]=MathClamp(h[2],this.#Fi[2],o[2]);this.#Fi[3]=MathClamp(h[3],this.#Fi[3],o[3]);return this}recordFullPageBBox(t){this.#Fi[0]=Math.max(0,this.#Ii[0]);this.#Fi[1]=Math.max(0,this.#Ii[1]);this.#Fi[2]=Math.min(this.#Bi,this.#Ii[2]);this.#Fi[3]=Math.min(this.#Ri,this.#Ii[3]);return this}recordOperation(t,e=!1,i){if(this._pendingBBoxIdx!==t)return this;const s=J(256*this.#Fi[0]/this.#Bi),n=J(256*this.#Fi[1]/this.#Ri),r=Z(256*this.#Fi[2]/this.#Bi),a=Z(256*this.#Fi[3]/this.#Ri);expandBBox(this.#Oi,t,s,n,r,a);if(i)for(const e of i)for(const i of e)i!==t&&expandBBox(this.#Oi,i,s,n,r,a);e||(this._pendingBBoxIdx=-1);return this}bboxToClipBoxDropOperation(t){if(this._pendingBBoxIdx===t){this._pendingBBoxIdx=-1;this.#Ii[0]=Math.max(this.#Ii[0],this.#Fi[0]);this.#Ii[1]=Math.max(this.#Ii[1],this.#Fi[1]);this.#Ii[2]=Math.min(this.#Ii[2],this.#Fi[2]);this.#Ii[3]=Math.min(this.#Ii[3],this.#Fi[3])}return this}take(){return new BBoxReader(this.#Di,this.#Oi)}takeDebugMetadata(){throw new Error("Unreachable")}recordSimpleData(t,e){return this}recordIncrementalData(t,e){return this}resetIncrementalData(t,e){return this}recordNamedData(t,e){return this}recordSimpleDataFromNamed(t,e,i){return this}recordFutureForcedDependency(t,e){return this}inheritSimpleDataAsFutureForcedDependencies(t){return this}inheritPendingDependenciesAsFutureForcedDependencies(){return this}recordCharacterBBox(t,e,i,s=1,n=0,r=0,a){return this}getSimpleIndex(t){}recordDependencies(t,e){return this}recordNamedDependency(t,e){return this}recordShowTextOperation(t,e=!1){return this}}class CanvasDependencyTracker{#Ni={__proto__:null};#Ui={__proto__:null,transform:[],moveText:[],sameLineText:[],[Q]:[]};#Hi=new Map;#zi=new Set;#Gi=new Map;#Wi;#Vi;#ji;constructor(t,e=!1){this.#ji=t;if(e){this.#Wi=new Map;this.#Vi=(t,e)=>{ensureDebugMetadata(this.#Wi,e).dependencies.add(t)}}}get clipBox(){return this.#ji.clipBox}growOperationsCount(t){this.#ji.growOperationsCount(t)}save(t){this.#Ni={__proto__:this.#Ni};this.#Ui={__proto__:this.#Ui,transform:{__proto__:this.#Ui.transform},moveText:{__proto__:this.#Ui.moveText},sameLineText:{__proto__:this.#Ui.sameLineText},[Q]:{__proto__:this.#Ui[Q]}};this.#ji.save(t);return this}restore(t){this.#ji.restore(t,this.#Vi);const e=Object.getPrototypeOf(this.#Ni);if(null===e)return this;this.#Ni=e;this.#Ui=Object.getPrototypeOf(this.#Ui);return this}recordOpenMarker(t){this.#ji.recordOpenMarker(t,this.#Vi);return this}getOpenMarker(){return this.#ji.getOpenMarker()}recordCloseMarker(t){this.#ji.recordCloseMarker(t,this.#Vi);return this}beginMarkedContent(t){this.#ji.beginMarkedContent(t);return this}endMarkedContent(t){this.#ji.endMarkedContent(t,this.#Vi);return this}pushBaseTransform(t){this.#ji.pushBaseTransform(t);return this}popBaseTransform(){this.#ji.popBaseTransform();return this}recordSimpleData(t,e){this.#Ni[t]=e;return this}recordIncrementalData(t,e){this.#Ui[t].push(e);return this}resetIncrementalData(t,e){this.#Ui[t].length=0;return this}recordNamedData(t,e){this.#Hi.set(t,e);return this}recordSimpleDataFromNamed(t,e,i){this.#Ni[t]=this.#Hi.get(e)??i}recordFutureForcedDependency(t,e){this.recordIncrementalData(Q,e);return this}inheritSimpleDataAsFutureForcedDependencies(t){for(const e of t)e in this.#Ni&&this.recordFutureForcedDependency(e,this.#Ni[e]);return this}inheritPendingDependenciesAsFutureForcedDependencies(){for(const t of this.#zi)this.recordFutureForcedDependency(Q,t);return this}resetBBox(t){this.#ji.resetBBox(t);return this}recordClipBox(t,e,i,s,n,r){this.#ji.recordClipBox(t,e,i,s,n,r);return this}recordBBox(t,e,i,s,n,r){this.#ji.recordBBox(t,e,i,s,n,r);return this}recordCharacterBBox(t,e,i,s=1,n=0,r=0,a){const o=i.bbox;let l,h;if(o){l=o[2]!==o[0]&&o[3]!==o[1]&&this.#Gi.get(i);if(!1!==l){h=[0,0,0,0];Util.axialAlignedBoundingBox(o,i.fontMatrix,h);1===s&&0===n&&0===r||Util.scaleMinMax([s,0,0,-s,n,r],h);if(l)return this.recordBBox(t,e,h[0],h[2],h[1],h[3])}}if(!a)return this.recordFullPageBBox(t);const d=a();if(o&&h&&void 0===l){l=h[0]<=n-d.actualBoundingBoxLeft&&h[2]>=n+d.actualBoundingBoxRight&&h[1]<=r-d.actualBoundingBoxAscent&&h[3]>=r+d.actualBoundingBoxDescent;this.#Gi.set(i,l);if(l)return this.recordBBox(t,e,h[0],h[2],h[1],h[3])}return this.recordBBox(t,e,n-d.actualBoundingBoxLeft,n+d.actualBoundingBoxRight,r-d.actualBoundingBoxAscent,r+d.actualBoundingBoxDescent)}recordFullPageBBox(t){this.#ji.recordFullPageBBox(t);return this}getSimpleIndex(t){return this.#Ni[t]}recordDependencies(t,e){const i=this.#zi,s=this.#Ni,n=this.#Ui;for(const t of e)t in this.#Ni?i.add(s[t]):t in n&&n[t].forEach(i.add,i);return this}recordNamedDependency(t,e){this.#Hi.has(e)&&this.#zi.add(this.#Hi.get(e));return this}recordOperation(t,e=!1){this.recordDependencies(t,[Q]);if(this.#Wi){const e=ensureDebugMetadata(this.#Wi,t),{dependencies:i}=e;this.#zi.forEach(i.add,i);this.#ji._savesStack.forEach(i.add,i);this.#ji._markedContentStack.forEach(i.add,i);i.delete(t);e.isRenderingOperation=!0}const i=!e&&t===this.#ji._pendingBBoxIdx;this.#ji.recordOperation(t,e,[this.#zi,this.#ji._savesStack,this.#ji._markedContentStack]);i&&this.#zi.clear();return this}recordShowTextOperation(t,e=!1){const i=Array.from(this.#zi);this.recordOperation(t,e);this.recordIncrementalData("sameLineText",t);for(const t of i)this.recordIncrementalData("sameLineText",t);return this}bboxToClipBoxDropOperation(t,e=!1){const i=!e&&t===this.#ji._pendingBBoxIdx;this.#ji.bboxToClipBoxDropOperation(t);i&&this.#zi.clear();return this}take(){this.#Gi.clear();return this.#ji.take()}takeDebugMetadata(){return this.#Wi}}class CanvasNestedDependencyTracker{#$i;#Xi;#qi;#Ki=0;#Yi=0;constructor(t,e,i){if(t instanceof CanvasNestedDependencyTracker&&t.#qi===!!i)return t;this.#$i=t;this.#Xi=e;this.#qi=!!i}get clipBox(){return this.#$i.clipBox}growOperationsCount(){throw new Error("Unreachable")}save(t){this.#Yi++;this.#$i.save(this.#Xi);return this}restore(t){if(this.#Yi>0){this.#$i.restore(this.#Xi);this.#Yi--}return this}recordOpenMarker(t){this.#Ki++;return this}getOpenMarker(){return this.#Ki>0?this.#Xi:this.#$i.getOpenMarker()}recordCloseMarker(t){this.#Ki--;return this}beginMarkedContent(t){return this}endMarkedContent(t){return this}pushBaseTransform(t){this.#$i.pushBaseTransform(t);return this}popBaseTransform(){this.#$i.popBaseTransform();return this}recordSimpleData(t,e){this.#$i.recordSimpleData(t,this.#Xi);return this}recordIncrementalData(t,e){this.#$i.recordIncrementalData(t,this.#Xi);return this}resetIncrementalData(t,e){this.#$i.resetIncrementalData(t,this.#Xi);return this}recordNamedData(t,e){return this}recordSimpleDataFromNamed(t,e,i){this.#$i.recordSimpleDataFromNamed(t,e,this.#Xi);return this}recordFutureForcedDependency(t,e){this.#$i.recordFutureForcedDependency(t,this.#Xi);return this}inheritSimpleDataAsFutureForcedDependencies(t){this.#$i.inheritSimpleDataAsFutureForcedDependencies(t);return this}inheritPendingDependenciesAsFutureForcedDependencies(){this.#$i.inheritPendingDependenciesAsFutureForcedDependencies();return this}resetBBox(t){this.#qi||this.#$i.resetBBox(this.#Xi);return this}recordClipBox(t,e,i,s,n,r){this.#qi||this.#$i.recordClipBox(this.#Xi,e,i,s,n,r);return this}recordBBox(t,e,i,s,n,r){this.#qi||this.#$i.recordBBox(this.#Xi,e,i,s,n,r);return this}recordCharacterBBox(t,e,i,s,n,r,a){this.#qi||this.#$i.recordCharacterBBox(this.#Xi,e,i,s,n,r,a);return this}recordFullPageBBox(t){this.#qi||this.#$i.recordFullPageBBox(this.#Xi);return this}getSimpleIndex(t){return this.#$i.getSimpleIndex(t)}recordDependencies(t,e){this.#$i.recordDependencies(this.#Xi,e);return this}recordNamedDependency(t,e){this.#$i.recordNamedDependency(this.#Xi,e);return this}recordOperation(t){this.#$i.recordOperation(this.#Xi,!0);return this}recordShowTextOperation(t){this.#$i.recordShowTextOperation(this.#Xi,!0);return this}bboxToClipBoxDropOperation(t){this.#qi||this.#$i.bboxToClipBoxDropOperation(this.#Xi,!0);return this}take(){throw new Error("Unreachable")}takeDebugMetadata(){throw new Error("Unreachable")}}const et=["path","transform","filter","strokeColor","strokeAlpha","lineWidth","lineCap","lineJoin","miterLimit","dash"],it=["path","transform","filter","fillColor","fillAlpha","globalCompositeOperation","SMask"],st=["transform","SMask","filter","fillAlpha","strokeAlpha","globalCompositeOperation"],nt=["filter","fillColor","fillAlpha"],rt=["transform","leading","charSpacing","wordSpacing","hScale","textRise","moveText","textMatrix","font","fontObj","filter","fillColor","textRenderingMode","SMask","fillAlpha","strokeAlpha","globalCompositeOperation","sameLineText"],at=["transform"],ot=["transform","fillColor"];class CanvasImagesTracker{#Bi;#Ri;#Qi=4;#Ji=0;#Mi=new CanvasImagesTracker.#Zi(6*this.#Qi);static#Zi=FeatureTest.isFloat16ArraySupported?Float16Array:Float32Array;constructor(t){this.#Bi=t.width;this.#Ri=t.height}record(t,i,s,n){if(this.#Ji===this.#Qi){this.#Qi*=2;const t=new CanvasImagesTracker.#Zi(6*this.#Qi);t.set(this.#Mi);this.#Mi=t}const r=Util.domMatrixToTransform(t.getTransform());let a;if(n[0]!==1/0){const t=e.slice();Util.axialAlignedBoundingBox([0,-s,i,0],r,t);const o=Util.intersect(n,t);if(!o)return;const[l,h,d,c]=o;if(l!==t[0]||h!==t[1]||d!==t[2]||c!==t[3]){const t=Math.atan2(r[1],r[0]),e=Math.abs(Math.sin(t)),i=Math.abs(Math.cos(t));if(e<1e-6||i<1e-6||Math.abs(e-i)<1e-6)a=[l,h,l,c,d,h];else{const t=d-l,s=c-h,n=e*e,r=i*i,o=i*e,u=r-n,p=(s*r-t*o)/u;a=[l+(s*o-t*n)/u,h,l,h+p,d,c-p]}}}if(!a){a=[0,-s,0,0,i,-s];Util.applyTransform(a,r,0);Util.applyTransform(a,r,2);Util.applyTransform(a,r,4)}a[0]/=this.#Bi;a[1]/=this.#Ri;a[2]/=this.#Bi;a[3]/=this.#Ri;a[4]/=this.#Bi;a[5]/=this.#Ri;this.#Mi.set(a,6*this.#Ji);this.#Ji++}take(){return this.#Mi.subarray(0,6*this.#Ji)}}class FontLoader{#ts=new Set;constructor({ownerDocument:t=globalThis.document,styleElement:e=null}){this._document=t;this.nativeFontFaces=new Set;this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.add(t);this._document.fonts.add(t)}removeNativeFontFace(t){this.nativeFontFaces.delete(t);this._document.fonts.delete(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.clear();this.#ts.clear();if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async loadSystemFont({systemFontInfo:t,disableFontFace:e,_inspectFont:i}){if(t&&!this.#ts.has(t.loadedName)){assert(!e,"loadSystemFont shouldn't be called when `disableFontFace` is set.");if(this.isFontLoadingAPISupported){const{loadedName:e,src:s,style:n}=t,r=new FontFace(e,s,n);this.addNativeFontFace(r);try{await r.load();this.#ts.add(e);i?.(t)}catch{warn(`Cannot load system font: ${t.baseFontName}, installing it could help to improve PDF rendering.`);this.removeNativeFontFace(r)}return}unreachable("Not implemented: loadSystemFont without the Font Loading API.")}}async bind(t){if(t.attached||t.missingFile&&!t.systemFontInfo)return;t.attached=!0;if(t.systemFontInfo){await this.loadSystemFont(t);return}if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(i){warn(`Failed to load font '${e.family}': '${i}'.`);t.disableFontFace=!0;throw i}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise(e=>{const i=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,i)})}}get isFontLoadingAPISupported(){return shadow(this,"isFontLoadingAPISupported",!!this._document?.fonts)}get isSyncFontLoadingSupported(){return shadow(this,"isSyncFontLoadingSupported",t||FeatureTest.platform.isFirefox)}_queueLoadingCallback(t){const{loadingRequests:e}=this,i={done:!1,complete:function completeRequest(){assert(!i.done,"completeRequest() cannot be called twice.");i.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(i);return i}get _loadTestFont(){return shadow(this,"_loadTestFont",atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=="))}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,i,s){return t.substring(0,e)+s+t.substring(e+i)}let i,s;const n=this._document.createElement("canvas");n.width=1;n.height=1;const r=n.getContext("2d");let a=0;const o=`lt${Date.now()}${this.loadTestFontId++}`;let l=this._loadTestFont;l=spliceString(l,976,o.length,o);const h=1482184792;let d=int32(l,16);for(i=0,s=o.length-3;i>24&255,t>>16&255,t>>8&255,255&t)}(d));const c=`@font-face {font-family:"${o}";src:${`url(data:font/opentype;base64,${btoa(l)});`}}`;this.insertRule(c);const u=this._document.createElement("div");u.style.visibility="hidden";u.style.width=u.style.height="10px";u.style.position="absolute";u.style.top=u.style.left="0px";for(const e of[t.loadedName,o]){const t=this._document.createElement("span");t.textContent="Hi";t.style.fontFamily=e;u.append(t)}this._document.body.append(u);!function isFontReady(t,e){if(++a>30){warn("Load test font never loaded.");e();return}r.font="30px "+t;r.fillText(".",0,20);r.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(o,()=>{u.remove();e.complete()})}}class FontFaceObject{compiledGlyphs=Object.create(null);#es;constructor(t,e=null,i,s){this.#es=t;this._inspectFont=e;i&&(this.charProcOperatorList=i);s&&Object.assign(this,s)}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this._inspectFont?.(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=`url(data:${this.mimetype};base64,${this.data.toBase64()});`;let e;if(this.cssFontInfo){let i=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(i+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);e=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${i}src:${t}}`}else e=`@font-face {font-family:"${this.loadedName}";src:${t}}`;this._inspectFont?.(this,t);return e}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];const i=this.loadedName+"_path_"+e;let s;try{s=t.get(i)}catch(t){warn(`getPathGenerator - ignoring character: "${t}".`)}const n=makePathFromDrawOPS(s?.path);this.fontExtraProperties||t.delete(i);return this.compiledGlyphs[e]=n}get black(){return this.#es.black}get bold(){return this.#es.bold}get disableFontFace(){return this.#es.disableFontFace}set disableFontFace(t){shadow(this,"disableFontFace",!!t)}get fontExtraProperties(){return this.#es.fontExtraProperties}get isInvalidPDFjsFont(){return this.#es.isInvalidPDFjsFont}get isType3Font(){return this.#es.isType3Font}get italic(){return this.#es.italic}get missingFile(){return this.#es.missingFile}get remeasure(){return this.#es.remeasure}get vertical(){return this.#es.vertical}get ascent(){return this.#es.ascent}get defaultWidth(){return this.#es.defaultWidth}get descent(){return this.#es.descent}get bbox(){return this.#es.bbox}get fontMatrix(){return this.#es.fontMatrix}get fallbackName(){return this.#es.fallbackName}get loadedName(){return this.#es.loadedName}get mimetype(){return this.#es.mimetype}get name(){return this.#es.name}get data(){return this.#es.data}clearData(){this.#es.clearData()}get cssFontInfo(){return this.#es.cssFontInfo}get systemFontInfo(){return this.#es.systemFontInfo}get defaultVMetrics(){return this.#es.defaultVMetrics}}class CSS_FONT_INFO{static strings=["fontFamily","fontWeight","italicAngle"]}class SYSTEM_FONT_INFO{static strings=["css","loadedName","baseFontName","src"]}class FONT_INFO{static bools=["black","bold","disableFontFace","fontExtraProperties","isInvalidPDFjsFont","isType3Font","italic","missingFile","remeasure","vertical"];static numbers=["ascent","defaultWidth","descent"];static strings=["fallbackName","loadedName","mimetype","name"];static OFFSET_NUMBERS=Math.ceil(2*this.bools.length/8);static OFFSET_BBOX=this.OFFSET_NUMBERS+8*this.numbers.length;static OFFSET_FONT_MATRIX=this.OFFSET_BBOX+1+8;static OFFSET_DEFAULT_VMETRICS=this.OFFSET_FONT_MATRIX+1+48;static OFFSET_STRINGS=this.OFFSET_DEFAULT_VMETRICS+1+6}class PATTERN_INFO{static KIND=0;static HAS_BBOX=1;static HAS_BACKGROUND=2;static SHADING_TYPE=3;static N_COORD=4;static N_COLOR=8;static N_STOP=12;static N_FIGURES=16}class CssFontInfo{#is;#ss=new TextDecoder;#ns;constructor(t){this.#is=t;this.#ns=new DataView(t)}#rs(t){assert(t>i&3;return 0===s?void 0:2===s}get black(){return this.#as(0)}get bold(){return this.#as(1)}get disableFontFace(){return this.#as(2)}get fontExtraProperties(){return this.#as(3)}get isInvalidPDFjsFont(){return this.#as(4)}get isType3Font(){return this.#as(5)}get italic(){return this.#as(6)}get missingFile(){return this.#as(7)}get remeasure(){return this.#as(8)}get vertical(){return this.#as(9)}#os(t){assert(t0){i=e.slice();for(let t=0,e=h.length;t"object"==typeof t&&Number.isInteger(t?.num)&&t.num>=0&&Number.isInteger(t?.gen)&&t.gen>=0,lt=function _isValidExplicitDest(t,e,i){if(!Array.isArray(i)||i.length<2)return!1;const[s,n,...r]=i;if(!t(s)&&!Number.isInteger(s))return!1;if(!e(n))return!1;const a=r.length;let o=!0;switch(n.name){case"XYZ":if(a<2||a>3)return!1;break;case"Fit":case"FitB":return 0===a;case"FitH":case"FitBH":case"FitV":case"FitBV":if(a>1)return!1;break;case"FitR":if(4!==a)return!1;o=!1;break;default:return!1}for(const t of r)if(!("number"==typeof t||o&&null===t))return!1;return!0}.bind(null,isRefProxy,t=>"object"==typeof t&&"string"==typeof t?.name);class LoopbackPort{#ds=new Map;#cs=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#cs.then(()=>{for(const[t]of this.#ds)t.call(this,i)})}addEventListener(t,e,i=null){let s=null;if(i?.signal instanceof AbortSignal){const{signal:n}=i;if(n.aborted){warn("LoopbackPort - cannot use an `aborted` signal.");return}const onAbort=()=>this.removeEventListener(t,e);s=()=>n.removeEventListener("abort",onAbort);n.addEventListener("abort",onAbort)}this.#ds.set(e,s)}removeEventListener(t,e){const i=this.#ds.get(e);i?.();this.#ds.delete(e)}terminate(){for(const[,t]of this.#ds)t?.();this.#ds.clear()}}const ht=1,dt=2,ct=1,ut=2,pt=3,gt=4,mt=5,ft=6,bt=7,yt=8;function onFn(){}function wrapReason(t){if(t instanceof AbortException||t instanceof InvalidPDFException||t instanceof PasswordException||t instanceof ResponseException||t instanceof UnknownErrorException)return t;t instanceof Error||"object"==typeof t&&null!==t||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new AbortException(t.message);case"InvalidPDFException":return new InvalidPDFException(t.message);case"PasswordException":return new PasswordException(t.message,t.code);case"ResponseException":return new ResponseException(t.message,t.status,t.missing);case"UnknownErrorException":return new UnknownErrorException(t.message,t.details)}return new UnknownErrorException(t.message,t.toString())}class MessageHandler{#us=new AbortController;constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#ps.bind(this),{signal:this.#us.signal})}#ps({data:t}){if(t.targetName!==this.sourceName)return;if(t.stream){this.#gs(t);return}if(t.callback){const e=t.callbackId,i=this.callbackCapabilities[e];if(!i)throw new Error(`Cannot resolve callback ${e}`);delete this.callbackCapabilities[e];if(t.callback===ht)i.resolve(t.data);else{if(t.callback!==dt)throw new Error("Unexpected callback case");i.reject(wrapReason(t.reason))}return}const e=this.actionHandler[t.action];if(!e)throw new Error(`Unknown action from worker: ${t.action}`);if(t.callbackId){const i=this.sourceName,s=t.sourceName,n=this.comObj;Promise.try(e,t.data).then(function(e){n.postMessage({sourceName:i,targetName:s,callback:ht,callbackId:t.callbackId,data:e})},function(e){n.postMessage({sourceName:i,targetName:s,callback:dt,callbackId:t.callbackId,reason:wrapReason(e)})});return}t.streamId?this.#ms(t):e(t.data)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const s=this.callbackId++,n=Promise.withResolvers();this.callbackCapabilities[s]=n;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:s,data:e},i)}catch(t){n.reject(t)}return n.promise}sendWithStream(t,e,i,s){const n=this.streamId++,r=this.sourceName,a=this.targetName,o=this.comObj;return new ReadableStream({start:i=>{const l=Promise.withResolvers();this.streamControllers[n]={controller:i,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:r,targetName:a,action:t,streamId:n,data:e,desiredSize:i.desiredSize},s);return l.promise},pull:t=>{const e=Promise.withResolvers();this.streamControllers[n].pullCall=e;o.postMessage({sourceName:r,targetName:a,stream:ft,streamId:n,desiredSize:t.desiredSize});return e.promise},cancel:t=>{assert(t instanceof Error,"cancel must have a valid reason");const e=Promise.withResolvers();this.streamControllers[n].cancelCall=e;this.streamControllers[n].isClosed=!0;o.postMessage({sourceName:r,targetName:a,stream:ct,streamId:n,reason:wrapReason(t)});return e.promise}},i)}#ms(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,r=this,a=this.actionHandler[t.action],o={enqueue(t,r=1,a){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=r;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}n.postMessage({sourceName:i,targetName:s,stream:gt,streamId:e,chunk:t},a)},close(){if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:pt,streamId:e});delete r.streamSinks[e]}},error(t){assert(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,reason:wrapReason(t)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[e]=o;Promise.try(a,t.data,o).then(function(){n.postMessage({sourceName:i,targetName:s,stream:yt,streamId:e,success:!0})},function(t){n.postMessage({sourceName:i,targetName:s,stream:yt,streamId:e,reason:wrapReason(t)})})}#gs(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,r=this.streamControllers[e],a=this.streamSinks[e];switch(t.stream){case yt:t.success?r.startCall.resolve():r.startCall.reject(wrapReason(t.reason));break;case bt:t.success?r.pullCall.resolve():r.pullCall.reject(wrapReason(t.reason));break;case ft:if(!a){n.postMessage({sourceName:i,targetName:s,stream:bt,streamId:e,success:!0});break}a.desiredSize<=0&&t.desiredSize>0&&a.sinkCapability.resolve();a.desiredSize=t.desiredSize;Promise.try(a.onPull||onFn).then(function(){n.postMessage({sourceName:i,targetName:s,stream:bt,streamId:e,success:!0})},function(t){n.postMessage({sourceName:i,targetName:s,stream:bt,streamId:e,reason:wrapReason(t)})});break;case gt:assert(r,"enqueue should have stream controller");if(r.isClosed)break;r.controller.enqueue(t.chunk);break;case pt:assert(r,"close should have stream controller");if(r.isClosed)break;r.isClosed=!0;r.controller.close();this.#fs(r,e);break;case mt:assert(r,"error should have stream controller");r.controller.error(wrapReason(t.reason));this.#fs(r,e);break;case ut:t.success?r.cancelCall.resolve():r.cancelCall.reject(wrapReason(t.reason));this.#fs(r,e);break;case ct:if(!a)break;const o=wrapReason(t.reason);Promise.try(a.onCancel||onFn,o).then(function(){n.postMessage({sourceName:i,targetName:s,stream:ut,streamId:e,success:!0})},function(t){n.postMessage({sourceName:i,targetName:s,stream:ut,streamId:e,reason:wrapReason(t)})});a.sinkCapability.reject(o);a.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#fs(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.#us?.abort();this.#us=null}}class BaseBinaryDataFactory{#bs=Object.freeze({cMapUrl:"CMap",standardFontDataUrl:"font",wasmUrl:"wasm"});constructor({cMapUrl:t=null,standardFontDataUrl:e=null,wasmUrl:i=null}){this.cMapUrl=t;this.standardFontDataUrl=e;this.wasmUrl=i}async fetch({kind:t,filename:e}){switch(t){case"cMapUrl":case"standardFontDataUrl":case"wasmUrl":break;default:unreachable(`Not implemented: ${t}`)}const i=this[t];if(!i)throw new Error(`Ensure that the \`${t}\` API parameter is provided.`);const s=`${i}${e}`;return this._fetch(s,t).catch(e=>{throw new Error(`Unable to load ${this.#bs[t]} data at: ${s}`)})}async _fetch(t,e){unreachable("Abstract method `_fetch` called.")}}class DOMBinaryDataFactory extends BaseBinaryDataFactory{async _fetch(t,e){const i="cMapUrl"!==e||t.endsWith(".bcmap")?"bytes":"text",s=await fetchData(t,i);return s instanceof Uint8Array?s:stringToBytes(s)}}class BaseCanvasFactory{#ys=!1;constructor({enableHWA:t=!1}){this.#ys=t}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const i=this._createCanvas(t,e);return{canvas:i,context:i.getContext("2d",{willReadFrequently:!this.#ys})}}reset({canvas:t},e,i){if(!t)throw new Error("Canvas is not specified");if(e<=0||i<=0)throw new Error("Invalid canvas size");t.width=e;t.height=i}destroy(t){const{canvas:e}=t;if(!e)throw new Error("Canvas is not specified");e.width=e.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){unreachable("Abstract method `_createCanvas` called.")}}class DOMCanvasFactory extends BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document,enableHWA:e=!1}){super({enableHWA:e});this._document=t}_createCanvas(t,e){const i=this._document.createElement("canvas");i.width=t;i.height=e;return i}}class BaseFilterFactory{addFilter(t){return"none"}addHCMFilter(t,e){return"none"}addAlphaFilter(t){return"none"}addLuminosityFilter(t){return"none"}addHighlightHCMFilter(t,e,i,s,n){return"none"}destroy(t=!1){}}class DOMFilterFactory extends BaseFilterFactory{#As;#vs;#ws;#xs;#Cs;#Es;#S=0;constructor({docId:t,ownerDocument:e=globalThis.document}){super();this.#xs=t;this.#Cs=e}get#P(){return this.#vs||=new Map}get#Ts(){return this.#Es||=new Map}get#Ss(){if(!this.#ws){const t=this.#Cs.createElement("div"),{style:e}=t;e.visibility="hidden";e.contain="strict";e.width=e.height=0;e.position="absolute";e.top=e.left=0;e.zIndex=-1;const i=this.#Cs.createElementNS(V,"svg");i.setAttribute("width",0);i.setAttribute("height",0);this.#ws=this.#Cs.createElementNS(V,"defs");t.append(i);i.append(this.#ws);this.#Cs.body.append(t)}return this.#ws}#_s(t){if(1===t.length){const e=t[0],i=new Array(256);for(let t=0;t<256;t++)i[t]=e[t]/255;const s=i.join(",");return[s,s,s]}const[e,i,s]=t,n=new Array(256),r=new Array(256),a=new Array(256);for(let t=0;t<256;t++){n[t]=e[t]/255;r[t]=i[t]/255;a[t]=s[t]/255}return[n.join(","),r.join(","),a.join(",")]}#Ps(t){if(void 0===this.#As){this.#As="";const t=this.#Cs.URL;t!==this.#Cs.baseURI&&(isDataScheme(t)?warn('#createUrl: ignore "data:"-URL for performance reasons.'):this.#As=updateUrlHash(t,""))}return`url(${this.#As}#${t})`}addFilter(t){if(!t)return"none";let e=this.#P.get(t);if(e)return e;const[i,s,n]=this.#_s(t),r=1===t.length?i:`${i}${s}${n}`;e=this.#P.get(r);if(e){this.#P.set(t,e);return e}const a=`g_${this.#xs}_transfer_map_${this.#S++}`,o=this.#Ps(a);this.#P.set(t,o);this.#P.set(r,o);const l=this.#Ds(a);this.#Ms(i,s,n,l);return o}addHCMFilter(t,e){const i=`${t}-${e}`,s="base";let n=this.#Ts.get(s);if(n?.key===i)return n.url;if(n){n.filter?.remove();n.key=i;n.url="none";n.filter=null}else{n={key:i,url:"none",filter:null};this.#Ts.set(s,n)}if(!t||!e)return n.url;const r=this.#ks(t);t=Util.makeHexColor(...r);const a=this.#ks(e);e=Util.makeHexColor(...a);this.#Ss.style.color="";if("#000000"===t&&"#ffffff"===e||t===e)return n.url;const o=new Array(256);for(let t=0;t<=255;t++){const e=t/255;o[t]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}const l=o.join(","),h=`g_${this.#xs}_hcm_filter`,d=n.filter=this.#Ds(h);this.#Ms(l,l,l,d);this.#Is(d);const getSteps=(t,e)=>{const i=r[t]/255,s=a[t]/255,n=new Array(e+1);for(let t=0;t<=e;t++)n[t]=i+t/e*(s-i);return n.join(",")};this.#Ms(getSteps(0,5),getSteps(1,5),getSteps(2,5),d);n.url=this.#Ps(h);return n.url}addAlphaFilter(t){let e=this.#P.get(t);if(e)return e;const[i]=this.#_s([t]),s=`alpha_${i}`;e=this.#P.get(s);if(e){this.#P.set(t,e);return e}const n=`g_${this.#xs}_alpha_map_${this.#S++}`,r=this.#Ps(n);this.#P.set(t,r);this.#P.set(s,r);const a=this.#Ds(n);this.#Fs(i,a);return r}addLuminosityFilter(t){let e,i,s=this.#P.get(t||"luminosity");if(s)return s;if(t){[e]=this.#_s([t]);i=`luminosity_${e}`}else i="luminosity";s=this.#P.get(i);if(s){this.#P.set(t,s);return s}const n=`g_${this.#xs}_luminosity_map_${this.#S++}`,r=this.#Ps(n);this.#P.set(t,r);this.#P.set(i,r);const a=this.#Ds(n);this.#Bs(a);t&&this.#Fs(e,a);return r}addHighlightHCMFilter(t,e,i,s,n){const r=`${e}-${i}-${s}-${n}`;let a=this.#Ts.get(t);if(a?.key===r)return a.url;if(a){a.filter?.remove();a.key=r;a.url="none";a.filter=null}else{a={key:r,url:"none",filter:null};this.#Ts.set(t,a)}if(!e||!i)return a.url;const[o,l]=[e,i].map(this.#ks.bind(this));let h=Math.round(.2126*o[0]+.7152*o[1]+.0722*o[2]),d=Math.round(.2126*l[0]+.7152*l[1]+.0722*l[2]),[c,u]=[s,n].map(this.#ks.bind(this));d{const s=new Array(256),n=(d-h)/i,r=t/255,a=(e-t)/(255*i);let o=0;for(let t=0;t<=i;t++){const e=Math.round(h+t*n),i=r+t*a;for(let t=o;t<=e;t++)s[t]=i;o=e+1}for(let t=o;t<256;t++)s[t]=s[o-1];return s.join(",")},p=`g_${this.#xs}_hcm_${t}_filter`,g=a.filter=this.#Ds(p);this.#Is(g);this.#Ms(getSteps(c[0],u[0],5),getSteps(c[1],u[1],5),getSteps(c[2],u[2],5),g);a.url=this.#Ps(p);return a.url}destroy(t=!1){if(!t||!this.#Es?.size){this.#ws?.parentNode.parentNode.remove();this.#ws=null;this.#vs?.clear();this.#vs=null;this.#Es?.clear();this.#Es=null;this.#S=0}}#Bs(t){const e=this.#Cs.createElementNS(V,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0");t.append(e)}#Is(t){const e=this.#Cs.createElementNS(V,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");t.append(e)}#Ds(t){const e=this.#Cs.createElementNS(V,"filter");e.setAttribute("color-interpolation-filters","sRGB");e.setAttribute("id",t);this.#Ss.append(e);return e}#Rs(t,e,i){const s=this.#Cs.createElementNS(V,e);s.setAttribute("type","discrete");s.setAttribute("tableValues",i);t.append(s)}#Ms(t,e,i,s){const n=this.#Cs.createElementNS(V,"feComponentTransfer");s.append(n);this.#Rs(n,"feFuncR",t);this.#Rs(n,"feFuncG",e);this.#Rs(n,"feFuncB",i)}#Fs(t,e){const i=this.#Cs.createElementNS(V,"feComponentTransfer");e.append(i);this.#Rs(i,"feFuncA",t)}#ks(t){this.#Ss.style.color=t;return getRGB(getComputedStyle(this.#Ss).getPropertyValue("color"))}}t&&warn("Please use the `legacy` build in Node.js environments.");class NodeFilterFactory extends BaseFilterFactory{}class NodeCanvasFactory extends BaseCanvasFactory{_createCanvas(t,e){return process.getBuiltinModule("module").createRequire(import.meta.url)("@napi-rs/canvas").createCanvas(t,e)}}class NodeBinaryDataFactory extends BaseBinaryDataFactory{async _fetch(t,e){return async function node_utils_fetchData(t){const e=process.getBuiltinModule("fs/promises"),i=await e.readFile(t);return new Uint8Array(i)}(t)}}const At=new class WebGPU{#Os=null;#Ls=null;#Ns=null;#Us=null;async#Hs(){if(!globalThis.navigator?.gpu)return!1;try{const t=await navigator.gpu.requestAdapter();if(!t)return!1;this.#Us=navigator.gpu.getPreferredCanvasFormat();this.#Ls=await t.requestDevice();return!0}catch{return!1}}init(){return this.#Os||=this.#Hs()}get isReady(){return null!==this.#Ls}loadMeshShader(){if(!this.#Ls||this.#Ns)return;const t=this.#Ls.createShaderModule({code:"\nstruct Uniforms {\n offsetX : f32,\n offsetY : f32,\n scaleX : f32,\n scaleY : f32,\n paddedWidth : f32,\n paddedHeight : f32,\n borderSize : f32,\n _pad : f32,\n};\n\n@group(0) @binding(0) var u : Uniforms;\n\nstruct VertexInput {\n @location(0) position : vec2,\n @location(1) color : vec4,\n};\n\nstruct VertexOutput {\n @builtin(position) position : vec4,\n @location(0) color : vec3,\n};\n\n@vertex\nfn vs_main(in : VertexInput) -> VertexOutput {\n var out : VertexOutput;\n let cx = (in.position.x + u.offsetX) * u.scaleX;\n let cy = (in.position.y + u.offsetY) * u.scaleY;\n out.position = vec4(\n ((cx + u.borderSize) / u.paddedWidth) * 2.0 - 1.0,\n 1.0 - ((cy + u.borderSize) / u.paddedHeight) * 2.0,\n 0.0,\n 1.0\n );\n out.color = in.color.rgb;\n return out;\n}\n\n@fragment\nfn fs_main(in : VertexOutput) -> @location(0) vec4 {\n return vec4(in.color, 1.0);\n}\n"});this.#Ns=this.#Ls.createRenderPipeline({layout:"auto",vertex:{module:t,entryPoint:"vs_main",buffers:[{arrayStride:8,attributes:[{shaderLocation:0,offset:0,format:"float32x2"}]},{arrayStride:4,attributes:[{shaderLocation:1,offset:0,format:"unorm8x4"}]}]},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this.#Us}]},primitive:{topology:"triangle-list"}})}draw(t,e,i,s,n,r,a,o){this.loadMeshShader();const l=this.#Ls,{offsetX:h,offsetY:d,scaleX:c,scaleY:u}=s,p=l.createBuffer({size:Math.max(t.byteLength,4),usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST});t.byteLength>0&&l.queue.writeBuffer(p,0,t);const g=l.createBuffer({size:Math.max(e.byteLength,4),usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST});e.byteLength>0&&l.queue.writeBuffer(g,0,e);const m=l.createBuffer({size:32,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});l.queue.writeBuffer(m,0,new Float32Array([h,d,c,u,r,a,o,0]));const f=l.createBindGroup({layout:this.#Ns.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:m}}]}),b=new OffscreenCanvas(r,a),y=b.getContext("webgpu");y.configure({device:l,format:this.#Us,alphaMode:n?"opaque":"premultiplied"});const A=n?{r:n[0]/255,g:n[1]/255,b:n[2]/255,a:1}:{r:0,g:0,b:0,a:0},v=l.createCommandEncoder(),w=v.beginRenderPass({colorAttachments:[{view:y.getCurrentTexture().createView(),clearValue:A,loadOp:"clear",storeOp:"store"}]});if(i>0){w.setPipeline(this.#Ns);w.setBindGroup(0,f);w.setVertexBuffer(0,p);w.setVertexBuffer(1,g);w.draw(i)}w.end();l.queue.submit([v.finish()]);p.destroy();g.destroy();m.destroy();return b.transferToImageBitmap()}};const vt="Fill",wt="Stroke",xt="Shading";function applyBoundingBox(t,e){if(!e)return;const i=e[2]-e[0],s=e[3]-e[1],n=new Path2D;n.rect(e[0],e[1],i,s);t.clip(n)}class BaseShadingPattern{isModifyingCurrentTransform(){return!1}getPattern(){unreachable("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}isOriginBased(){return 0===this._p0[0]&&0===this._p0[1]&&(!this.isRadial()||0===this._p1[0]&&0===this._p1[1])}isRadial(){return"radial"===this._type}areConic(){if(!this.isRadial())return!1;const t=Math.hypot(this._p0[0]-this._p1[0],this._p0[1]-this._p1[1]);return t+this._r1>this._r0&&t+this._r0>this._r1}_createGradient(t,e=null){let i,s=this._p0,n=this._p1;if(e){s=s.slice();n=n.slice();Util.applyTransform(s,e);Util.applyTransform(n,e)}if("axial"===this._type)i=t.createLinearGradient(s[0],s[1],n[0],n[1]);else if("radial"===this._type){let r=this._r0,a=this._r1;if(e){const t=new Float32Array(2);Util.singularValueDecompose2dScale(e,t);r*=t[0];a*=t[0]}i=t.createRadialGradient(s[0],s[1],r,n[0],n[1],a)}for(const t of this._colorStops)i.addColorStop(t[0],t[1]);return i}_createReversedGradient(t,e=null){let i=this._p1,s=this._p0;if(e){i=i.slice();s=s.slice();Util.applyTransform(i,e);Util.applyTransform(s,e)}let n=this._r1,r=this._r0;if(e){const t=new Float32Array(2);Util.singularValueDecompose2dScale(e,t);n*=t[0];r*=t[0]}const a=t.createRadialGradient(i[0],i[1],n,s[0],s[1],r),o=this._colorStops.map(([t,e])=>[1-t,e]).reverse();for(const[t,e]of o)a.addColorStop(t,e);return a}getPattern(t,e,i,s){let n;if(s===wt||s===vt){if(this.isOriginBased()){let s=Util.transform(i,e.baseTransform);this.matrix&&(s=Util.transform(s,this.matrix));const n=.001,r=Math.hypot(s[0],s[1]),a=Math.hypot(s[2],s[3]),o=(s[0]*s[2]+s[1]*s[3])/(r*a);if(Math.abs(o)l[2*s+1]){u=i;i=s;s=u;u=r;r=a;a=u}if(l[2*s+1]>l[2*n+1]){u=s;s=n;n=u;u=a;a=o;o=u}if(l[2*i+1]>l[2*s+1]){u=i;i=s;s=u;u=r;r=a;a=u}const p=(l[2*i]+e.offsetX)*e.scaleX,g=(l[2*i+1]+e.offsetY)*e.scaleY,m=(l[2*s]+e.offsetX)*e.scaleX,f=(l[2*s+1]+e.offsetY)*e.scaleY,b=(l[2*n]+e.offsetX)*e.scaleX,y=(l[2*n+1]+e.offsetY)*e.scaleY;if(g>=y)return;const A=h[4*r],v=h[4*r+1],w=h[4*r+2],x=h[4*a],C=h[4*a+1],E=h[4*a+2],T=h[4*o],S=h[4*o+1],_=h[4*o+2],P=Math.round(g),D=Math.round(y);let M,k,I,F,B,R,O,L;for(let t=P;t<=D;t++){if(ty?1:f===y?0:(f-t)/(f-y);M=m-(m-b)*e;k=x-(x-T)*e;I=C-(C-S)*e;F=E-(E-_)*e}let e;e=ty?1:(g-t)/(g-y);B=p-(p-b)*e;R=A-(A-T)*e;O=v-(v-S)*e;L=w-(w-_)*e;const i=Math.round(Math.min(M,B)),s=Math.round(Math.max(M,B));let n=c*t+4*i;for(let t=i;t<=s;t++){e=(M-t)/(M-B);e<0?e=0:e>1&&(e=1);d[n++]=k-(k-R)*e|0;d[n++]=I-(I-O)*e|0;d[n++]=F-(F-L)*e|0;d[n++]=255}}}class MeshShadingPattern extends BaseShadingPattern{constructor(t){super();this._posData=t[2];this._colData=t[3];this._vertexCount=t[4];this._bounds=t[5];this._bbox=t[6];this._background=t[7];this.matrix=null;!function loadMeshShader(){At.loadMeshShader()}()}_createMeshCanvas(t,e,i){const s=Math.floor(this._bounds[0]),n=Math.floor(this._bounds[1]),r=Math.ceil(this._bounds[2])-s,a=Math.ceil(this._bounds[3])-n,o=Math.min(Math.ceil(Math.abs(r*t[0]*1.1)),3e3)||1,l=Math.min(Math.ceil(Math.abs(a*t[1]*1.1)),3e3)||1,h=r?r/o:1,d=a?a/l:1,c={coords:this._posData,colors:this._colData,offsetX:-s,offsetY:-n,scaleX:1/h,scaleY:1/d},u=o+4,p=l+4,g=i.create(u,p);if(function isGPUReady(){return At.isReady}()&&this._vertexCount>48)g.context.drawImage(function drawMeshWithGPU(t,e,i,s,n,r,a,o){return At.draw(t,e,i,s,n,r,a,o)}(this._posData,this._colData,this._vertexCount,c,e,u,p,2),0,0);else{const t=g.context.createImageData(o,l);if(e){const i=t.data;for(let t=0,s=i.length;tl+1e-6||e>h+1e-6)return null;const d=Math.floor((i-a)/l)+1,c=Math.ceil((i+t-n)/l)-1,u=Math.floor((s-o)/h)+1,p=Math.ceil((s+e-r)/h)-1;return c<=d&&p<=u?[d,u]:null}updatePatternDims(t,e){const i=Util.inverseTransform(this.patternBaseMatrix),s=[t[0],t[1]],n=[t[2],t[3]];Util.applyTransform(s,i);Util.applyTransform(n,i);e[0]=Math.abs(n[0]-s[0]);e[1]=Math.abs(n[1]-s[1]);e[2]=Math.min(s[0],n[0]);e[3]=Math.min(s[1],n[1])}_renderTileCanvas(t,e,i,s){const[n,r,a,o]=this.bbox,l=t.canvasFactory.create(i.size,s.size),h=l.context,d=this.canvasGraphicsFactory.createCanvasGraphics(h,e);d.groupLevel=t.groupLevel;this.setFillAndStrokeStyleToContext(d,this.paintType,this.color);h.translate(-i.scale*n,-s.scale*r);d.transform(0,i.scale,0,0,s.scale,0,0);h.save();d.dependencyTracker?.save();this.clipBbox(d,n,r,a,o);d.baseTransform=getCurrentTransform(d.ctx);d.executeOperatorList(this.operatorList);d.endDrawing();d.dependencyTracker?.restore();h.restore();return l}_getCombinedScales(){const t=new Float32Array(2);Util.singularValueDecompose2dScale(this.matrix,t);const[e,i]=t;Util.singularValueDecompose2dScale(this.baseTransform,t);return[e*t[0],i*t[1]]}drawPattern(t,e,i=!1,[s,n],r){const[a,o,l,h]=this.bbox,d=t.dependencyTracker;d&&(t.dependencyTracker=new CanvasNestedDependencyTracker(d,r));t.save();i?t.ctx.clip(e,"evenodd"):t.ctx.clip(e);t.ctx.setTransform(...this.patternBaseMatrix);t.ctx.translate(s*this.xstep,n*this.ystep);if(this.needsIsolation||1!==t.ctx.globalAlpha||"source-over"!==t.ctx.globalCompositeOperation||t.inSMaskMode){const e=l-a,i=h-o,[s,n]=this._getCombinedScales(),d=this.getSizeAndScale(e,this.ctx.canvas.width,s),c=this.getSizeAndScale(i,this.ctx.canvas.height,n),u=this._renderTileCanvas(t,r,d,c);t.ctx.drawImage(u.canvas,a,o,e,i);t.canvasFactory.destroy(u)}else{this.setFillAndStrokeStyleToContext(t,this.paintType,this.color);this.clipBbox(t,a,o,l,h);t.baseTransformStack.push(t.baseTransform);t.baseTransform=getCurrentTransform(t.ctx);t.executeOperatorList(this.operatorList);t.baseTransform=t.baseTransformStack.pop()}t.restore();d&&(t.dependencyTracker=d)}createPatternCanvas(t,e){const[i,s,n,r]=this.bbox,a=n-i,o=r-s;let{xstep:l,ystep:h}=this;l=Math.abs(l);h=Math.abs(h);info("TilingType: "+this.tilingType);const[d,c]=this._getCombinedScales();let u=a,p=o,g=!1,m=!1;Math.ceil(l*d)>=Math.ceil(a*d)?u=l:g=!0;Math.ceil(h*c)>=Math.ceil(o*c)?p=h:m=!0;const f=this.getSizeAndScale(u,this.ctx.canvas.width,d),b=this.getSizeAndScale(p,this.ctx.canvas.height,c),y=this._renderTileCanvas(t,e,f,b);if(g||m){const e=y.canvas;g&&(u=l);m&&(p=h);const n=this.getSizeAndScale(u,this.ctx.canvas.width,d),r=this.getSizeAndScale(p,this.ctx.canvas.height,c),f=n.size,b=r.size,A=t.canvasFactory.create(f,b),v=A.context,w=g?Math.floor(a/l):0,x=m?Math.floor(o/h):0;for(let t=0;t<=w;t++)for(let i=0;i<=x;i++)v.drawImage(e,f*t,b*i,f,b,0,0,f,b);t.canvasFactory.destroy(y);return{canvas:A.canvas,canvasEntry:A,scaleX:n.scale,scaleY:r.scale,offsetX:i,offsetY:s}}return{canvas:y.canvas,canvasEntry:y,scaleX:f.scale,scaleY:b.scale,offsetX:i,offsetY:s}}getSizeAndScale(t,e,i){const s=Math.max(TilingPattern.MAX_PATTERN_SIZE,e);let n=Math.ceil(t*i);n>=s?n=s:i=n/t;return{scale:i,size:n}}clipBbox(t,e,i,s,n){const r=s-e,a=n-i,o=new Path2D;o.rect(e,i,r,a);Util.axialAlignedBoundingBox([e,i,s,n],getCurrentTransform(t.ctx),t.current.minMax);t.ctx.clip(o);t.current.updateClipFromPath()}setFillAndStrokeStyleToContext(t,e,i){const s=t.ctx,n=t.current;n.patternFill=n.patternStroke=!1;switch(e){case Ct:const{fillStyle:t,strokeStyle:r}=this.ctx;s.fillStyle=n.fillColor=t;s.strokeStyle=n.strokeColor=r;break;case Et:s.fillStyle=s.strokeStyle=i;n.fillColor=n.strokeColor=i;break;default:throw new FormatError(`Unsupported paint type: ${e}`)}}isModifyingCurrentTransform(){return!1}getPattern(t,e,i,s,n){const r=s!==xt?Util.transform(i,this.patternBaseMatrix):i,a=this.createPatternCanvas(e,n);let o=new DOMMatrix(r);o=o.translate(a.offsetX,a.offsetY);o=o.scale(1/a.scaleX,1/a.scaleY);const l=t.createPattern(a.canvas,"repeat");e.canvasFactory.destroy(a.canvasEntry);l.setTransform(o);return l}}function convertBlackAndWhiteToRGBA({src:t,srcPos:e=0,dest:i,width:s,height:n,nonBlackColor:r=4294967295,inverseDecode:a=!1}){const o=FeatureTest.isLittleEndian?4278190080:255,[l,h]=a?[r,o]:[o,r],d=s>>3,c=7&s,u=l^h,p=t.length;i=new Uint32Array(i.buffer);let g=0;for(let s=0;s>7&1)&u;i[g+1]=l^-(s>>6&1)&u;i[g+2]=l^-(s>>5&1)&u;i[g+3]=l^-(s>>4&1)&u;i[g+4]=l^-(s>>3&1)&u;i[g+5]=l^-(s>>2&1)&u;i[g+6]=l^-(s>>1&1)&u;i[g+7]=l^-(1&s)&u}if(0===c)continue;const s=e>7-t&1)&u}return{srcPos:e,destPos:g}}const Tt=16,St=new DOMMatrix,_t=new Float32Array(2);function drawImageAtIntegerCoords(t,e,i,s,n,r,a,o,l,h){const[d,c,u,p,g,m]=getCurrentTransform(t);if(0===c&&0===u){const f=a*d+g,b=Math.round(f),y=o*p+m,A=Math.round(y),v=(a+l)*d+g,w=Math.abs(Math.round(v)-b)||1,x=(o+h)*p+m,C=Math.abs(Math.round(x)-A)||1;t.setTransform(Math.sign(d),0,0,Math.sign(p),b,A);t.drawImage(e,i,s,n,r,0,0,w,C);t.setTransform(d,c,u,p,g,m);return[w,C]}if(0===d&&0===p){const f=o*u+g,b=Math.round(f),y=a*c+m,A=Math.round(y),v=(o+h)*u+g,w=Math.abs(Math.round(v)-b)||1,x=(a+l)*c+m,C=Math.abs(Math.round(x)-A)||1;t.setTransform(0,Math.sign(c),Math.sign(u),0,b,A);t.drawImage(e,i,s,n,r,0,0,C,w);t.setTransform(d,c,u,p,g,m);return[C,w]}t.drawImage(e,i,s,n,r,a,o,l,h);return[Math.hypot(d,c)*l,Math.hypot(u,p)*h]}class CanvasExtraState{alphaIsShape=!1;fontSize=0;fontSizeScale=1;textMatrix=null;textMatrixScale=1;fontMatrix=s;leading=0;x=0;y=0;lineX=0;lineY=0;charSpacing=0;wordSpacing=0;textHScale=1;textRenderingMode=y;textRise=0;fillColor="#000000";strokeColor="#000000";tilingPatternDims=null;patternFill=!1;patternStroke=!1;fillAlpha=1;strokeAlpha=1;lineWidth=1;activeSMask=null;transferMaps="none";minMax=i.slice();constructor(t,e){this.clipBox=new Float32Array([0,0,t,e])}clone(){const t=Object.create(this);t.clipBox=this.clipBox.slice();t.minMax=this.minMax.slice();t.tilingPatternDims=this.tilingPatternDims?.slice();return t}getPathBoundingBox(t=vt,e=null){const i=this.minMax.slice();if(t===wt){e||unreachable("Stroke bounding box must include transform.");Util.singularValueDecompose2dScale(e,_t);const t=_t[0]*this.lineWidth/2,s=_t[1]*this.lineWidth/2;i[0]-=t;i[1]-=s;i[2]+=t;i[3]+=s}return i}updateClipFromPath(){const t=Util.intersect(this.clipBox,this.getPathBoundingBox());this.startNewPathAndClipBox(t||[0,0,0,0])}isEmptyClip(){return this.minMax[0]===1/0}startNewPathAndClipBox(t){this.clipBox.set(t,0);this.minMax.set(i,0)}getClippedPathBoundingBox(t=vt,e=null){return Util.intersect(this.clipBox,this.getPathBoundingBox(t,e))}}function putBinaryImageData(t,e){if(e instanceof ImageData){t.putImageData(e,0,0);return}const i=e.height,s=e.width,n=i%Tt,r=(i-n)/Tt,a=0===n?r:r+1,o=t.createImageData(s,Tt);let l,h=0;const d=e.data,c=o.data;let u,p,g,m;if(e.kind===E.GRAYSCALE_1BPP){const e=d.byteLength,i=new Uint32Array(c.buffer,0,c.byteLength>>2),m=i.length,f=s+7>>3,b=4294967295,y=FeatureTest.isLittleEndian?4278190080:255;for(u=0;uf?s:8*t-7,a=-8&r;let o=0,c=0;for(;n>=1}}for(;l=r){g=n;m=s*g}l=0;for(p=m;p--;){c[l++]=d[h++];c[l++]=d[h++];c[l++]=d[h++];c[l++]=255}t.putImageData(o,0,u*Tt)}}}function putBinaryImageMask(t,e){if(e.bitmap){t.drawImage(e.bitmap,0,0);return}const i=e.height,s=e.width,n=i%Tt,r=(i-n)/Tt,a=0===n?r:r+1,o=t.createImageData(s,Tt);let l=0;const h=e.data,d=o.data;for(let e=0;e10&&"function"==typeof i,d=h?Date.now()+15:0;let c=0;const u=this.commonObjs,p=this.objs;let g,m;for(;;){if(void 0!==s){if(o===s.nextBreakPoint){s.breakIt(o,i);return o}if(s.shouldSkip(o)){if(++o===l)return o;continue}}if(!n||n(o)){g=a[o];m=r[o]??null;if(g!==I.dependency)null===m?this[g](o):this[g](o,...m);else for(const t of m){this.dependencyTracker?.recordNamedData(t,o);const e=t.startsWith("g_")?u:p;if(!e.has(t)){e.get(t,i);return o}}}o++;if(o===l)return o;if(h&&++c>10){if(Date.now()>d){i();return o}c=0}}}#zs(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.current.activeSMask=null;this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.canvasFactory.destroy(this.transparentCanvasEntry);this.transparentCanvas=null;this.transparentCanvasEntry=null}}endDrawing(){this.#zs();for(const t of this.smaskGroupCanvases)this.canvasFactory.destroy(t);this.smaskGroupCanvases.length=0;this._clearPreparedSMask();this.tempSMask=null;this.smaskStack.length=0;this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#Gs()}#Gs(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width??t.displayWidth,s=t.height??t.displayHeight,n=[];let r=Math.max(Math.hypot(e[0],e[1]),1),a=Math.max(Math.hypot(e[2],e[3]),1),o=i,l=s;for(;r>2&&o>1||a>2&&l>1;){let t=o,e=l;if(r>2&&o>1){t=Math.ceil(o/2);r/=o/t}if(a>2&&l>1){e=Math.ceil(l/2);a/=l/e}n.push({newWidth:t,newHeight:e});o=t;l=e}if(0===n.length)return{img:t,paintWidth:i,paintHeight:s,tmpCanvas:null};if(1===n.length){const{newWidth:e,newHeight:r}=n[0],a=this.canvasFactory.create(e,r);a.context.drawImage(t,0,0,i,s,0,0,e,r);return{img:a.canvas,paintWidth:e,paintHeight:r,tmpCanvas:a}}let h=this.canvasFactory.create(1,1),d=this.canvasFactory.create(1,1),c=i,u=s,p=t;for(const{newWidth:t,newHeight:e}of n){this.canvasFactory.reset(d,t,e);d.context.drawImage(p,0,0,c,u,0,0,t,e);[h,d]=[d,h];p=h.canvas;c=t;u=e}this.canvasFactory.destroy(d);return{img:h.canvas,paintWidth:c,paintHeight:u,tmpCanvas:h}}_createMaskCanvas(t,e){const s=this.ctx,{width:n,height:r}=e,a=this.current.fillColor,o=this.current.patternFill,l=getCurrentTransform(s);let h,d,c,u;if((e.bitmap||e.data)&&e.count>1){const i=e.bitmap||e.data.buffer;d=JSON.stringify(o?l:[l.slice(0,4),a]);h=this._cachedBitmapsMap.getOrInsertComputed(i,makeMap);const s=h.get(d);if(s&&!o){const e=Math.round(Math.min(l[0],l[2])+l[4]),i=Math.round(Math.min(l[1],l[3])+l[5]);this.dependencyTracker?.recordDependencies(t,ot);return{canvas:s,offsetX:e,offsetY:i}}c=s}if(!c){u=this.canvasFactory.create(n,r);putBinaryImageMask(u.context,e)}let p=Util.transform(l,[1/n,0,0,-1/r,0,0]);p=Util.transform(p,[1,0,0,1,0,-r]);const g=i.slice();Util.axialAlignedBoundingBox([0,0,n,r],p,g);const[m,f,b,y]=g,A=Math.round(b-m)||1,v=Math.round(y-f)||1,w=this.canvasFactory.create(A,v),x=w.context,C=m,E=f;x.translate(-C,-E);x.transform(...p);let T=null;if(!c){const t=this._scaleImage(u.canvas,getCurrentTransformInverse(x));c=t.img;T=t.tmpCanvas;if(c!==u.canvas){this.canvasFactory.destroy(u);u=null}if(h&&o){h.set(d,c);T=null;u=null}}x.imageSmoothingEnabled=getImageSmoothingEnabled(getCurrentTransform(x),e.interpolate);drawImageAtIntegerCoords(x,c,0,0,c.width,c.height,0,0,n,r);T&&this.canvasFactory.destroy(T);u&&this.canvasFactory.destroy(u);x.globalCompositeOperation="source-in";const S=Util.transform(getCurrentTransformInverse(x),[1,0,0,1,-C,-E]);x.fillStyle=o?a.getPattern(s,this,S,vt,t):a;x.fillRect(0,0,n,r);h&&!o&&h.set(d,w.canvas);this.dependencyTracker?.recordDependencies(t,ot);return{canvas:w.canvas,canvasEntry:h&&!o?null:w,offsetX:Math.round(C),offsetY:Math.round(E)}}setLineWidth(t,e){this.dependencyTracker?.recordSimpleData("lineWidth",t);e!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=e;this.ctx.lineWidth=e}setLineCap(t,e){this.dependencyTracker?.recordSimpleData("lineCap",t);this.ctx.lineCap=Pt[e]}setLineJoin(t,e){this.dependencyTracker?.recordSimpleData("lineJoin",t);this.ctx.lineJoin=Dt[e]}setMiterLimit(t,e){this.dependencyTracker?.recordSimpleData("miterLimit",t);this.ctx.miterLimit=e}setDash(t,e,i){this.dependencyTracker?.recordSimpleData("dash",t);const s=this.ctx;if(void 0!==s.setLineDash){s.setLineDash(e);s.lineDashOffset=i}}setRenderingIntent(t,e){}setFlatness(t,e){}setGState(t,e){for(const[i,s]of e)switch(i){case"LW":this.setLineWidth(t,s);break;case"LC":this.setLineCap(t,s);break;case"LJ":this.setLineJoin(t,s);break;case"ML":this.setMiterLimit(t,s);break;case"D":this.setDash(t,s[0],s[1]);break;case"RI":this.setRenderingIntent(t,s);break;case"FL":this.setFlatness(t,s);break;case"Font":this.setFont(t,s[0],s[1]);break;case"CA":this.dependencyTracker?.recordSimpleData("strokeAlpha",t);this.current.strokeAlpha=s;break;case"ca":this.dependencyTracker?.recordSimpleData("fillAlpha",t);this.ctx.globalAlpha=this.current.fillAlpha=s;break;case"BM":this.dependencyTracker?.recordSimpleData("globalCompositeOperation",t);this.ctx.globalCompositeOperation=s;break;case"SMask":this.dependencyTracker?.recordSimpleData("SMask",t);this.current.activeSMask=s?this.tempSMask:null;this.current.activeSMask&&(this.current.activeSMask.blendMode=this.ctx.globalCompositeOperation);this.tempSMask=null;this.checkSMaskState(t);break;case"TR":this.dependencyTracker?.recordSimpleData("filter",t);this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(s)}}get inSMaskMode(){return!!this.suspendedCtx}_clearPreparedSMask(){if(this.smaskPreparedEntry){this.canvasFactory.destroy(this.smaskPreparedEntry);this.smaskPreparedEntry=null}this.smaskPreparedFor=null;this.smaskPreparedOffsetX=0;this.smaskPreparedOffsetY=0}_ensurePreparedSMask(t,e,i){if(t!==this.smaskPreparedFor){this._clearPreparedSMask();this._prepareSMaskCanvas(t,e,i)}}checkSMaskState(t){const e=this.inSMaskMode;this.current.activeSMask&&!e?this.beginSMaskMode(t):!this.current.activeSMask&&e?this.endSMaskMode():this.current.activeSMask&&e&&this._ensurePreparedSMask(this.current.activeSMask,this.ctx.canvas.width,this.ctx.canvas.height)}_prepareSMaskCanvas(t,e,i){const{canvas:s,subtype:n,backdrop:r,transferMap:a}=t,o="Luminosity"===n||"Alpha"===n&&a;if(!r&&!o){this.smaskPreparedFor=t;return}let l,h,d;if(r&&o){const o=this.canvasFactory.create(e,i),c=o.context;c.drawImage(s,t.offsetX,t.offsetY);c.globalCompositeOperation="destination-atop";c.fillStyle=r;c.fillRect(0,0,e,i);c.globalCompositeOperation="source-over";l=this.canvasFactory.create(e,i);const u=l.context;u.filter="Alpha"===n?this.filterFactory.addAlphaFilter(a):this.filterFactory.addLuminosityFilter(a);u.drawImage(o.canvas,0,0);u.filter="none";this.canvasFactory.destroy(o);h=d=0}else if(o){l=this.canvasFactory.create(s.width,s.height);const e=l.context;e.filter="Alpha"===n?this.filterFactory.addAlphaFilter(a):this.filterFactory.addLuminosityFilter(a);e.drawImage(s,0,0);e.filter="none";({offsetX:h,offsetY:d}=t)}else{l=this.canvasFactory.create(e,i);const n=l.context;n.drawImage(s,t.offsetX,t.offsetY);n.globalCompositeOperation="destination-atop";n.fillStyle=r;n.fillRect(0,0,e,i);n.globalCompositeOperation="source-over";h=d=0}this.smaskPreparedEntry=l;this.smaskPreparedFor=t;this.smaskPreparedOffsetX=h;this.smaskPreparedOffsetY=d}beginSMaskMode(t){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const{width:e,height:i}=this.ctx.canvas,s=this.canvasFactory.create(e,i);this.smaskScratchCanvas=s;this.suspendedCtx=this.ctx;const n=this.ctx=s.context;n.setTransform(this.suspendedCtx.getTransform());copyCtxState(this.suspendedCtx,n);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function(){e.save();this.__originalSave()};t.restore=function(){e.restore();this.__originalRestore()};t.translate=function(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function(t,i,s,n,r,a){e.transform(t,i,s,n,r,a);this.__originalTransform(t,i,s,n,r,a)};t.setTransform=function(t,i,s,n,r,a){if(void 0===i){e.setTransform(t);this.__originalSetTransform(t)}else{e.setTransform(t,i,s,n,r,a);this.__originalSetTransform(t,i,s,n,r,a)}};t.resetTransform=function(){e.resetTransform();this.__originalResetTransform()};t.rotate=function(t){e.rotate(t);this.__originalRotate(t)};t.clip=function(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,r,a){e.bezierCurveTo(t,i,s,n,r,a);this.__originalBezierCurveTo(t,i,s,n,r,a)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(n,this.suspendedCtx);this._ensurePreparedSMask(this.current.activeSMask,e,i);this.setGState(t,[["BM","source-over"]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null;this.canvasFactory.destroy(this.smaskScratchCanvas);this.smaskScratchCanvas=null;this._clearPreparedSMask()}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask,i=this.suspendedCtx;this.composeSMask(i,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(t[0],t[1],t[2]-t[0],t[3]-t[1]);this.ctx.restore()}composeSMask(t,e,i,s){const n=s[0],r=s[1],a=s[2]-n,o=s[3]-r;if(0===a||0===o)return;const l=this.smaskPreparedEntry;if(l){const t=n-this.smaskPreparedOffsetX,e=r-this.smaskPreparedOffsetY;i.save();i.globalAlpha=1;i.setTransform(1,0,0,1,0,0);const s=new Path2D;s.rect(n,r,a,o);i.clip(s);i.globalCompositeOperation="destination-in";i.drawImage(l.canvas,t,e,a,o,n,r,a,o);i.restore()}else this.genericComposeSMask(e.context,i,a,o,n,r,e.offsetX,e.offsetY);t.save();t.globalAlpha=1;t.globalCompositeOperation=e.blendMode||"source-over";t.setTransform(1,0,0,1,0,0);t.drawImage(i.canvas,n,r,a,o,n,r,a,o);t.restore()}genericComposeSMask(t,e,i,s,n,r,a,o){const l=t.canvas,h=n-a,d=r-o;e.save();e.globalAlpha=1;e.setTransform(1,0,0,1,0,0);const c=new Path2D;c.rect(n,r,i,s);e.clip(c);e.globalCompositeOperation="destination-in";e.drawImage(l,h,d,i,s,n,r,i,s);e.restore()}save(t){this.inSMaskMode&©CtxState(this.ctx,this.suspendedCtx);this.ctx.save();const e=this.current;this.stateStack.push(e);this.current=e.clone();this.dependencyTracker?.save(t)}restore(t){this.dependencyTracker?.restore(t);if(0!==this.stateStack.length){this.current=this.stateStack.pop();this.ctx.restore();if(this.inSMaskMode){copyCtxState(this.suspendedCtx,this.ctx);this.ctx.setTransform(this.suspendedCtx.getTransform())}this.checkSMaskState(t);this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}else this.inSMaskMode&&this.endSMaskMode()}transform(t,e,i,s,n,r,a){this.dependencyTracker?.recordIncrementalData("transform",t);this.ctx.transform(e,i,s,n,r,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i,s){let[n]=i;if(!s){n||=i[0]=new Path2D;e!==I.stroke&&e!==I.closeStroke&&(this.current.tilingPatternDims=null);this[e](t,n);return}if(null!==this.dependencyTracker){const i=e===I.stroke?this.current.lineWidth/2:0;this.dependencyTracker.resetBBox(t).recordBBox(t,this.ctx,s[0]-i,s[2]+i,s[1]-i,s[3]+i).recordDependencies(t,["transform"])}n instanceof Path2D||(n=i[0]=makePathFromDrawOPS(n));Util.axialAlignedBoundingBox(s,getCurrentTransform(this.ctx),this.current.minMax);const r=this.current.tilingPatternDims;if(r&&e!==I.stroke&&e!==I.closeStroke&&this.current.fillColor instanceof TilingPattern){const t=Util.intersect(this.current.clipBox,this.current.minMax);t?this.current.fillColor.updatePatternDims(t,r):this.current.tilingPatternDims=null}this[e](t,n);this._pathStartIdx=t}closePath(t){this.ctx.closePath()}stroke(t,e,i=!0){const s=this.ctx,n=this.current.strokeColor;s.globalAlpha=this.current.strokeAlpha;if(this.contentVisible)if("object"==typeof n&&n?.getPattern){const i=n.isModifyingCurrentTransform()?s.getTransform():null;s.save();s.strokeStyle=n.getPattern(s,this,getCurrentTransformInverse(s),wt,t);if(i){const t=new Path2D;t.addPath(e,s.getTransform().invertSelf().multiplySelf(i));e=t}this.rescaleAndStroke(e,!1);s.restore()}else this.rescaleAndStroke(e,!0);this.dependencyTracker?.recordDependencies(t,et);i&&this.consumePath(t,e,this.current.getClippedPathBoundingBox(wt,getCurrentTransform(this.ctx)));s.globalAlpha=this.current.fillAlpha}closeStroke(t,e){this.stroke(t,e)}fill(t,e,i=!0){const s=this.ctx,n=this.current.fillColor,r=this.current.patternFill;let a=!1;const o=this.current.getClippedPathBoundingBox();this.dependencyTracker?.recordDependencies(t,it);if(r){const r=this.current.tilingPatternDims,l=r&&n.canSkipPatternCanvas(r);if(l){n.drawPattern(this,e,this.pendingEOFill,l,t);this.pendingEOFill=!1;i&&this.consumePath(t,e,o);this.current.tilingPatternDims=null;return}const h=n.isModifyingCurrentTransform()?s.getTransform():null;this.dependencyTracker?.save(t);s.save();s.fillStyle=n.getPattern(s,this,getCurrentTransformInverse(s),vt,t);if(h){const t=new Path2D;t.addPath(e,s.getTransform().invertSelf().multiplySelf(h));e=t}a=!0}if(this.contentVisible&&null!==o)if(this.pendingEOFill){s.fill(e,"evenodd");this.pendingEOFill=!1}else s.fill(e);if(a){s.restore();this.dependencyTracker?.restore(t)}i&&this.consumePath(t,e,o)}eoFill(t,e){this.pendingEOFill=!0;this.fill(t,e)}fillStroke(t,e){this.fill(t,e,!1);this.stroke(t,e,!1);this.consumePath(t,e)}eoFillStroke(t,e){this.pendingEOFill=!0;this.fillStroke(t,e)}closeFillStroke(t,e){this.fillStroke(t,e)}closeEOFillStroke(t,e){this.pendingEOFill=!0;this.fillStroke(t,e)}endPath(t,e){this.consumePath(t,e)}rawFillPath(t,e){this.ctx.fill(e);this.dependencyTracker?.recordDependencies(t,nt).recordOperation(t)}clip(t){this.dependencyTracker?.recordFutureForcedDependency("clipMode",t);this.pendingClip=Mt}eoClip(t){this.dependencyTracker?.recordFutureForcedDependency("clipMode",t);this.pendingClip=kt}beginText(t){this.current.textMatrix=null;this.current.textMatrixScale=1;this.current.x=this.current.lineX=0;this.current.y=this.current.lineY=0;this.dependencyTracker?.recordOpenMarker(t).resetIncrementalData("sameLineText").resetIncrementalData("moveText",t)}endText(t){const e=this.pendingTextPaths,i=this.ctx;if(this.dependencyTracker){const{dependencyTracker:i}=this;void 0!==e&&i.recordFutureForcedDependency("textClip",i.getOpenMarker()).recordFutureForcedDependency("textClip",t);i.recordCloseMarker(t)}if(void 0!==e){const t=new Path2D,s=i.getTransform().invertSelf();for(const{transform:i,x:n,y:r,fontSize:a,path:o}of e)o&&t.addPath(o,new DOMMatrix(i).preMultiplySelf(s).translate(n,r).scale(a,-a));i.clip(t)}delete this.pendingTextPaths}setCharSpacing(t,e){this.dependencyTracker?.recordSimpleData("charSpacing",t);this.current.charSpacing=e}setWordSpacing(t,e){this.dependencyTracker?.recordSimpleData("wordSpacing",t);this.current.wordSpacing=e}setHScale(t,e){this.dependencyTracker?.recordSimpleData("hScale",t);this.current.textHScale=e/100}setLeading(t,e){this.dependencyTracker?.recordSimpleData("leading",t);this.current.leading=-e}setFont(t,e,i){this.dependencyTracker?.recordSimpleData("font",t).recordSimpleDataFromNamed("fontObj",e,t);const n=this.commonObjs.get(e),r=this.current;if(!n)throw new Error(`Can't find font for ${e}`);r.fontMatrix=n.fontMatrix||s;0!==r.fontMatrix[0]&&0!==r.fontMatrix[3]||warn("Invalid font matrix for font "+e);if(i<0){i=-i;r.fontDirection=-1}else r.fontDirection=1;this.current.font=n;this.current.fontSize=i;if(n.isType3Font)return;const a=n.loadedName||"sans-serif",o=n.systemFontInfo?.css||`"${a}", ${n.fallbackName}`;let l="normal";n.black?l="900":n.bold&&(l="bold");const h=n.italic?"italic":"normal";let d=i;i<16?d=16:i>100&&(d=100);this.current.fontSizeScale=i/d;this.ctx.font=`${h} ${l} ${d}px ${o}`}setTextRenderingMode(t,e){this.dependencyTracker?.recordSimpleData("textRenderingMode",t);this.current.textRenderingMode=e}setTextRise(t,e){this.dependencyTracker?.recordSimpleData("textRise",t);this.current.textRise=e}moveText(t,e,i){this.dependencyTracker?.resetIncrementalData("sameLineText").recordIncrementalData("moveText",t);this.current.x=this.current.lineX+=e;this.current.y=this.current.lineY+=i}setLeadingMoveText(t,e,i){this.setLeading(t,-i);this.moveText(t,e,i)}setTextMatrix(t,e){this.dependencyTracker?.resetIncrementalData("sameLineText").recordSimpleData("textMatrix",t);const{current:i}=this;i.textMatrix=e;i.textMatrixScale=Math.hypot(e[0],e[1]);i.x=i.lineX=0;i.y=i.lineY=0}nextLine(t){this.moveText(t,0,this.current.leading);this.dependencyTracker?.recordIncrementalData("moveText",this.dependencyTracker.getSimpleIndex("leading")??t)}#Ws(t,e,i){const s=new Path2D;s.addPath(t,new DOMMatrix(i).invertSelf().multiplySelf(e));return s}paintChar(t,e,i,s,n,r){const a=this.ctx,o=this.current,l=o.font,h=o.textRenderingMode,d=o.fontSize/o.fontSizeScale,c=h&x,u=!!(h&C),p=o.patternFill&&!l.missingFile,g=o.patternStroke&&!l.missingFile;let m;(l.disableFontFace||u||p||g)&&!l.missingFile&&(m=l.getPathGenerator(this.commonObjs,e));if(m&&(l.disableFontFace||p||g)){a.save();a.translate(i,s);a.scale(d,-d);this.dependencyTracker?.recordCharacterBBox(t,a,l);let e;if(c===y||c===v)if(n){e=a.getTransform();a.setTransform(...n);const t=this.#Ws(m,e,n);a.fill(t)}else a.fill(m);if(c===A||c===v)if(r){e||=a.getTransform();a.setTransform(...r);const{a:t,b:i,c:s,d:n}=e,o=Util.inverseTransform(r),l=Util.transform([t,i,s,n,0,0],o);Util.singularValueDecompose2dScale(l,_t);a.lineWidth*=Math.max(_t[0],_t[1])/d;a.stroke(this.#Ws(m,e,r))}else{a.lineWidth/=d;a.stroke(m)}a.restore()}else{if(c===y||c===v){a.fillText(e,i,s);this.dependencyTracker?.recordCharacterBBox(t,a,l,d,i,s,()=>a.measureText(e))}if(c===A||c===v){this.dependencyTracker&&this.dependencyTracker?.recordCharacterBBox(t,a,l,d,i,s,()=>a.measureText(e)).recordDependencies(t,et);a.strokeText(e,i,s)}}if(u){(this.pendingTextPaths||=[]).push({transform:getCurrentTransform(a),x:i,y:s,fontSize:d,path:m});this.dependencyTracker?.recordCharacterBBox(t,a,l,d,i,s)}}get isFontSubpixelAAEnabled(){const t=this.canvasFactory.create(10,10),e=t.context;e.scale(1.5,1);e.fillText("I",0,10);const i=e.getImageData(0,0,10,10).data;this.canvasFactory.destroy(t);let s=!1;for(let t=3;t0&&i[t]<255){s=!0;break}return shadow(this,"isFontSubpixelAAEnabled",s)}showText(t,e){if(this.dependencyTracker){this.dependencyTracker.recordDependencies(t,rt).resetBBox(t);this.current.textRenderingMode&C&&this.dependencyTracker.recordFutureForcedDependency("textClip",t).inheritPendingDependenciesAsFutureForcedDependencies()}const i=this.current,s=i.font;if(s.isType3Font){this.showType3Text(t,e);this.dependencyTracker?.recordShowTextOperation(t);return}const n=i.fontSize;if(0===n){this.dependencyTracker?.recordOperation(t);return}const r=this.ctx,a=i.fontSizeScale,o=i.charSpacing,l=i.wordSpacing,h=i.fontDirection,d=i.textHScale*h,c=e.length,u=s.vertical,p=u?1:-1,g=s.defaultVMetrics,m=n*i.fontMatrix[0],f=i.textRenderingMode===y&&!s.disableFontFace&&!i.patternFill;r.save();i.textMatrix&&r.transform(...i.textMatrix);r.translate(i.x,i.y+i.textRise);h>0?r.scale(d,-1):r.scale(d,1);let b,w;const E=i.textRenderingMode&x,T=E===y||E===v,S=E===A||E===v;let _=i.lineWidth;const P=i.textMatrixScale;0===P||0===_?S&&(_=this.getSinglePixelWidth()):_/=P;if(1!==a){r.scale(a,a);_/=a}r.lineWidth=_;if(T&&i.patternFill){r.save();const e=i.fillColor.getPattern(r,this,getCurrentTransformInverse(r),vt,t);b=getCurrentTransform(r);r.restore();r.fillStyle=e}if(S&&i.patternStroke){r.save();const e=i.strokeColor.getPattern(r,this,getCurrentTransformInverse(r),wt,t);w=getCurrentTransform(r);r.restore();r.strokeStyle=e}if(s.isInvalidPDFjsFont){const s=[];let n=0;for(const t of e){s.push(t.unicode);n+=t.width}const a=s.join("");r.fillText(a,0,0);if(null!==this.dependencyTracker){const e=r.measureText(a);this.dependencyTracker.recordBBox(t,this.ctx,-e.actualBoundingBoxLeft,e.actualBoundingBoxRight,-e.actualBoundingBoxAscent,e.actualBoundingBoxDescent).recordShowTextOperation(t)}i.x+=n*m*d;r.restore();this.compose();return}let D,M=0;for(D=0;D0){C=r.measureText(y);const t=1e3*C.width/n*a;if(EC??r.measureText(y))}else{this.paintChar(t,y,v,x,b,w);if(A){const e=v+n*A.offset.x/a,i=x-n*A.offset.y/a;this.paintChar(t,A.fontChar,e,i,b,w)}}M+=u?E*m-c*h:E*m+c*h;d&&r.restore()}u?i.y-=M:i.x+=M*d;r.restore();this.compose();this.dependencyTracker?.recordShowTextOperation(t)}showType3Text(t,e){const i=this.ctx,n=this.current,r=n.font,a=n.fontSize,o=n.fontDirection,l=r.vertical?1:-1,h=n.charSpacing,d=n.wordSpacing,c=n.textHScale*o,u=n.fontMatrix||s,p=e.length;let g,m,f,b;if(n.textRenderingMode===w||0===a)return;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null;i.save();n.textMatrix&&i.transform(...n.textMatrix);i.translate(n.x,n.y+n.textRise);i.scale(c,o);const y=this.dependencyTracker;this.dependencyTracker=y?new CanvasNestedDependencyTracker(y,t):null;for(g=0;gnew CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack},void 0,void 0,this.dependencyTracker?new CanvasNestedDependencyTracker(this.dependencyTracker,e,!0):null)};i=new TilingPattern(e,this.ctx,s,t)}else i=this._getPattern(t,e[1],e[2]);return i}setStrokeColorN(t,...e){this.dependencyTracker?.recordSimpleData("strokeColor",t);this.current.strokeColor=this.getColorN_Pattern(t,e);this.current.patternStroke=!0}setFillColorN(t,...e){this.dependencyTracker?.recordSimpleData("fillColor",t);const i=this.current.fillColor=this.getColorN_Pattern(t,e);this.current.patternFill=!0;this.current.tilingPatternDims=i instanceof TilingPattern?[0,0,0,0]:null}setStrokeRGBColor(t,e){this.dependencyTracker?.recordSimpleData("strokeColor",t);this.ctx.strokeStyle=this.current.strokeColor=e;this.current.patternStroke=!1}setStrokeTransparent(t){this.dependencyTracker?.recordSimpleData("strokeColor",t);this.ctx.strokeStyle=this.current.strokeColor="transparent";this.current.patternStroke=!1}setFillRGBColor(t,e){this.dependencyTracker?.recordSimpleData("fillColor",t);this.ctx.fillStyle=this.current.fillColor=e;this.current.patternFill=!1;this.current.tilingPatternDims=null}setFillTransparent(t){this.dependencyTracker?.recordSimpleData("fillColor",t);this.ctx.fillStyle=this.current.fillColor="transparent";this.current.patternFill=!1;this.current.tilingPatternDims=null}_getPattern(t,e,i=null){let s;if(this.cachedPatterns.has(e))s=this.cachedPatterns.get(e);else{s=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)}(this.getObject(t,e));this.cachedPatterns.set(e,s)}i&&(s.matrix=i);return s}shadingFill(t,e){if(!this.contentVisible)return;const s=this.ctx;this.save(t);const n=this._getPattern(t,e);s.fillStyle=n.getPattern(s,this,getCurrentTransformInverse(s),xt,t);const r=getCurrentTransformInverse(s);if(r){const{width:t,height:e}=s.canvas,n=i.slice();Util.axialAlignedBoundingBox([0,0,t,e],r,n);const[a,o,l,h]=n;this.ctx.fillRect(a,o,l-a,h-o)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.dependencyTracker?.resetBBox(t).recordFullPageBBox(t).recordDependencies(t,at).recordDependencies(t,it).recordOperation(t);this.compose(this.current.getClippedPathBoundingBox());this.restore(t)}beginInlineImage(){unreachable("Should not call beginInlineImage")}beginImageData(){unreachable("Should not call beginImageData")}paintFormXObjectBegin(t,e,i){if(this.contentVisible){this.save(t);this.baseTransformStack.push(this.baseTransform);e&&this.transform(t,...e);this.baseTransform=getCurrentTransform(this.ctx);if(i){Util.axialAlignedBoundingBox(i,this.baseTransform,this.current.minMax);const[e,s,n,r]=i,a=new Path2D;a.rect(e,s,n-e,r-s);this.ctx.clip(a);this.dependencyTracker?.recordClipBox(t,this.ctx,e,n,s,r);this.endPath(t)}}}paintFormXObjectEnd(t){if(this.contentVisible){this.restore(t);this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t,e){if(!this.contentVisible)return;this.save(t);const{inSMaskMode:s}=this;if(s){this.endSMaskMode();this.current.activeSMask=null}const n=this.ctx;e.isolated||info("TODO: Support non-isolated groups.");e.knockout&&warn("Knockout groups not supported.");if(!e.needsIsolation&&1===n.globalAlpha&&"source-over"===n.globalCompositeOperation&&!s){if(e.bbox){let t=new Path2D;const[i,s,r,a]=e.bbox;t.rect(i,s,r-i,a-s);if(e.matrix){const i=new Path2D;i.addPath(t,new DOMMatrix(e.matrix));t=i}n.clip(t)}this.groupStack.push(null);this.groupLevel++;return}const r=getCurrentTransform(n);e.matrix&&n.transform(...e.matrix);const a=[0,0,n.canvas.width,n.canvas.height];let o;if(e.bbox){o=i.slice();Util.axialAlignedBoundingBox(e.bbox,getCurrentTransform(n),o);o=Util.intersect(o,a)||[0,0,0,0]}else o=a;const l=Math.floor(o[0]),h=Math.floor(o[1]),d=Math.max(Math.ceil(o[2])-l,1),c=Math.max(Math.ceil(o[3])-h,1);this.current.startNewPathAndClipBox([0,0,d,c]);const u=this.canvasFactory.create(d,c);e.smask&&this.smaskGroupCanvases.push(u);const p=u.context;p.translate(-l,-h);p.transform(...r);if(!e.isolated&&!e.smask&&s&&e.needsIsolation){p.save();p.setTransform(1,0,0,1,0,0);p.drawImage(n.canvas,-l,-h);p.restore()}if(e.bbox){let t=new Path2D;const[i,s,n,r]=e.bbox;t.rect(i,s,n-i,r-s);if(e.matrix){const i=new Path2D;i.addPath(t,new DOMMatrix(e.matrix));t=i}p.clip(t)}e.smask&&this.smaskStack.push({canvas:u.canvas,context:p,offsetX:l,offsetY:h,subtype:e.smask.subtype,backdrop:e.smask.backdrop,transferMap:e.smask.transferMap||null});if(!e.smask||this.dependencyTracker){n.setTransform(1,0,0,1,0,0);n.translate(l,h);n.save()}copyCtxState(n,p);this.ctx=p;this.dependencyTracker?.inheritSimpleDataAsFutureForcedDependencies(["fillAlpha","strokeAlpha","globalCompositeOperation"]).pushBaseTransform(n);this.setGState(t,[["BM","source-over"],["ca",1],["CA",1],["TR",null]]);this.groupStack.push(n);this.groupLevel++}endGroup(t,e){if(!this.contentVisible)return;this.groupLevel--;const s=this.ctx,n=this.groupStack.pop();if(null!==n){this.ctx=n;this.ctx.imageSmoothingEnabled=!1;this.dependencyTracker?.popBaseTransform();if(e.smask){this.tempSMask=this.smaskStack.pop();this.restore(t);if(this.dependencyTracker){this.ctx.restore();this.inSMaskMode&&this.ctx.setTransform(this.suspendedCtx.getTransform())}}else{this.ctx.restore();const e=getCurrentTransform(this.ctx);this.restore(t);this.ctx.save();this.ctx.setTransform(...e);const n=i.slice();Util.axialAlignedBoundingBox([0,0,s.canvas.width,s.canvas.height],e,n);this.ctx.drawImage(s.canvas,0,0);this.ctx.restore();this.canvasFactory.destroy({canvas:s.canvas,context:s});this.compose(n)}}else this.restore(t)}beginAnnotation(t,e,i,s,n,r){this.#zs();resetCtxToDefault(this.ctx);this.ctx.save();this.save(t);this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(i){const n=i[2]-i[0],a=i[3]-i[1];if(r&&this.annotationCanvasMap){(s=s.slice())[4]-=i[0];s[5]-=i[1];(i=i.slice())[0]=i[1]=0;i[2]=n;i[3]=a;Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx),_t);const{viewportScale:t}=this,r=Math.ceil(n*this.outputScaleX*t),o=Math.ceil(a*this.outputScaleY*t);this.annotationCanvas=this.canvasFactory.create(r,o);const{canvas:l,context:h}=this.annotationCanvas;this.annotationCanvasMap.set(e,l);this.annotationCanvas.savedCtx=this.ctx;this.ctx=h;this.ctx.save();this.ctx.setTransform(_t[0],0,0,-_t[1],0,a*_t[1]);resetCtxToDefault(this.ctx)}else{resetCtxToDefault(this.ctx);this.endPath(t);const e=new Path2D;e.rect(i[0],i[1],n,a);this.ctx.clip(e)}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.baseTransformStack.push(this.baseTransform);this.transform(t,...s);this.transform(t,...n);this.baseTransform=getCurrentTransform(this.ctx)}endAnnotation(t){if(this.annotationCanvas){this.ctx.restore();this.#Gs();this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}this.baseTransform=this.baseTransformStack.pop()}paintImageMaskXObject(t,e){if(!this.contentVisible)return;const i=e.count;(e=this.getObject(t,e.data,e)).count=i;const s=this.ctx,n=this._createMaskCanvas(t,e),r=n.canvas;s.save();s.setTransform(1,0,0,1,0,0);s.drawImage(r,n.offsetX,n.offsetY);this.dependencyTracker?.resetBBox(t).recordBBox(t,this.ctx,n.offsetX,n.offsetX+r.width,n.offsetY,n.offsetY+r.height).recordOperation(t);s.restore();n.canvasEntry&&this.canvasFactory.destroy(n.canvasEntry);this.compose()}paintImageMaskXObjectRepeat(t,e,i,s=0,n=0,r,a){if(!this.contentVisible)return;e=this.getObject(t,e.data,e);const o=this.ctx;o.save();const l=getCurrentTransform(o);o.transform(i,s,n,r,0,0);const h=this._createMaskCanvas(t,e);o.setTransform(1,0,0,1,h.offsetX-l[4],h.offsetY-l[5]);this.dependencyTracker?.resetBBox(t);for(let e=0,d=a.length;ee?h/e:1;a=l>e?l/e:1}}this._cachedScaleForStroking[0]=r;this._cachedScaleForStroking[1]=a}return this._cachedScaleForStroking}rescaleAndStroke(t,e){const{ctx:i,current:{lineWidth:s}}=this,[n,r]=this.getScaleForStroking();if(n===r){i.lineWidth=(s||1)*n;i.stroke(t);return}const a=i.getLineDash();e&&i.save();i.scale(n,r);St.a=1/n;St.d=1/r;const o=new Path2D;o.addPath(t,St);if(a.length>0){const t=Math.max(n,r);i.setLineDash(a.map(e=>e/t));i.lineDashOffset/=t}i.lineWidth=s||1;i.stroke(o);e&&i.restore()}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}for(const t in I)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[I[t]]=CanvasGraphics.prototype[t]);class BasePDFStream{#Vs=null;#js=null;_fullReader=null;_rangeReaders=new Set;_source=null;constructor(t,e,i){this._source=t;this.#Vs=e;this.#js=i}get _progressiveDataLength(){return this._fullReader?._loaded??0}getFullReader(){assert(!this._fullReader,"BasePDFStream.getFullReader can only be called once.");return this._fullReader=new this.#Vs(this)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new this.#js(this,t,e);this._rangeReaders.add(i);return i}cancelAllRequests(t){this._fullReader?.cancel(t);for(const e of new Set(this._rangeReaders))e.cancel(t)}}class BasePDFStreamReader{onProgress=null;_contentLength=0;_filename=null;_headersCapability=Promise.withResolvers();_isRangeSupported=!1;_isStreamingSupported=!1;_loaded=0;_stream=null;constructor(t){this._stream=t}_callOnProgress(){this.onProgress?.({loaded:this._loaded,total:this._contentLength})}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){unreachable("Abstract method `read` called")}cancel(t){unreachable("Abstract method `cancel` called")}}class BasePDFStreamRangeReader{_stream=null;constructor(t,e,i){this._stream=t}async read(){unreachable("Abstract method `read` called")}cancel(t){unreachable("Abstract method `cancel` called")}}function createHeaders(t,e){const i=new Headers;if(!t||!e||"object"!=typeof e)return i;for(const t in e){const s=e[t];void 0!==s&&i.append(t,s)}return i}function getResponseOrigin(t){return URL.parse(t)?.origin??null}function validateRangeRequestCapabilities({responseHeaders:t,isHttp:e,rangeChunkSize:i,disableRange:s}){const n={contentLength:0,isRangeSupported:!1},r=parseInt(t.get("Content-Length"),10);if(!Number.isInteger(r))return n;n.contentLength=r;if(r<=2*i)return n;if(s||!e)return n;if("bytes"!==t.get("Accept-Ranges"))return n;"identity"===(t.get("Content-Encoding")||"identity")&&(n.isRangeSupported=!0);return n}function extractFilenameFromHeader(t){const e=t.get("Content-Disposition");if(e){let t=function getFilenameFromContentDispositionHeader(t){let e=!0,i=toParamRegExp("filename\\*","i").exec(t);if(i){i=i[1];let t=rfc2616unquote(i);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}i=function rfc2231getparam(t){const e=[];let i;const s=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(i=s.exec(t));){let[,t,s,n]=i;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[s,n]}const n=[];for(let t=0;t{t._responseOrigin=getResponseOrigin(i.url);ensureResponseStatus(i.status,n);this._reader=i.body.getReader();const r=i.headers,{contentLength:a,isRangeSupported:o}=validateRangeRequestCapabilities({responseHeaders:r,isHttp:!0,rangeChunkSize:s,disableRange:e});this._contentLength=a;this._isRangeSupported=o;this._filename=extractFilenameFromHeader(r);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."));this._headersCapability.resolve()}).catch(this._headersCapability.reject)}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this._callOnProgress();return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader extends BasePDFStreamRangeReader{_abortController=new AbortController;_readCapability=Promise.withResolvers();_reader=null;constructor(t,e,i){super(t,e,i);const{url:s,withCredentials:n}=t._source,r=new Headers(t.headers);r.append("Range",`bytes=${e}-${i-1}`);fetchUrl(s,r,n,this._abortController).then(e=>{ensureResponseOrigin(getResponseOrigin(e.url),t._responseOrigin);ensureResponseStatus(e.status,s);this._reader=e.body.getReader();this._readCapability.resolve()}).catch(this._readCapability.reject)}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();return e?{value:t,done:e}:{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}function transport_stream_getArrayBuffer(t){return t instanceof Uint8Array&&t.byteLength===t.buffer.byteLength?t.buffer:new Uint8Array(t).buffer}function endRequests(){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}class PDFDataTransportStream extends BasePDFStream{_progressiveDone=!1;_queuedChunks=[];constructor(t){super(t,PDFDataTransportStreamReader,PDFDataTransportStreamRangeReader);const{pdfDataRangeTransport:e}=t,{initialData:i,progressiveDone:s}=e;if(i?.length>0){const t=transport_stream_getArrayBuffer(i);this._queuedChunks.push(t)}this._progressiveDone=s;e.transportReady(t=>{switch(t.type){case"range":case"progressiveRead":this.#$s(t.begin,t.chunk);break;case"progressiveDone":this._fullReader?.progressiveDone();this._progressiveDone=!0}})}#$s(t,e){const i=transport_stream_getArrayBuffer(e);if(void 0===t)this._fullReader?this._fullReader._enqueue(i):this._queuedChunks.push(i);else{const e=this._rangeReaders.keys().find(e=>e._begin===t);assert(e,"#onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.");e._enqueue(i)}}getFullReader(){const t=super.getFullReader();this._queuedChunks=null;return t}getRangeReader(t,e){const i=super.getRangeReader(t,e);if(i){i.onDone=()=>this._rangeReaders.delete(i);this._source.pdfDataRangeTransport.requestDataRange(t,e)}return i}cancelAllRequests(t){super.cancelAllRequests(t);this._source.pdfDataRangeTransport.abort()}}class PDFDataTransportStreamReader extends BasePDFStreamReader{#Xs=endRequests.bind(this);_done=!1;_queuedChunks=null;_requests=[];constructor(t){super(t);const{pdfDataRangeTransport:e,disableRange:i,disableStream:s}=t._source,{length:n,contentDispositionFilename:r}=e;this._queuedChunks=t._queuedChunks||[];for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._done=t._progressiveDone;this._contentLength=n;this._isStreamingSupported=!s;this._isRangeSupported=!i;isPdfFile(r)&&(this._filename=r);this._headersCapability.resolve();const a=this._loaded;Promise.resolve().then(()=>{a>0&&this._loaded===a&&this._callOnProgress()})}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength;this._callOnProgress()}}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this.#Xs()}progressiveDone(){this._done||=!0;0===this._queuedChunks.length&&this.#Xs()}}class PDFDataTransportStreamRangeReader extends BasePDFStreamRangeReader{#Xs=endRequests.bind(this);onDone=null;_begin=-1;_done=!1;_queuedChunk=null;_requests=[];constructor(t,e,i){super(t,e,i);this._begin=e}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});this.#Xs()}this._done=!0;this.onDone?.()}}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this.#Xs();this.onDone?.()}}class PDFNetworkStream extends BasePDFStream{#qs=new WeakMap;_responseOrigin=null;constructor(t){super(t,PDFNetworkStreamReader,PDFNetworkStreamRangeReader);const{httpHeaders:e,url:i}=t;this.url=i;this.isHttp=/https?:/.test(i.protocol);this.headers=createHeaders(this.isHttp,e)}_request(t){const e=new XMLHttpRequest,i={validateStatus:null,onHeadersReceived:t.onHeadersReceived,onDone:t.onDone,onError:t.onError,onProgress:t.onProgress};this.#qs.set(e,i);e.open("GET",this.url);e.withCredentials=this._source.withCredentials;for(const[t,i]of this.headers)e.setRequestHeader(t,i);if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);i.validateStatus=t=>206===t||200===t}else i.validateStatus=t=>200===t;e.responseType="arraybuffer";assert(t.onError,"Expected `onError` callback to be provided.");e.onerror=()=>t.onError(e.status);e.onreadystatechange=this.#Ks.bind(this,e);e.onprogress=this.#Ys.bind(this,e);e.send(null);return e}#Ys(t,e){const i=this.#qs.get(t);i?.onProgress?.(e)}#Ks(t,e){const i=this.#qs.get(t);if(!i)return;if(t.readyState>=2&&i.onHeadersReceived){i.onHeadersReceived();delete i.onHeadersReceived}if(4!==t.readyState)return;if(!this.#qs.has(t))return;this.#qs.delete(t);if(0===t.status&&this.isHttp){i.onError(t.status);return}const s=t.status||200;if(!i.validateStatus(s)){i.onError(t.status);return}const n=function network_getArrayBuffer(t){return"string"!=typeof t?t:stringToBytes(t).buffer}(t.response);if(206===s){const e=t.getResponseHeader("Content-Range");if(/bytes (\d+)-(\d+)\/(\d+)/.test(e))i.onDone(n);else{warn('Missing or invalid "Content-Range" header.');i.onError(0)}}else n?i.onDone(n):i.onError(t.status)}_abortRequest(t){if(this.#qs.has(t)){this.#qs.delete(t);t.abort()}}getRangeReader(t,e){const i=super.getRangeReader(t,e);i&&(i.onClosed=()=>this._rangeReaders.delete(i));return i}}class PDFNetworkStreamReader extends BasePDFStreamReader{#Xs=endRequests.bind(this);_cachedChunks=[];_done=!1;_requests=[];_storedError=null;constructor(t){super(t);this._fullRequestXhr=t._request({onHeadersReceived:this.#Qs.bind(this),onDone:this.#Js.bind(this),onError:this.#Zs.bind(this),onProgress:this.#Ys.bind(this)})}#Qs(){const t=this._stream,{disableRange:e,rangeChunkSize:i}=t._source,s=this._fullRequestXhr;t._responseOrigin=getResponseOrigin(s.responseURL);const n=s.getAllResponseHeaders(),r=new Headers(n?n.trimStart().replace(/[^\S ]+$/,"").split(/[\r\n]+/).map(t=>{const[e,...i]=t.split(": ");return[e,i.join(": ")]}):[]),{contentLength:a,isRangeSupported:o}=validateRangeRequestCapabilities({responseHeaders:r,isHttp:t.isHttp,rangeChunkSize:i,disableRange:e});this._contentLength=a;this._isRangeSupported=o;this._filename=extractFilenameFromHeader(r);this._isRangeSupported&&t._abortRequest(s);this._headersCapability.resolve()}#Js(t){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._cachedChunks.push(t);this._done=!0;0===this._cachedChunks.length&&this.#Xs()}#Zs(t){this._storedError=createResponseError(t,this._stream.url);this._headersCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}#Ys(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}async read(){await this._headersCapability.promise;if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersCapability.reject(t);this.#Xs();this._stream._abortRequest(this._fullRequestXhr);this._fullRequestXhr=null}}class PDFNetworkStreamRangeReader extends BasePDFStreamRangeReader{#Xs=endRequests.bind(this);onClosed=null;_done=!1;_queuedChunk=null;_requests=[];_storedError=null;constructor(t,e,i){super(t,e,i);this._requestXhr=t._request({begin:e,end:i,onHeadersReceived:this.#Qs.bind(this),onDone:this.#Js.bind(this),onError:this.#Zs.bind(this),onProgress:null})}#Qs(){const t=getResponseOrigin(this._requestXhr?.responseURL);try{ensureResponseOrigin(t,this._stream._responseOrigin)}catch(t){this._storedError=t;this.#Zs(0)}}#Js(t){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunk=t;this._done=!0;this.#Xs();this.onClosed?.()}#Zs(t){this._storedError??=createResponseError(t,this._stream.url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this.#Xs();this._stream._abortRequest(this._requestXhr);this.onClosed?.()}}function getReadableStream(t,e=null){const i=process.getBuiltinModule("fs"),{Readable:s}=process.getBuiltinModule("stream"),n=i.createReadStream(t,e);return s.toWeb(n)}class PDFNodeStream extends BasePDFStream{constructor(t){super(t,PDFNodeStreamReader,PDFNodeStreamRangeReader);const{url:e}=t;assert("file:"===e.protocol,"PDFNodeStream only supports file:// URLs.")}}class PDFNodeStreamReader extends BasePDFStreamReader{_reader=null;constructor(t){super(t);const{disableRange:e,disableStream:i,rangeChunkSize:s,url:n}=t._source;this._isStreamingSupported=!i;process.getBuiltinModule("fs/promises").lstat(n).then(t=>{const i=getReadableStream(n);this._reader=i.getReader();const{size:r}=t;this._contentLength=r;this._isRangeSupported=!e&&r>2*s;!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."));this._headersCapability.resolve()}).catch(t=>{"ENOENT"===t.code&&(t=createResponseError(0,n));this._headersCapability.reject(t)})}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this._callOnProgress();return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t)}}class PDFNodeStreamRangeReader extends BasePDFStreamRangeReader{_readCapability=Promise.withResolvers();_reader=null;constructor(t,e,i){super(t,e,i);const{url:s}=t._source;try{const t=getReadableStream(s,{start:e,end:i-1});this._reader=t.getReader();this._readCapability.resolve()}catch(t){this._readCapability.reject(t)}}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();return e?{value:t,done:e}:{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t)}}class GlobalWorkerOptions{static#tn=null;static#en="";static get workerPort(){return this.#tn}static set workerPort(t){if(!("undefined"!=typeof Worker&&t instanceof Worker)&&null!==t)throw new Error("Invalid `workerPort` type.");this.#tn=t}static get workerSrc(){return this.#en}static set workerSrc(t){if("string"!=typeof t)throw new Error("Invalid `workerSrc` type.");this.#en=t}}class Metadata{#in;#sn;constructor({parsedData:t,rawData:e}){this.#in=t;this.#sn=e}getRaw(){return this.#sn}get(t){return this.#in.get(t)??null}[Symbol.iterator](){return this.#in.entries()}}const It=Symbol("INTERNAL");class OptionalContentGroup{#nn=!1;#rn=!1;#an=!1;#on=!0;constructor(t,{name:e,intent:i,usage:s,rbGroups:n}){this.#nn=!!(t&a);this.#rn=!!(t&o);this.name=e;this.intent=i;this.usage=s;this.rbGroups=n}get visible(){if(this.#an)return this.#on;if(!this.#on)return!1;const{print:t,view:e}=this.usage;return this.#nn?"OFF"!==e?.viewState:!this.#rn||"OFF"!==t?.printState}_setVisible(t,e,i=!1){t!==It&&unreachable("Internal method `_setVisible` called.");this.#an=i;this.#on=e}}class OptionalContentConfig{#ln=null;#hn=new Map;#dn=null;#cn=null;constructor(t,e=a){this.renderingIntent=e;this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#cn=t.order;for(const i of t.groups)this.#hn.set(i.id,new OptionalContentGroup(e,i));if("OFF"===t.baseState)for(const t of this.#hn.values())t._setVisible(It,!1);for(const e of t.on)this.#hn.get(e)._setVisible(It,!0);for(const e of t.off)this.#hn.get(e)._setVisible(It,!1);this.#dn=this.getHash()}}#un(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let s=1;st===e+1)&&(this.#pn=null)}deletePages(t){this.#yn();const e=this.#pn,i=this.#An();this.#bn={pageNumberToId:e.slice(),pagesNumber:this.#mn,prevPageNumbers:this.#gn.slice()};const s=this.#mn-t.length;this.#mn=s;const n=this.#pn=new Uint32Array(s);this.#gn=new Int32Array(s);let r=0,a=0;for(const i of t){const t=i-1;if(t!==r){n.set(e.subarray(r,t),a);a+=t-r}r=t+1}rthis.#pn[t-1])}}cancelCopy(){this.#fn=null}pastePages(t){this.#yn();const e=this.#pn,i=this.#An(),{pageNumbers:s,pageIds:n}=this.#fn,r=this.#mn+s.length;this.#mn=r;const a=this.#pn=new Uint32Array(r);this.#gn=new Int32Array(r);a.set(e.subarray(0,t),0);a.set(n,t);a.set(e.subarray(t),t+s.length);this.#vn(i,null,t,s);this.#fn=null}#vn(t,e=null,i=-1,s=null){const n=this.#gn,r=this.#pn,a=i+(s?.length??0),o=new Map;for(let l=0,h=this.#mn;l=i&&lt[0]-e[0]);for(let i=0,s=t.length;it-e);const e=new Map;for(let i=0,s=t.length;i({...Promise.withResolvers(),data:Ft});class PDFObjects{#wn=new Map;get(t,e=null){if(e){const i=this.#wn.getOrInsertComputed(t,dataObj);i.promise.then(()=>e(i.data));return null}const i=this.#wn.get(t);if(!i||i.data===Ft)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#wn.get(t);return!!e&&e.data!==Ft}delete(t){const e=this.#wn.get(t);if(!e||e.data===Ft)return!1;this.#wn.delete(t);return!0}resolve(t,e=null){const i=this.#wn.getOrInsertComputed(t,dataObj);if(i.data!==Ft)throw new Error(`Object already resolved ${t}.`);i.data=e;i.resolve()}clear(){for(const{data:t}of this.#wn.values())t?.bitmap?.close();this.#wn.clear()}*[Symbol.iterator](){for(const[t,{data:e}]of this.#wn)e!==Ft&&(yield[t,e])}}class TextLayer{#xn=Promise.withResolvers();#_t=null;#Cn=!1;#En=!!globalThis.FontInspector?.enabled;#Tn=null;#Sn=null;#_n=null;#Pn=0;#Dn=0;#Mn=null;#kn=null;#In=0;#Fn=0;#Bn=Object.create(null);#Rn=[];#On=null;#Ln=[];#Nn=new WeakMap;#Un=null;static#Hn=new Map;static#zn=new Map;static#Gn=new WeakMap;static#Wn=null;static#Vn=new Set;constructor({textContentSource:t,images:e,container:i,viewport:s}){if(t instanceof ReadableStream)this.#On=t;else{if("object"!=typeof t)throw new Error('No "textContentSource" parameter specified.');this.#On=new ReadableStream({start(e){e.enqueue(t);e.close()}})}this.#_t=this.#kn=i;this.#Tn=e;this.#Fn=s.scale*OutputScale.pixelRatio;this.#In=s.rotation;this.#_n={div:null,properties:null,ctx:null};const{pageWidth:n,pageHeight:r,pageX:a,pageY:o}=s.rawDims;this.#Un=[1,0,0,-1,-a,o+r];this.#Dn=n;this.#Pn=r;TextLayer.#jn();i.style.setProperty("--min-font-size",TextLayer.#Wn);setLayerDimensions(i,s);this.#xn.promise.finally(()=>{TextLayer.#Vn.delete(this);this.#_n=null;this.#Bn=null}).catch(()=>{})}static get fontFamilyMap(){const{isWindows:t,isFirefox:e}=FeatureTest.platform;return shadow(this,"fontFamilyMap",new Map([["sans-serif",(t&&e?"Calibri, ":"")+"sans-serif"],["monospace",(t&&e?"Lucida Console, ":"")+"monospace"]]))}render(){this.#Tn&&this.#_t.append(this.#Tn.render());const pump=()=>{this.#Mn.read().then(({value:t,done:e})=>{if(e)this.#xn.resolve();else{this.#Sn??=t.lang;Object.assign(this.#Bn,t.styles);this.#$n(t.items);pump()}},this.#xn.reject)};this.#Mn=this.#On.getReader();TextLayer.#Vn.add(this);pump();return this.#xn.promise}update({viewport:t,onBefore:e=null}){const i=t.scale*OutputScale.pixelRatio,s=t.rotation;if(s!==this.#In){e?.();this.#In=s;setLayerDimensions(this.#kn,{rotation:s})}if(i!==this.#Fn){e?.();this.#Fn=i;const t={div:null,properties:null,ctx:TextLayer.#Xn(this.#Sn)};for(const e of this.#Ln){t.properties=this.#Nn.get(e);t.div=e;this.#qn(t)}}}cancel(){const t=new AbortException("TextLayer task cancelled.");this.#Mn?.cancel(t).catch(()=>{});this.#Mn=null;this.#xn.reject(t)}get textDivs(){return this.#Ln}get textContentItemsStr(){return this.#Rn}#$n(t){if(this.#Cn)return;this.#_n.ctx??=TextLayer.#Xn(this.#Sn);const e=this.#Ln,i=this.#Rn;for(const s of t){if(e.length>1e5){warn("Ignoring additional textDivs for performance reasons.");this.#Cn=!0;return}if(void 0!==s.str){i.push(s.str);this.#Kn(s)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this.#_t;this.#_t=document.createElement("span");this.#_t.classList.add("markedContent");s.id&&this.#_t.setAttribute("id",`${s.id}`);"Artifact"===s.tag&&(this.#_t.ariaHidden=!0);t.append(this.#_t)}else"endMarkedContent"===s.type&&(this.#_t=this.#_t.parentNode)}}#Kn(t){const e=document.createElement("span"),i={angle:0,canvasWidth:0,hasText:""!==t.str,hasEOL:t.hasEOL,fontSize:0};this.#Ln.push(e);const s=Util.transform(this.#Un,t.transform);let n=Math.atan2(s[1],s[0]);const r=this.#Bn[t.fontName];r.vertical&&(n+=Math.PI/2);let a=this.#En&&r.fontSubstitution||r.fontFamily;a=TextLayer.fontFamilyMap.get(a)||a;const o=Math.hypot(s[2],s[3]),l=o*TextLayer.#Yn(a,r,this.#Sn);let h,d;if(0===n){h=s[4];d=s[5]-l}else{h=s[4]+l*Math.sin(n);d=s[5]-l*Math.cos(n)}const c=e.style;c.left=`${(100*h/this.#Dn).toFixed(2)}%`;c.top=`${(100*d/this.#Pn).toFixed(2)}%`;c.setProperty("--font-height",`${o.toFixed(2)}px`);c.fontFamily=a;i.fontSize=o;e.setAttribute("role","presentation");e.textContent=t.str;e.dir=t.dir;this.#En&&(e.dataset.fontName=r.fontSubstitutionLoadedName||t.fontName);0!==n&&(i.angle=n*(180/Math.PI));let u=!1;if(t.str.length>1)u=!0;else if(" "!==t.str&&t.transform[0]!==t.transform[3]){const e=Math.abs(t.transform[0]),i=Math.abs(t.transform[3]);e!==i&&Math.max(e,i)/Math.min(e,i)>1.5&&(u=!0)}u&&(i.canvasWidth=r.vertical?t.height:t.width);this.#Nn.set(e,i);this.#_n.div=e;this.#_n.properties=i;this.#qn(this.#_n);i.hasText&&this.#_t.append(e);if(i.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this.#_t.append(t)}}#qn(t){const{div:e,properties:i,ctx:s}=t,{style:n}=e;if(0!==i.canvasWidth&&i.hasText){const{fontFamily:t}=n,{canvasWidth:r,fontSize:a}=i;TextLayer.#Qn(s,a*this.#Fn,t);const{width:o}=s.measureText(e.textContent);o>0&&n.setProperty("--scale-x",r*this.#Fn/o)}0!==i.angle&&n.setProperty("--rotate",`${i.angle}deg`)}static cleanup(){if(!(this.#Vn.size>0)){this.#Hn.clear();for(const{canvas:t}of this.#zn.values())t.remove();this.#zn.clear()}}static#Xn(t=null){let e=this.#zn.get(t||="");if(!e){const i=document.createElement("canvas");i.className="hiddenCanvasElement";i.lang=t;document.body.append(i);e=i.getContext("2d",{alpha:!1,willReadFrequently:!0});this.#zn.set(t,e);this.#Gn.set(e,{size:0,family:""})}return e}static#Qn(t,e,i){const s=this.#Gn.get(t);if(e!==s.size||i!==s.family){t.font=`${e}px ${i}`;s.size=e;s.family=i}}static#jn(){if(null!==this.#Wn)return;const t=document.createElement("div");t.style.opacity=0;t.style.lineHeight=1;t.style.fontSize="1px";t.style.position="absolute";t.textContent="X";document.body.append(t);this.#Wn=t.getBoundingClientRect().height;t.remove()}static#Yn(t,e,i){const s=this.#Hn.get(t);if(s)return s;const n=this.#Xn(i);n.canvas.width=n.canvas.height=30;this.#Qn(n,30,t);const r=n.measureText(""),a=r.fontBoundingBoxAscent,o=Math.abs(r.fontBoundingBoxDescent);n.canvas.width=n.canvas.height=0;let l=.8;if(a)l=a/(a+o);else{FeatureTest.platform.isFirefox&&warn("Enable the `dom.textMetrics.fontBoundingBox.enabled` preference in `about:config` to improve TextLayer rendering.");e.ascent?l=e.ascent:e.descent&&(l=1+e.descent)}this.#Hn.set(t,l);return l}}function getDocument(e={}){"string"==typeof e||e instanceof URL?e={url:e}:(e instanceof ArrayBuffer||ArrayBuffer.isView(e))&&(e={data:e});const i=new PDFDocumentLoadingTask,{docId:s}=i,n=e.url?function getUrlProp(e){if(e instanceof URL)return e;if("string"==typeof e){if(t){if(/^[a-z][a-z0-9\-+.]+:/i.test(e))return new URL(e);const t=process.getBuiltinModule("url");return new URL(t.pathToFileURL(e))}const i=URL.parse(e,window.location);if(i)return i}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}(e.url):null,r=e.data?function getDataProp(e){if(t&&"undefined"!=typeof Buffer&&e instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength)return e;if("string"==typeof e)return stringToBytes(e);if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)||"object"==typeof e&&!isNaN(e?.length))return new Uint8Array(e);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}(e.data):null,a=e.httpHeaders||null,o=!0===e.withCredentials,l=e.password??null,h=e.range instanceof PDFDataRangeTransport?e.range:null,d=Number.isInteger(e.rangeChunkSize)&&e.rangeChunkSize>0?e.rangeChunkSize:65536;let c=e.worker instanceof PDFWorker?e.worker:null;const u=e.verbosity,p="string"!=typeof e.docBaseUrl||isDataScheme(e.docBaseUrl)?null:e.docBaseUrl,g=getFactoryUrlProp(e.cMapUrl),m=!1!==e.cMapPacked,f=getFactoryUrlProp(e.iccUrl),b=getFactoryUrlProp(e.standardFontDataUrl),y=getFactoryUrlProp(e.wasmUrl),A=!0!==e.stopAtErrors,v=Number.isInteger(e.maxImageSize)&&e.maxImageSize>-1?e.maxImageSize:-1,w="boolean"==typeof e.isOffscreenCanvasSupported?e.isOffscreenCanvasSupported:!t,x="boolean"==typeof e.isImageDecoderSupported?e.isImageDecoderSupported:!t&&(FeatureTest.platform.isFirefox||!globalThis.chrome),C=Number.isInteger(e.canvasMaxAreaInBytes)?e.canvasMaxAreaInBytes:-1,E="boolean"==typeof e.disableFontFace?e.disableFontFace:t,T=!0===e.fontExtraProperties,S=!0===e.enableXfa,_=e.ownerDocument||globalThis.document,P=!0===e.disableRange,D=!0===e.disableStream,M=!0===e.disableAutoFetch,k=!0===e.pdfBug,I=e.CanvasFactory||(t?NodeCanvasFactory:DOMCanvasFactory),F=e.FilterFactory||(t?NodeFilterFactory:DOMFilterFactory),B=e.BinaryDataFactory||(t?NodeBinaryDataFactory:DOMBinaryDataFactory),R=!0===e.enableHWA,O=!0===e.enableWebGPU?function initGPU(){return At.init()}():Promise.resolve(!1),L=!1!==e.useWasm,N=e.pagesMapper||new PagesMapper,U="boolean"==typeof e.useSystemFonts?e.useSystemFonts:!t&&!E,H="boolean"==typeof e.useWorkerFetch?e.useWorkerFetch:!!(B===DOMBinaryDataFactory&&g&&m&&b&&y&&isValidFetchUrl(g,document.baseURI)&&isValidFetchUrl(b,document.baseURI)&&isValidFetchUrl(y,document.baseURI));setVerbosityLevel(u);const z={canvasFactory:new I({ownerDocument:_,enableHWA:R}),filterFactory:new F({docId:s,ownerDocument:_}),binaryDataFactory:H?null:new B({cMapUrl:g,standardFontDataUrl:b,wasmUrl:y})};if(!c){c=PDFWorker.create({verbosity:u,port:GlobalWorkerOptions.workerPort});i._worker=c}const G={docId:s,apiVersion:"5.7.284",data:r,password:l,disableAutoFetch:M,rangeChunkSize:d,docBaseUrl:p,enableXfa:S,evaluatorOptions:{maxImageSize:v,disableFontFace:E,ignoreErrors:A,isOffscreenCanvasSupported:w,isImageDecoderSupported:x,canvasMaxAreaInBytes:C,fontExtraProperties:T,useSystemFonts:U,useWasm:L,useWorkerFetch:H,cMapUrl:g,cMapPacked:m,iccUrl:f,standardFontDataUrl:b,wasmUrl:y,hasGPU:!1}},W={ownerDocument:_,pdfBug:k,styleElement:null,enableHWA:R,loadingParams:{disableAutoFetch:M,enableXfa:S}};Promise.all([c.promise,O]).then(function([,e]){if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");G.evaluatorOptions.hasGPU=e;const l=c.messageHandler.sendWithPromise("GetDocRequest",G,r?[r.buffer]:null);let u;if(r);else if(h)u=new PDFDataTransportStream({pdfDataRangeTransport:h,disableRange:P,disableStream:D});else{if(!n)throw new Error("getDocument - expected either `data`, `range`, or `url` parameter.");{const e=function getNetworkStream(e){return isValidFetchUrl(e)?PDFFetchStream:t?PDFNodeStream:PDFNetworkStream}(n);u=new e({url:n,httpHeaders:a,withCredentials:o,rangeChunkSize:d,disableRange:P,disableStream:D})}}return l.then(t=>{if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=new MessageHandler(s,t,c.port),n=new WorkerTransport(e,i,u,W,z,N);i._transport=n;e.send("Ready",null)})}).catch(i._capability.reject);return i}class PDFDocumentLoadingTask{static#xs=0;_capability=Promise.withResolvers();_transport=null;_worker=null;docId="d"+PDFDocumentLoadingTask.#xs++;destroyed=!1;onPassword=null;onProgress=null;get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;this._worker?.destroy();this._worker=null}async getData(){return this._transport.getData()}}class PDFDataRangeTransport{#xn=Promise.withResolvers();#Jn=null;constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s;Object.defineProperty(this,"onDataProgress",{value:()=>{!function deprecated(t){console.log("Deprecated API usage: "+t)}("`PDFDataRangeTransport.prototype.onDataProgress` - method was removed, since loading progress is now reported automatically through the `PDFDataTransportStream` class (and related code).")}})}onDataRange(t,e){this.#Jn({type:"range",begin:t,chunk:e})}onDataProgressiveRead(t){this.#xn.promise.then(()=>{this.#Jn({type:"progressiveRead",chunk:t})})}onDataProgressiveDone(){this.#xn.promise.then(()=>{this.#Jn({type:"progressiveDone"})})}transportReady(t){this.#Jn=t;this.#xn.resolve()}requestDataRange(t,e){unreachable("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get pagesMapper(){return this._transport.pagesMapper}get annotationStorage(){return this._transport.annotationStorage}get canvasFactory(){return this._transport.canvasFactory}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getAnnotationsByType(t,e){return this._transport.getAnnotationsByType(t,e)}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getOptionalContentConfig(e)}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}extractPages(t){return this._transport.extractPages(t)}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}getRawData(t){return this._transport.getRawData(t)}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}cachedPageNumber(t){return this._transport.cachedPageNumber(t)}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}class PDFPageProxy{#Zn=!1;#tr=null;constructor(t,e,i,s,n=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=n?new StatTimer:null;this._pdfBug=n;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._intentStates=new Map;this.destroyed=!1;this.recordedBBoxes=null;this.#tr=s;this.imageCoordinates=null}clone(t){const e=new PDFPageProxy(t,this._pageInfo,this._transport,this.#tr,this._pdfBug);e.clonedFromIndex=this.clonedFromIndex??this._pageIndex;this._transport.updatePage(e);return e}get pageNumber(){return this._pageIndex+1}set pageNumber(t){this._pageIndex=t-1;this._transport.updatePage(this)}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.view,userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,canvas:e=t.canvas,viewport:i,intent:s="display",annotationMode:n=p.ENABLE,transform:r=null,background:a=null,optionalContentConfigPromise:l=null,annotationCanvasMap:h=null,pageColors:d=null,printAnnotationStorage:c=null,isEditing:u=!1,recordImages:g=!1,recordOperations:m=!1,operationsFilter:f=null}){this._stats?.time("Overall");const b=this._transport.getRenderingIntent(s,n,c,u),{renderingIntent:y,cacheKey:A}=b;this.#Zn=!1;l||=this._transport.getOptionalContentConfig(y);const v=this._intentStates.getOrInsertComputed(A,makeObj);if(v.streamReaderCancelTimeout){clearTimeout(v.streamReaderCancelTimeout);v.streamReaderCancelTimeout=null}const w=!!(y&o);if(!v.displayReadyCapability){v.displayReadyCapability=Promise.withResolvers();v.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(b)}const x=!(!this._pdfBug||!globalThis.StepperManager?.enabled),C=!!e&&!this.recordedBBoxes&&(m||x),E=!!e&&!this.imageCoordinates&&g,complete=t=>{v.renderTasks.delete(_);if(C){const t=_.gfx?.dependencyTracker.take();if(t){_.stepper?.setOperatorBBoxes(t,_.gfx.dependencyTracker.takeDebugMetadata());m&&(this.recordedBBoxes=t)}}E&&!t&&(this.imageCoordinates=_.gfx?.imagesTracker.take());w&&(this.#Zn=!0);this.#er();if(t){_.capability.reject(t);this._abortOperatorList({intentState:v,reason:t instanceof Error?t:new Error(t)})}else _.capability.resolve();if(this._stats){this._stats.timeEnd("Rendering");this._stats.timeEnd("Overall");globalThis.Stats?.enabled&&globalThis.Stats.add(this.pageNumber,this._stats)}};let T=null,S=null;(C||E)&&(S=new CanvasBBoxTracker(e,v.operatorList.length));C&&(T=new CanvasDependencyTracker(S,x));const _=new InternalRenderTask({callback:complete,params:{canvas:e,canvasContext:t,dependencyTracker:T??S,imagesTracker:E?new CanvasImagesTracker(e):null,viewport:i,transform:r,background:a},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:h,operatorList:v.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!w,pdfBug:this._pdfBug,pageColors:d,enableHWA:this._transport.enableHWA,operationsFilter:f});(v.renderTasks||=new Set).add(_);const P=_.task;Promise.all([v.displayReadyCapability.promise,l]).then(([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");if(!(e.renderingIntent&y))throw new Error("Must use the same `intent`-argument when calling the `PDFPageProxy.render` and `PDFDocumentProxy.getOptionalContentConfig` methods.");_.initializeGraphics({transparency:t,optionalContentConfig:e});_.operatorListChanged()}}).catch(complete);return P}getOperatorList({intent:t="display",annotationMode:e=p.ENABLE,printAnnotationStorage:i=null,isEditing:s=!1}={}){const n=this._transport.getRenderingIntent(t,e,i,s,!0),r=this._intentStates.getOrInsertComputed(n.cacheKey,makeObj);let a;if(!r.opListReadCapability){a=Object.create(null);a.operatorListChanged=function operatorListChanged(){if(r.operatorList.lastChunk){r.opListReadCapability.resolve(r.operatorList);r.renderTasks.delete(a)}};r.opListReadCapability=Promise.withResolvers();(r.renderTasks||=new Set).add(a);r.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return r.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageId:this.#tr.getPageId(this._pageIndex+1)-1,pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}async getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then(t=>XfaText.textContent(t));const e=this.streamTextContent(t),i={items:[],styles:Object.create(null),lang:null};for await(const t of e){i.lang??=t.lang;Object.assign(i.styles,t.styles);i.items.push(...t.items)}return i}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#Zn=!1;return Promise.all(t)}cleanup(t=!1){this.#Zn=!0;const e=this.#er();t&&e&&(this._stats&&=new StatTimer);return e}#er(){if(!this.#Zn||this.destroyed)return!1;for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#Zn=!1;return!0}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i{a.read().then(({value:t,done:e})=>{if(e)o.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,o);pump()}},t=>{o.streamReader=null;if(!this._transport.destroyed){if(o.operatorList){o.operatorList.lastChunk=!0;for(const t of o.renderTasks)t.operatorListChanged();this.#er()}if(o.displayReadyCapability)o.displayReadyCapability.reject(t);else{if(!o.opListReadCapability)throw t;o.opListReadCapability.reject(t)}}})};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof RenderingCancelledException){let i=100;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout(()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})},i);return}}t.streamReader.cancel(new AbortException(e.message)).catch(()=>{});t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}class PDFWorker{#xn=Promise.withResolvers();#ir=null;#tn=null;#sr=null;static#nr=0;static#rr=!1;static#ar=new WeakMap;static{if(t){this.#rr=!0;GlobalWorkerOptions.workerSrc||="./pdf.worker.mjs"}this._isSameOrigin=(t,e)=>{const i=URL.parse(t);if(!i?.origin||"null"===i.origin)return!1;const s=new URL(e,i);return i.origin===s.origin};this._createCDNWrapper=t=>{const e=`await import("${t}");`;return URL.createObjectURL(new Blob([e],{type:"text/javascript"}))}}constructor({name:t=null,port:e=null,verbosity:i=getVerbosityLevel()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;if(e){if(PDFWorker.#ar.has(e))throw new Error("Cannot use more than one PDFWorker per port.");PDFWorker.#ar.set(e,this);this.#or(e)}else this.#lr()}get promise(){return this.#xn.promise}#hr(){this.#xn.resolve();this.#ir.send("configure",{verbosity:this.verbosity})}get port(){return this.#tn}get messageHandler(){return this.#ir}#or(t){this.#tn=t;this.#ir=new MessageHandler("main","worker",t);this.#ir.on("ready",()=>{});this.#hr()}#lr(){if(PDFWorker.#rr||PDFWorker.#dr){this.#cr();return}let{workerSrc:t}=PDFWorker;try{PDFWorker._isSameOrigin(window.location,t)||(t=PDFWorker._createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t,{type:"module"}),i=new MessageHandler("main","worker",e),terminateEarly=()=>{s.abort();i.destroy();e.terminate();this.destroyed?this.#xn.reject(new Error("Worker was destroyed")):this.#cr()},s=new AbortController;e.addEventListener("error",()=>{this.#sr||terminateEarly()},{signal:s.signal});i.on("test",t=>{s.abort();if(!this.destroyed&&t){this.#ir=i;this.#tn=e;this.#sr=e;this.#hr()}else terminateEarly()});i.on("ready",t=>{s.abort();if(this.destroyed)terminateEarly();else try{sendTest()}catch{this.#cr()}});const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{info("The worker has been disabled.")}this.#cr()}#cr(){if(!PDFWorker.#rr){warn("Setting up fake worker.");PDFWorker.#rr=!0}PDFWorker._setupFakeWorkerGlobal.then(t=>{if(this.destroyed){this.#xn.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this.#tn=e;const i="fake"+PDFWorker.#nr++,s=new MessageHandler(i+"_worker",i,e);t.setup(s,e);this.#ir=new MessageHandler(i,i+"_worker",e);this.#hr()}).catch(t=>{this.#xn.reject(new Error(`Setting up fake worker failed: "${t.message}".`))})}destroy(){this.destroyed=!0;this.#sr?.terminate();this.#sr=null;PDFWorker.#ar.delete(this.#tn);this.#tn=null;this.#ir?.destroy();this.#ir=null}static create(t){const e=this.#ar.get(t?.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.create - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(GlobalWorkerOptions.workerSrc)return GlobalWorkerOptions.workerSrc;throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get#dr(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){return shadow(this,"_setupFakeWorkerGlobal",(async()=>{if(this.#dr)return this.#dr;return(await import( +/*webpackIgnore: true*/ +/*@vite-ignore*/ +this.workerSrc)).WorkerMessageHandler})())}}class WorkerTransport{downloadInfoCapability=Promise.withResolvers();#ur=null;#pr=new Map;#gr=null;#mr=new Map;#fr=new Map;#br=new Map;#yr=null;constructor(t,e,i,s,n,r){this.messageHandler=t;this.loadingTask=e;this.#gr=i;this.commonObjs=new PDFObjects;this.fontLoader=new FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this.enableHWA=s.enableHWA;this.loadingParams=s.loadingParams;this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.binaryDataFactory=n.binaryDataFactory;this.pagesMapper=r;this.destroyed=!1;this.destroyCapability=null;this.setupMessageHandler()}updatePage(t){const{_pageIndex:e}=t;this.#mr.set(e,t);this.#fr.set(e,Promise.resolve(t))}#Ar(t,e=null){return this.#pr.getOrInsertComputed(t,()=>this.messageHandler.sendWithPromise(t,e))}#Ys({loaded:t,total:e}){this.loadingTask.onProgress?.({loaded:t,total:e,percent:e?MathClamp(Math.round(t/e*100),0,100):NaN})}get annotationStorage(){return shadow(this,"annotationStorage",new AnnotationStorage)}getRenderingIntent(t,e=p.ENABLE,i=null,s=!1,n=!1){let g=a,m=Y;switch(t){case"any":g=r;break;case"display":break;case"print":g=o;break;default:warn(`getRenderingIntent - invalid intent: ${t}`)}const f=g&o&&i instanceof PrintAnnotationStorage?i:this.annotationStorage;switch(e){case p.DISABLE:g+=d;break;case p.ENABLE:break;case p.ENABLE_FORMS:g+=l;break;case p.ENABLE_STORAGE:g+=h;m=f.serializable;break;default:warn(`getRenderingIntent - invalid annotationMode: ${e}`)}s&&(g+=c);n&&(g+=u);const{ids:b,hash:y}=f.modifiedIds;return{renderingIntent:g,cacheKey:[g,m.hash,y].join("_"),annotationStorageSerializable:m,modifiedIds:b}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=Promise.withResolvers();this.#yr?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#mr.values())t.push(e._destroy());this.#mr.clear();this.#fr.clear();this.#br.clear();Object.hasOwn(this,"annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then(()=>{this.commonObjs.clear();this.fontLoader.clear();this.#pr.clear();this.filterFactory.destroy();TextLayer.cleanup();this.#gr?.cancelAllRequests(new AbortException("Worker was terminated."));this.messageHandler?.destroy();this.messageHandler=null;this.destroyCapability.resolve()},this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",(t,e)=>{assert(this.#gr,"GetReader - no `BasePDFStream` instance available.");this.#ur=this.#gr.getFullReader();this.#ur.onProgress=t=>this.#Ys(t);e.onPull=()=>{this.#ur.read().then(function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}}).catch(t=>{e.error(t)})};e.onCancel=t=>{this.#ur.cancel(t);e.ready.catch(t=>{if(!this.destroyed)throw t})}});t.on("ReaderHeadersReady",async t=>{await this.#ur.headersReady;const{isStreamingSupported:e,isRangeSupported:i,contentLength:s}=this.#ur;e&&i&&(this.#ur.onProgress=null);return{isStreamingSupported:e,isRangeSupported:i,contentLength:s}});t.on("GetRangeReader",(t,e)=>{assert(this.#gr,"GetRangeReader - no `BasePDFStream` instance available.");const i=this.#gr.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then(function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}}).catch(t=>{e.error(t)})};e.onCancel=t=>{i.cancel(t);e.ready.catch(t=>{if(!this.destroyed)throw t})}}else e.close()});t.on("GetDoc",({pdfInfo:t})=>{this.pagesMapper.pagesNumber=t.numPages;this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))});t.on("DocException",t=>{e._capability.reject(wrapReason(t))});t.on("PasswordRequest",t=>{this.#yr=Promise.withResolvers();try{if(!e.onPassword)throw wrapReason(t);const updatePassword=t=>{t instanceof Error?this.#yr.reject(t):this.#yr.resolve({password:t})};e.onPassword(updatePassword,t.code)}catch(t){this.#yr.reject(t)}return this.#yr.promise});t.on("DataLoaded",t=>{this.#Ys({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)});t.on("StartRenderPage",t=>{if(this.destroyed)return;this.#mr.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)});t.on("commonobj",([e,i,s])=>{if(this.destroyed)return null;if(this.commonObjs.has(e))return null;switch(i){case"Font":if("error"in s){const t=s.error;warn(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const n=new FontInfo(s),r=this._params.pdfBug&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,a=new FontFaceObject(n,r,s.charProcOperatorList,s.extra);this.fontLoader.bind(a).catch(()=>t.sendWithPromise("FontFallback",{id:e})).finally(()=>{a.fontExtraProperties||a.clearData();this.commonObjs.resolve(e,a)});break;case"CopyLocalImage":const{imageRef:o}=s;assert(o,"The imageRef must be defined.");for(const t of this.#mr.values())for(const[,i]of t.objs){if(i?.ref!==o)continue;if(!i.dataLen)return null;const t=structuredClone(i);this.commonObjs.resolve(e,t);return i.dataLen}break;case"FontPath":this.commonObjs.resolve(e,new FontPathInfo(s));break;case"Image":this.commonObjs.resolve(e,s);break;case"Pattern":const l=new PatternInfo(s);this.commonObjs.resolve(e,l.getIR());break;default:throw new Error(`Got unknown common object type ${i}`)}return null});t.on("obj",([t,e,i,s])=>{if(this.destroyed)return;const n=this.#mr.get(e);if(!n.objs.has(t))if(0!==n._intentStates.size)switch(i){case"Image":case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}else s?.bitmap?.close()});t.on("DocProgress",t=>{this.destroyed||this.#Ys(t)});t.on("FetchBinaryData",async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");if(!this.binaryDataFactory)throw new Error("`BinaryDataFactory` not initialized, see the `useWorkerFetch` parameter.");return this.binaryDataFactory.fetch(t)})}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&warn("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfer:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this.#ur?.filename??null},e).finally(()=>{this.annotationStorage.resetModified()})}extractPages(t){const e={pageInfos:t};let i;if(this.annotationStorage.size>0){const{map:t,transfer:s}=this.annotationStorage.serializable;e.annotationStorage=t;i=s}return this.messageHandler.sendWithPromise("ExtractPages",e,i).finally(()=>{this.annotationStorage.resetModified()})}getPage(t){if(!Number.isInteger(t)||t<=0||t>this.pagesMapper.pagesNumber)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.pagesMapper.getPageId(t)-1,s=this.#fr.get(e);if(s)return s;const n=this.messageHandler.sendWithPromise("GetPage",{pageIndex:i}).then(t=>{if(this.destroyed)throw new Error("Transport destroyed");t.refStr&&this.#br.set(t.refStr,i);const s=new PDFPageProxy(e,t,this,this.pagesMapper,this._params.pdfBug);this.#mr.set(e,s);return s});this.#fr.set(e,n);return n}async getPageIndex(t){if(!isRefProxy(t))throw new Error("Invalid pageIndex request.");const e=await this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen}),i=this.pagesMapper.getPageNumber(e+1);if(0===i)throw new Error("GetPageIndex: page has been removed.");return i-1}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:this.pagesMapper.getPageId(t+1)-1,intent:e})}getFieldObjects(){return this.#Ar("GetFieldObjects")}hasJSActions(){return this.#Ar("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getAnnotationsByType(t,e){return this.messageHandler.sendWithPromise("GetAnnotationsByType",{types:t,pageIndexesToSkip:e})}getDocJSActions(){return this.#Ar("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:this.pagesMapper.getPageId(t+1)-1})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:this.pagesMapper.getPageId(t+1)-1})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(t){return this.#Ar("GetOptionalContentConfig").then(e=>new OptionalContentConfig(e,t))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata";return this.#pr.getOrInsertComputed(t,()=>this.messageHandler.sendWithPromise(t,null).then(t=>({info:t[0],metadata:t[1]?new Metadata(t[1]):null,contentDispositionFilename:this.#ur?.filename??null,contentLength:this.#ur?.contentLength??null,hasStructTree:t[2]})))}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}getRawData(t){return this.messageHandler.sendWithPromise("GetRawData",t)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#mr.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#pr.clear();this.filterFactory.destroy(!0);TextLayer.cleanup()}}cachedPageNumber(t){if(!isRefProxy(t))return null;const e=0===t.gen?`${t.num}R`:`${t.num}R${t.gen}`,i=this.#br.get(e);if(i>=0){const t=this.pagesMapper.getPageNumber(i+1);if(0!==t)return t}return null}}class RenderTask{_internalRenderTask=null;onContinue=null;onError=null;constructor(t){this._internalRenderTask=t}get promise(){return this._internalRenderTask.capability.promise}cancel(t=0){this._internalRenderTask.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this._internalRenderTask.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this._internalRenderTask;return t.form||t.canvas&&e?.size>0}get imageCoordinates(){return this._internalRenderTask.imageCoordinates||null}}class InternalRenderTask{#vr=null;static#wr=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:r,pageIndex:a,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:d=!1,pageColors:c=null,enableHWA:u=!1,operationsFilter:p=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=r;this._pageIndex=a;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=d;this.pageColors=c;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=Promise.withResolvers();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvas;this._canvasContext=e.canvas?null:e.canvasContext;this._enableHWA=u;this._dependencyTracker=e.dependencyTracker;this._imagesTracker=e.imagesTracker;this._operationsFilter=p}get completed(){return this.capability.promise.catch(function(){})}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#wr.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#wr.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{viewport:i,transform:s,background:n,dependencyTracker:r,imagesTracker:a}=this.params,o=this._canvasContext||this._canvas.getContext("2d",{alpha:!1,willReadFrequently:!this._enableHWA});this.gfx=new CanvasGraphics(o,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors,r,a);this.gfx.beginDrawing({transform:s,viewport:i,transparency:t,background:n});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();if(this.#vr){window.cancelAnimationFrame(this.#vr);this.#vr=null}InternalRenderTask.#wr.delete(this._canvas);t||=new RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e);this.callback(t);this.task.onError?.(t)}operatorListChanged(){if(this.graphicsReady){this.gfx.dependencyTracker?.growOperationsCount(this.operatorList.fnArray.length);this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?this.#vr=window.requestAnimationFrame(()=>{this.#vr=null;this._nextBound().catch(this._cancelBound)}):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper,this._operationsFilter);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#wr.delete(this._canvas);this.callback()}}}}}const Bt="5.7.284",Rt="7e5b36c2d";class ColorPicker{#xr=null;#Cr=null;#Er;#Tr=null;#Sr=!1;#_r=!1;#a=null;#Pr;#Dr=null;#y=null;static#Mr=null;static get _keyboardManager(){return shadow(this,"_keyboardManager",new KeyboardManager([[["Escape","mac+Escape"],ColorPicker.prototype._hideDropdownFromKeyboard],[[" ","mac+ "],ColorPicker.prototype._colorSelectFromKeyboard],[["ArrowDown","ArrowRight","mac+ArrowDown","mac+ArrowRight"],ColorPicker.prototype._moveToNext],[["ArrowUp","ArrowLeft","mac+ArrowUp","mac+ArrowLeft"],ColorPicker.prototype._moveToPrevious],[["Home","mac+Home"],ColorPicker.prototype._moveToBeginning],[["End","mac+End"],ColorPicker.prototype._moveToEnd]]))}constructor({editor:t=null,uiManager:e=null}){if(t){this.#_r=!1;this.#a=t}else this.#_r=!0;this.#y=t?._uiManager||e;this.#Pr=this.#y._eventBus;this.#Er=t?.color?.toUpperCase()||this.#y?.highlightColors.values().next().value||"#FFFF98";ColorPicker.#Mr||=Object.freeze({blue:"pdfjs-editor-colorpicker-blue",green:"pdfjs-editor-colorpicker-green",pink:"pdfjs-editor-colorpicker-pink",red:"pdfjs-editor-colorpicker-red",yellow:"pdfjs-editor-colorpicker-yellow"})}renderButton(){const t=this.#xr=document.createElement("button");t.className="colorPicker";t.tabIndex="0";t.setAttribute("data-l10n-id","pdfjs-editor-colorpicker-button");t.ariaHasPopup="true";this.#a&&(t.ariaControls=`${this.#a.id}_colorpicker_dropdown`);const e=this.#y._signal;t.addEventListener("click",this.#kr.bind(this),{signal:e});t.addEventListener("keydown",this.#Ir.bind(this),{signal:e});const i=this.#Cr=document.createElement("span");i.className="swatch";i.ariaHidden="true";i.style.backgroundColor=this.#Er;t.append(i);return t}renderMainDropdown(){const t=this.#Tr=this.#Fr();t.ariaOrientation="horizontal";t.ariaLabelledBy="highlightColorPickerLabel";return t}#Fr(){const t=document.createElement("div"),e=this.#y._signal;t.addEventListener("contextmenu",noContextMenu,{signal:e});t.className="dropdown";t.role="listbox";t.ariaMultiSelectable="false";t.ariaOrientation="vertical";t.setAttribute("data-l10n-id","pdfjs-editor-colorpicker-dropdown");this.#a&&(t.id=`${this.#a.id}_colorpicker_dropdown`);for(const[i,s]of this.#y.highlightColors){const n=document.createElement("button");n.tabIndex="0";n.role="option";n.setAttribute("data-color",s);n.title=i;n.setAttribute("data-l10n-id",ColorPicker.#Mr[i]);const r=document.createElement("span");n.append(r);r.className="swatch";r.style.backgroundColor=s;n.ariaSelected=s===this.#Er;n.addEventListener("click",this.#Br.bind(this,s),{signal:e});t.append(n)}t.addEventListener("keydown",this.#Ir.bind(this),{signal:e});return t}#Br(t,e){e.stopPropagation();this.#Pr.dispatch("switchannotationeditorparams",{source:this,type:f.HIGHLIGHT_COLOR,value:t});this.updateColor(t)}_colorSelectFromKeyboard(t){if(t.target===this.#xr){this.#kr(t);return}const e=t.target.getAttribute("data-color");e&&this.#Br(e,t)}_moveToNext(t){this.#Rr?t.target!==this.#xr?t.target.nextSibling?.focus():this.#Tr.firstElementChild?.focus():this.#kr(t)}_moveToPrevious(t){if(t.target!==this.#Tr?.firstElementChild&&t.target!==this.#xr){this.#Rr||this.#kr(t);t.target.previousSibling?.focus()}else this.#Rr&&this._hideDropdownFromKeyboard()}_moveToBeginning(t){this.#Rr?this.#Tr.firstElementChild?.focus():this.#kr(t)}_moveToEnd(t){this.#Rr?this.#Tr.lastElementChild?.focus():this.#kr(t)}#Ir(t){ColorPicker._keyboardManager.exec(this,t)}#kr(t){if(this.#Rr){this.hideDropdown();return}this.#Sr=0===t.detail;if(!this.#Dr){this.#Dr=new AbortController;window.addEventListener("pointerdown",this.#p.bind(this),{signal:this.#y.combinedSignal(this.#Dr)})}this.#xr.ariaExpanded="true";if(this.#Tr){this.#Tr.classList.remove("hidden");return}const e=this.#Tr=this.#Fr();this.#xr.append(e)}#p(t){this.#Tr?.contains(t.target)||this.hideDropdown()}hideDropdown(){this.#Tr?.classList.add("hidden");this.#xr.ariaExpanded="false";this.#Dr?.abort();this.#Dr=null}get#Rr(){return this.#Tr&&!this.#Tr.classList.contains("hidden")}_hideDropdownFromKeyboard(){if(!this.#_r)if(this.#Rr){this.hideDropdown();this.#xr.focus({preventScroll:!0,focusVisible:this.#Sr})}else this.#a?.unselect()}updateColor(t){this.#Cr&&(this.#Cr.style.backgroundColor=t);if(!this.#Tr)return;const e=this.#y.highlightColors.values();for(const i of this.#Tr.children)i.ariaSelected=e.next().value===t.toUpperCase()}destroy(){this.#xr?.remove();this.#xr=null;this.#Cr=null;this.#Tr?.remove();this.#Tr=null}}class BasicColorPicker{#Or=null;#Lr=!1;#a=null;#y=null;static#Mr=null;constructor(t){this.#a=t;this.#y=t._uiManager;BasicColorPicker.#Mr||=Object.freeze({freetext:"pdfjs-editor-color-picker-free-text-input",ink:"pdfjs-editor-color-picker-ink-input"})}renderButton(){if(this.#Or)return this.#Or;const{editorType:t,colorType:e,colorAndOpacityType:i,opacityType:s,color:n,opacity:r}=this.#a,a=this.#Lr=FeatureTest.isAlphaColorInputSupported&&void 0!==s,o=this.#Or=document.createElement("input");o.type="color";if(a){o.setAttribute("alpha","");const t=Util.hexNums[Math.round(255*(r??1))];o.value=(n||"#000000")+t}else o.value=n||"#000000";o.className="basicColorPicker";o.tabIndex=0;o.setAttribute("data-l10n-id",BasicColorPicker.#Mr[t]);o.addEventListener("input",()=>{if(a){const t=getRGBA(o.value);if(!t)return;const[n,r,a,l]=t,h=Util.makeHexColor(n,r,a);if(void 0!==i)this.#y.updateParams(i,{color:h,opacity:l});else{this.#y.updateParams(e,h);this.#y.updateParams(s,l)}}else this.#y.updateParams(e,o.value)},{signal:this.#y._signal});return o}update(t){if(this.#Or)if(this.#Lr){const e=Util.hexNums[Math.round(255*this.#a.opacity)];this.#Or.value=t+e}else this.#Or.value=t}updateOpacity(t){if(!this.#Or||!this.#Lr)return;const e=Util.hexNums[Math.round(255*t)];this.#Or.value=this.#a.color+e}destroy(){this.#Or?.remove();this.#Or=null}hideDropdown(){}}function makeColorComp(t){return Math.floor(255*MathClamp(t,0,1)).toString(16).padStart(2,"0")}function scaleAndClamp(t){return 255*MathClamp(t,0,1)}class ColorConverters{static CMYK_G([t,e,i,s]){return["G",1-Math.min(1,.3*t+.59*i+.11*e+s)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_rgb([t]){return[t=scaleAndClamp(t),t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,i]){return["G",.3*t+.59*e+.11*i]}static RGB_rgb(t){return t.map(scaleAndClamp)}static RGB_HTML(t){return`#${t.map(makeColorComp).join("")}`}static T_HTML(){return"#00000000"}static T_rgb(){return[null]}static CMYK_RGB([t,e,i,s]){return["RGB",1-Math.min(1,t+s),1-Math.min(1,i+s),1-Math.min(1,e+s)]}static CMYK_rgb([t,e,i,s]){return[scaleAndClamp(1-Math.min(1,t+s)),scaleAndClamp(1-Math.min(1,i+s)),scaleAndClamp(1-Math.min(1,e+s))]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,i]){const s=1-t,n=1-e,r=1-i;return["CMYK",s,n,r,Math.min(s,n,r)]}}class BaseSVGFactory{create(t,e,i=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const s=this._createSVG("svg:svg");s.setAttribute("version","1.1");if(!i){s.setAttribute("width",`${t}px`);s.setAttribute("height",`${e}px`)}s.setAttribute("preserveAspectRatio","none");s.setAttribute("viewBox",`0 0 ${t} ${e}`);return s}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){unreachable("Abstract method `_createSVG` called.")}}class DOMSVGFactory extends BaseSVGFactory{_createSVG(t){return document.createElementNS(V,t)}}const Ot=new WeakSet,Lt=60*(new Date).getTimezoneOffset()*1e3;class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case T.LINK:return new LinkAnnotationElement(t);case T.TEXT:return new TextAnnotationElement(t);case T.WIDGET:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t);case"Sig":return new SignatureWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case T.POPUP:return new PopupAnnotationElement(t);case T.FREETEXT:return new FreeTextAnnotationElement(t);case T.LINE:return new LineAnnotationElement(t);case T.SQUARE:return new SquareAnnotationElement(t);case T.CIRCLE:return new CircleAnnotationElement(t);case T.POLYLINE:return new PolylineAnnotationElement(t);case T.CARET:return new CaretAnnotationElement(t);case T.INK:return new InkAnnotationElement(t);case T.POLYGON:return new PolygonAnnotationElement(t);case T.HIGHLIGHT:return new HighlightAnnotationElement(t);case T.UNDERLINE:return new UnderlineAnnotationElement(t);case T.SQUIGGLY:return new SquigglyAnnotationElement(t);case T.STRIKEOUT:return new StrikeOutAnnotationElement(t);case T.STAMP:return new StampAnnotationElement(t);case T.FILEATTACHMENT:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{#Nr=null;#Ur=!1;#Hr=null;constructor(t,{isRenderable:e=!1,ignoreBorder:i=!1,createQuadrilaterals:s=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableComment=t.enableComment;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;this.parent=t.parent;this.hasOwnCommentButton=!1;e&&(this.contentElement=this.container=this._createContainer(i));s&&this._createQuadrilaterals()}static _hasPopupData({contentsObj:t,richText:e}){return!(!t?.str&&!e?.str)}get _isEditable(){return this.data.isEditable}get hasPopupData(){return AnnotationElement._hasPopupData(this.data)||this.enableComment&&!!this.commentText}get commentData(){const{data:t}=this,e=this.annotationStorage?.getEditor(t.id);return e?e.getData():t}get hasCommentButton(){return this.enableComment&&this.hasPopupElement}get commentButtonPosition(){const t=this.annotationStorage?.getEditor(this.data.id);if(t)return t.commentButtonPositionInPage;const{quadPoints:e,inkLists:i,rect:s}=this.data;let n=-1/0,r=-1/0;if(e?.length>=8){for(let t=0;tr){r=e[t+1];n=e[t+2]}else e[t+1]===r&&(n=Math.max(n,e[t+2]));return[n,r]}if(i?.length>=1){for(const t of i)for(let e=0,i=t.length;er){r=t[e+1];n=t[e]}else t[e+1]===r&&(n=Math.max(n,t[e]));if(n!==1/0)return[n,r]}return s?[s[2],s[3]]:null}_normalizePoint(t){const{page:{view:e},viewport:{rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:r}}}=this.parent;t[1]=e[3]-t[1]+e[1];t[0]=100*(t[0]-n)/i;t[1]=100*(t[1]-r)/s;return t}get commentText(){const{data:t}=this;return this.annotationStorage.getRawValue(`${g}${t.id}`)?.popup?.contents||t.contentsObj?.str||""}set commentText(t){const{data:e}=this,i={deleted:!t,contents:t||""};this.annotationStorage.updateEditor(e.id,{popup:i})||this.annotationStorage.setValue(`${g}${e.id}`,{id:e.id,annotationType:e.annotationType,page:this.parent.page,popup:i,popupRef:e.popupRef,modificationDate:new Date});t||this.removePopup()}removePopup(){(this.#Hr?.popup||this.popup)?.remove();this.#Hr=this.popup=null}updateEdited(t){if(!this.container)return;t.rect&&(this.#Nr||={rect:this.data.rect.slice(0)});const{rect:e,popup:i}=t;e&&this.#zr(e);let s=this.#Hr?.popup||this.popup;if(!s&&i?.text){this._createPopup(i);s=this.#Hr.popup}if(s){s.updateEdited(t);if(i?.deleted){s.remove();this.#Hr=null;this.popup=null}}}resetEdited(){if(this.#Nr){this.#zr(this.#Nr.rect);this.#Hr?.popup.resetEdited();this.#Nr=null}}#zr(t){const{container:{style:e},data:{rect:i,rotation:s},parent:{viewport:{rawDims:{pageWidth:n,pageHeight:r,pageX:a,pageY:o}}}}=this;i?.splice(0,4,...t);e.left=100*(t[0]-a)/n+"%";e.top=100*(r-t[3]+o)/r+"%";if(0===s){e.width=100*(t[2]-t[0])/n+"%";e.height=100*(t[3]-t[1])/r+"%"}else this.setRotation(s)}_createContainer(t){const{data:e,parent:{page:i,viewport:s}}=this,n=document.createElement("section");n.setAttribute("data-annotation-id",e.id);this instanceof WidgetAnnotationElement||this instanceof LinkAnnotationElement||(n.tabIndex=0);const{style:r}=n;r.zIndex=this.parent.zIndex;this.parent.zIndex+=2;e.alternativeText&&(n.title=e.alternativeText);e.noRotate&&n.classList.add("norotate");if(!e.rect||this instanceof PopupAnnotationElement){const{rotation:t}=e;e.hasOwnCanvas||0===t||this.setRotation(t,n);return n}const{width:a,height:o}=this;if(!t&&e.borderStyle.width>0){r.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,i=e.borderStyle.verticalCornerRadius;if(t>0||i>0){const e=`calc(${t}px * var(--total-scale-factor)) / calc(${i}px * var(--total-scale-factor))`;r.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${a}px * var(--total-scale-factor)) / calc(${o}px * var(--total-scale-factor))`;r.borderRadius=t}switch(e.borderStyle.style){case S:r.borderStyle="solid";break;case _:r.borderStyle="dashed";break;case P:warn("Unimplemented border style: beveled");break;case D:warn("Unimplemented border style: inset");break;case M:r.borderBottomStyle="solid"}const s=e.borderColor||null;if(s){this.#Ur=!0;r.borderColor=Util.makeHexColor(...s)}else r.borderWidth=0}const l=Util.normalizeRect([e.rect[0],i.view[3]-e.rect[1]+i.view[1],e.rect[2],i.view[3]-e.rect[3]+i.view[1]]),{pageWidth:h,pageHeight:d,pageX:c,pageY:u}=s.rawDims;r.left=100*(l[0]-c)/h+"%";r.top=100*(l[1]-u)/d+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){r.width=100*a/h+"%";r.height=100*o/d+"%"}else this.setRotation(p,n);return n}setRotation(t,e=this.container){if(!this.data.rect)return;const{pageWidth:i,pageHeight:s}=this.parent.viewport.rawDims;let{width:n,height:r}=this;t%180!=0&&([n,r]=[r,n]);e.style.width=100*n/i+"%";e.style.height=100*r/s+"%";e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,i)=>{const s=i.detail[t],n=s[0],r=s.slice(1);i.target.style[e]=ColorConverters[`${n}_HTML`](r);this.annotationStorage.setValue(this.data.id,{[e]:ColorConverters[`${n}_rgb`](r)})};return shadow(this,"_commonActions",{display:t=>{const{display:e}=t.detail,i=e%2==1;this.container.style.visibility=i?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noView:i,noPrint:1===e||2===e})},print:t=>{this.annotationStorage.setValue(this.data.id,{noPrint:!t.detail.print})},hidden:t=>{const{hidden:e}=t.detail;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noPrint:e,noView:e})},focus:t=>{setTimeout(()=>t.target.focus({preventScroll:!1}),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.target.disabled=t.detail.readonly},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const i=this._commonActions;for(const s of Object.keys(e.detail)){const n=t[s]||i[s];n?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const i=this._commonActions;for(const[s,n]of Object.entries(e)){const r=i[s];if(r){r({detail:{[s]:n},target:t});delete e[s]}}}_createQuadrilaterals(){if(!this.container)return;const{quadPoints:t}=this.data;if(!t)return;const[e,i,s,n]=this.data.rect.map(t=>Math.fround(t));if(8===t.length){const[r,a,o,l]=t.subarray(2,6);if(s===r&&n===a&&e===o&&i===l)return}const{style:r}=this.container;let a;if(this.#Ur){const{borderColor:t,borderWidth:e}=r;r.borderWidth=0;a=["url('data:image/svg+xml;utf8,",'',``];this.container.classList.add("hasBorder")}const o=s-e,l=n-i,{svgFactory:h}=this,d=h.createElement("svg");d.classList.add("quadrilateralsContainer");d.setAttribute("width",0);d.setAttribute("height",0);d.role="none";const c=h.createElement("defs");d.append(c);const u=h.createElement("clipPath"),p=`clippath_${this.data.id}`;u.setAttribute("id",p);u.setAttribute("clipPathUnits","objectBoundingBox");c.append(u);for(let i=2,s=t.length;i`)}if(this.#Ur){a.push("')");r.backgroundImage=a.join("")}this.container.append(d);this.container.style.clipPath=`url(#${p})`}_createPopup(t=null){const{data:e}=this;let i,s;if(t){i={str:t.text};s=t.date}else{i=e.contentsObj;s=e.modificationDate}this.#Hr=new PopupAnnotationElement({data:{color:e.color,titleObj:e.titleObj,modificationDate:s,contentsObj:i,richText:e.richText,parentRect:e.rect,borderStyle:0,id:`popup_${e.id}`,rotation:e.rotation,noRotate:!0},linkService:this.linkService,parent:this.parent,elements:[this]})}get hasPopupElement(){return!!(this.#Hr||this.popup||this.data.popupRef)}get extraPopupElement(){return this.#Hr}render(){unreachable("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const i=[];if(this._fieldObjects){const s=this._fieldObjects[t];if(s)for(const{page:t,id:n,exportValues:r}of s){if(-1===t)continue;if(n===e)continue;const s="string"==typeof r?r:null,a=document.querySelector(`[data-element-id="${n}"]`);!a||Ot.has(a)?i.push({id:n,exportValue:s,domElement:a}):warn(`_getElementsByName - element not allowed: ${n}`)}return i}for(const s of document.getElementsByName(t)){const{exportValue:t}=s,n=s.getAttribute("data-element-id");n!==e&&(Ot.has(s)&&i.push({id:n,exportValue:t,domElement:s}))}return i}show(){this.container&&(this.container.hidden=!1);this.popup?.maybeShow()}hide(){this.container&&(this.container.hidden=!0);this.popup?.forceHide()}getElementsToTriggerPopup(){return this.container}addHighlightArea(){const t=this.getElementsToTriggerPopup();if(Array.isArray(t))for(const e of t)e.classList.add("highlightArea");else t.classList.add("highlightArea")}_editOnDoubleClick(){if(!this._isEditable)return;const{annotationEditorType:t,data:{id:e}}=this;this.container.addEventListener("dblclick",()=>{this.linkService.eventBus?.dispatch("switchannotationeditormode",{source:this,mode:t,editId:e,mustEnterInEditMode:!0})})}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class EditorAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0,ignoreBorder:!0});this.editor=t.editor}render(){this.container.className="editorAnnotation";return this.container}createOrUpdatePopup(){const{editor:t}=this;t.hasComment&&this._createPopup(t.comment)}get hasCommentButton(){return this.enableComment&&this.editor.hasComment}get commentButtonPosition(){return this.editor.commentButtonPositionInPage}get commentText(){return this.editor.comment.text}set commentText(t){this.editor.comment=t;t||this.removePopup()}get commentData(){return this.editor.getData()}remove(){this.parent.removeAnnotation(this.data.id);this.container.remove();this.container=null;this.removePopup()}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,i=document.createElement("a");i.setAttribute("data-element-id",t.id);let s=!1;if(t.url){e.addLinkAttributes(i,t.url,t.newWindow);s=!0}else if(t.action){this._bindNamedAction(i,t.action,t.overlaidText);s=!0}else if(t.attachment){this.#Gr(i,t.attachment,t.overlaidText,t.attachmentDest);s=!0}else if(t.setOCGState){this.#Wr(i,t.setOCGState,t.overlaidText);s=!0}else if(t.dest){this._bindLink(i,t.dest,t.overlaidText);s=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(i,t);s=!0}if(t.resetForm){this._bindResetFormAction(i,t.resetForm);s=!0}else if(this.isTooltipOnly&&!s){this._bindLink(i,"");s=!0}}this.container.classList.add("linkAnnotation");if(s){this.contentElement=i;this.container.append(i)}return this.container}#Vr(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e,i=""){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#Vr();i&&(t.title=i)}_bindNamedAction(t,e,i=""){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};i&&(t.title=i);this.#Vr()}#Gr(t,e,i="",s=null){t.href=this.linkService.getAnchorUrl("");e.description?t.title=e.description:i&&(t.title=i);t.onclick=()=>{this.downloadManager?.openOrDownloadData(e.content,e.filename,s);return!1};this.#Vr()}#Wr(t,e,i=""){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};i&&(t.title=i);this.#Vr()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const i=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const s of Object.keys(e.actions)){const n=i.get(s);n&&(t[n]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:s}});return!1})}e.overlaidText&&(t.title=e.overlaidText);t.onclick||(t.onclick=()=>!1);this.#Vr()}_bindResetFormAction(t,e){const i=t.onclick;i||(t.href=this.linkService.getAnchorUrl(""));this.#Vr();if(this._fieldObjects)t.onclick=()=>{i?.();const{fields:t,refs:s,include:n}=e,r=[];if(0!==t.length||0!==s.length){const e=new Set(s);for(const i of t){const t=this._fieldObjects[i]||[];for(const{id:i}of t)e.add(i)}for(const t of Object.values(this._fieldObjects))for(const i of t)e.has(i.id)===n&&r.push(i)}else for(const t of Object.values(this._fieldObjects))r.push(...t);const a=this.annotationStorage,o=[];for(const t of r){const{id:e}=t;o.push(e);switch(t.type){case"text":{const i=t.defaultValue||"";a.setValue(e,{value:i});break}case"checkbox":case"radiobutton":{const i=t.defaultValue===t.exportValues;a.setValue(e,{value:i});break}case"combobox":case"listbox":{const i=t.defaultValue||"";a.setValue(e,{value:i});break}default:continue}const i=document.querySelector(`[data-element-id="${e}"]`);i&&(Ot.has(i)?i.dispatchEvent(new Event("resetform")):warn(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:o,name:"ResetForm"}});return!1};else{warn('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');i||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0})}render(){this.container.classList.add("textAnnotation");const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.setAttribute("data-l10n-id","pdfjs-text-annotation-type");t.setAttribute("data-l10n-args",JSON.stringify({type:this.data.name}));if(!this.data.popupRef&&this.hasPopupData){this.hasOwnCommentButton=!0;this._createPopup()}this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){return this.container}showElementAndHideCanvas(t){if(this.data.hasOwnCanvas){"CANVAS"===t.previousSibling?.nodeName&&(t.previousSibling.hidden=!0);t.hidden=!1}}_getKeyModifier(t){return FeatureTest.platform.isMac?t.metaKey:t.ctrlKey}_setEventListener(t,e,i,s,n){i.includes("mouse")?t.addEventListener(i,t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})}):t.addEventListener(i,t=>{if("blur"===i){if(!e.focused||!t.relatedTarget)return;e.focused=!1}else if("focus"===i){if(e.focused)return;e.focused=!0}n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})})}_setEventListeners(t,e,i,s){for(const[n,r]of i)if("Action"===r||this.data.actions?.[r]){"Focus"!==r&&"Blur"!==r||(e||={focused:!1});this._setEventListener(t,e,n,r,s);"Focus"!==r||this.data.actions?.Blur?"Blur"!==r||this.data.actions?.Focus||this._setEventListener(t,e,"focus","Focus",null):this._setEventListener(t,e,"blur","Blur",null)}}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":Util.makeHexColor(...e)}_setTextStyle(t){const e=["left","center","right"],{fontColor:i}=this.data.defaultAppearanceData,s=this.data.defaultAppearanceData.fontSize||9,r=t.style;let a;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(n*s))||1);a=Math.min(s,roundToOneDecimal(e/n))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);a=Math.min(s,roundToOneDecimal(t/n))}r.fontSize=`calc(${a}px * var(--total-scale-factor))`;r.color=Util.makeHexColor(...i);null!==this.data.textAlignment&&(r.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||t.data.hasOwnCanvas||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,i,s){const n=this.annotationStorage;for(const r of this._getElementsByName(t.name,t.id)){r.domElement&&(r.domElement[e]=i);n.setValue(r.id,{[s]:i})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.classList.add("textWidgetAnnotation");let i=null;if(this.renderForms){const s=t.getValue(e,{value:this.data.fieldValue});let n=s.value||"";const r=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;r&&n.length>r&&(n=n.slice(0,r));let a=s.formattedValue||this.data.textContent?.join("\n")||null;a&&this.data.comb&&(a=a.replaceAll(/\s+/g,""));const o={userValue:n,formattedValue:a,lastCommittedValue:null,commitKey:1,focused:!1};if(this.data.multiLine){i=document.createElement("textarea");i.textContent=a??n;this.data.doNotScroll&&(i.style.overflowY="hidden")}else{i=document.createElement("input");i.type=this.data.password?"password":"text";i.setAttribute("value",a??n);this.data.doNotScroll&&(i.style.overflowX="hidden")}this.data.hasOwnCanvas&&(i.hidden=!0);Ot.add(i);this.contentElement=i;i.setAttribute("data-element-id",e);i.disabled=this.data.readOnly;i.name=this.data.fieldName;i.tabIndex=0;const{datetimeFormat:l,datetimeType:h,timeStep:d}=this.data,c=!!h&&this.enableScripting;l&&(i.title=l);this._setRequired(i,this.data.required);r&&(i.maxLength=r);i.addEventListener("input",s=>{t.setValue(e,{value:s.target.value});this.setPropertyOnSiblings(i,"value",s.target.value,"value");o.formattedValue=null});i.addEventListener("resetform",t=>{const e=this.data.defaultFieldValue??"";i.value=o.userValue=e;o.formattedValue=null});let blurListener=t=>{const{formattedValue:e}=o;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){i.addEventListener("focus",t=>{if(o.focused)return;const{target:e}=t;if(c){e.type=h;d&&(e.step=d)}if(o.userValue){const t=o.userValue;if(c)if("time"===h){const i=new Date(t),s=[i.getHours(),i.getMinutes(),i.getSeconds()];e.value=s.map(t=>t.toString().padStart(2,"0")).join(":")}else e.value=new Date(t-Lt).toISOString().split("date"===h?"T":".",1)[0];else e.value=t}o.lastCommittedValue=e.value;o.commitKey=1;this.data.actions?.Focus||(o.focused=!0)});i.addEventListener("updatefromsandbox",i=>{this.showElementAndHideCanvas(i.target);const s={value(i){o.userValue=i.detail.value??"";c||t.setValue(e,{value:o.userValue.toString()});i.target.value=o.userValue},formattedValue(i){const{formattedValue:s}=i.detail;o.formattedValue=s;null!=s&&i.target!==document.activeElement&&(i.target.value=s);const n={formattedValue:s};c&&(n.value=s);t.setValue(e,n)},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:i=>{const{charLimit:s}=i.detail,{target:n}=i;if(0===s){n.removeAttribute("maxLength");return}n.setAttribute("maxLength",s);let r=o.userValue;if(r&&!(r.length<=s)){r=r.slice(0,s);n.value=o.userValue=r;t.setValue(e,{value:r});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,willCommit:!0,commitKey:1,selStart:n.selectionStart,selEnd:n.selectionEnd}})}}};this._dispatchEventFromSandbox(s,i)});i.addEventListener("keydown",t=>{o.commitKey=1;let i=-1;"Escape"===t.key?i=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(o.commitKey=3):i=2;if(-1===i)return;const{value:s}=t.target;if(o.lastCommittedValue!==s){o.lastCommittedValue=s;o.userValue=s;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:i,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}});const s=blurListener;blurListener=null;i.addEventListener("blur",t=>{if(!o.focused||!t.relatedTarget)return;this.data.actions?.Blur||(o.focused=!1);const{target:i}=t;let{value:n}=i;if(c){if(n&&"time"===h){const t=n.split(":").map(t=>parseInt(t,10));n=new Date(2e3,0,1,t[0],t[1],t[2]||0).valueOf();i.step=""}else{n.includes("T")||(n=`${n}T00:00`);n=new Date(n).valueOf()}i.type="text"}o.userValue=n;o.lastCommittedValue!==n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,willCommit:!0,commitKey:o.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});s(t)});this.data.actions?.Keystroke&&i.addEventListener("beforeinput",t=>{o.lastCommittedValue=null;const{data:i,target:s}=t,{value:n,selectionStart:r,selectionEnd:a}=s;let l=r,h=a;switch(t.inputType){case"deleteWordBackward":{const t=n.substring(0,r).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=n.substring(r).match(/^[^\w]*\w*/);t&&(h+=t[0].length);break}case"deleteContentBackward":r===a&&(l-=1);break;case"deleteContentForward":r===a&&(h+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,change:i||"",willCommit:!1,selStart:l,selEnd:h}})});this._setEventListeners(i,o,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],t=>t.target.value)}blurListener&&i.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/r;i.classList.add("comb");i.style.letterSpacing=`calc(${t}px * var(--total-scale-factor) - 1ch)`}}else{i=document.createElement("div");i.textContent=this.data.fieldValue;i.style.verticalAlign="middle";i.style.display="table-cell";this.data.hasOwnCanvas&&(i.hidden=!0)}this._setTextStyle(i);this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class SignatureWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:!!t.data.hasOwnCanvas})}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof s){s="Off"!==s;t.setValue(i,{value:s})}this.container.classList.add("buttonWidgetAnnotation","checkBox");const n=document.createElement("input");Ot.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="checkbox";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.setAttribute("exportValue",e.exportValue);n.tabIndex=0;n.addEventListener("change",s=>{const{name:n,checked:r}=s.target;for(const s of this._getElementsByName(n,i)){const i=r&&s.exportValue===e.exportValue;s.domElement&&(s.domElement.checked=i);t.setValue(s.id,{value:i})}t.setValue(i,{value:r})});n.addEventListener("resetform",t=>{const i=e.defaultFieldValue||"Off";t.target.checked=i===e.exportValue});if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",e=>{const s={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(i,{value:e.target.checked})}};this._dispatchEventFromSandbox(s,e)});this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],t=>t.target.checked)}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("buttonWidgetAnnotation","radioButton");const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof s){s=s!==e.buttonValue;t.setValue(i,{value:s})}if(s)for(const s of this._getElementsByName(e.fieldName,i))t.setValue(s.id,{value:!1});const n=document.createElement("input");Ot.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="radio";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.tabIndex=0;n.addEventListener("change",e=>{const{name:s,checked:n}=e.target;for(const e of this._getElementsByName(s,i))t.setValue(e.id,{value:!1});t.setValue(i,{value:n})});n.addEventListener("resetform",t=>{const i=e.defaultFieldValue;t.target.checked=null!=i&&i===e.buttonValue});if(this.enableScripting&&this.hasJSActions){const s=e.buttonValue;n.addEventListener("updatefromsandbox",e=>{const n={value:e=>{const n=s===e.detail.value;for(const s of this._getElementsByName(e.target.name)){const e=n&&s.id===i;s.domElement&&(s.domElement.checked=e);t.setValue(s.id,{value:e})}}};this._dispatchEventFromSandbox(n,e)});this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],t=>t.target.checked)}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.classList.add("buttonWidgetAnnotation","pushButton");const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",t=>{this._dispatchEventFromSandbox({},t)})}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("choiceWidgetAnnotation");const t=this.annotationStorage,e=this.data.id,i=t.getValue(e,{value:this.data.fieldValue}),s=document.createElement("select");Ot.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;this._setRequired(s,this.data.required);s.name=this.data.fieldName;s.tabIndex=0;let n=this.data.combo&&this.data.options.length>0;if(!this.data.combo){s.size=this.data.options.length;this.data.multiSelect&&(s.multiple=!0)}s.addEventListener("resetform",t=>{const e=this.data.defaultFieldValue;for(const t of s.options)t.selected=t.value===e});const fixDisplayValue=(t,e)=>{const i=e.replaceAll(" "," ");t.textContent=i;i!==e&&t.setAttribute("display-value",e)};for(const t of this.data.options){const e=document.createElement("option");fixDisplayValue(e,t.displayValue);e.value=t.exportValue;if(i.value.includes(t.exportValue)){e.setAttribute("selected",!0);n=!1}s.append(e)}let r=null;if(n){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);s.prepend(t);r=()=>{t.remove();s.removeEventListener("input",r);r=null};s.addEventListener("input",r)}const getValue=t=>{const e=t?"value":"textContent",{options:i,multiple:n}=s;return n?Array.prototype.filter.call(i,t=>t.selected).map(t=>t[e]):-1===i.selectedIndex?null:i[i.selectedIndex][e]};let a=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,t=>({displayValue:t.getAttribute("display-value")||t.textContent,exportValue:t.value}))};if(this.enableScripting&&this.hasJSActions){s.addEventListener("updatefromsandbox",i=>{const n={value(i){r?.();const n=i.detail.value,o=new Set(Array.isArray(n)?n:[n]);for(const t of s.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});a=getValue(!1)},multipleSelection(t){s.multiple=!0},remove(i){const n=s.options,r=i.detail.remove;n[r].selected=!1;s.remove(r);if(n.length>0){-1===Array.prototype.findIndex.call(n,t=>t.selected)&&(n[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(i)});a=getValue(!1)},clear(i){for(;0!==s.length;)s.remove(0);t.setValue(e,{value:null,items:[]});a=getValue(!1)},insert(i){const{index:n,displayValue:r,exportValue:o}=i.detail.insert,l=s.children[n],h=document.createElement("option");fixDisplayValue(h,r);h.value=o;l?l.before(h):s.append(h);t.setValue(e,{value:getValue(!0),items:getItems(i)});a=getValue(!1)},items(i){const{items:n}=i.detail;for(;0!==s.length;)s.remove(0);for(const t of n){const{displayValue:e,exportValue:i}=t,n=document.createElement("option");fixDisplayValue(n,e);n.value=i;s.append(n)}s.options.length>0&&(s.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(i)});a=getValue(!1)},indices(i){const s=new Set(i.detail.indices);for(const t of i.target.options)t.selected=s.has(t.index);t.setValue(e,{value:getValue(!0)});a=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(n,i)});s.addEventListener("input",i=>{const s=getValue(!0),n=getValue(!1);t.setValue(e,{value:s});i.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,change:n,changeEx:s,willCommit:!1,commitKey:1,keyDown:!1}})});this._setEventListeners(s,null,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"],["input","Validate"]],t=>t.target.value)}else s.addEventListener("input",function(i){t.setValue(e,{value:getValue(!0)})});this.data.combo&&this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class PopupAnnotationElement extends AnnotationElement{constructor(t){const{data:e,elements:i,parent:s}=t,n=!!s._commentManager;super(t,{isRenderable:!n&&AnnotationElement._hasPopupData(e)});this.elements=i;if(n&&AnnotationElement._hasPopupData(e)){const t=this.popup=this.#jr();for(const e of i)e.popup=t}else this.popup=null}#jr(){return new PopupElement({container:this.container,color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate||this.data.creationDate,contentsObj:this.data.contentsObj,richText:this.data.richText,rect:this.data.rect,parentRect:this.data.parentRect||null,parent:this.parent,elements:this.elements,open:this.data.open,commentManager:this.parent._commentManager})}render(){const{container:t}=this;t.classList.add("popupAnnotation");t.role="comment";const e=this.popup=this.#jr(),i=[];for(const t of this.elements){t.popup=e;t.container.ariaHasPopup="dialog";i.push(t.data.id);t.addHighlightArea()}this.container.setAttribute("aria-controls",i.map(t=>`${W}${t}`).join(","));return this.container}}class PopupElement{#j=null;#$r=this.#Ir.bind(this);#Xr=this.#qr.bind(this);#Kr=this.#Yr.bind(this);#Qr=this.#Jr.bind(this);#Zr=null;#_t=null;#ta=null;#ea=null;#ia=null;#sa=null;#na=null;#ra=!1;#aa=null;#oa=null;#F=null;#la=null;#ha=null;#ve=null;#da=null;#fe=null;#ca=null;#Nr=null;#ua=!1;#pa=null;#ga=null;constructor({container:t,color:e,elements:i,titleObj:s,modificationDate:n,contentsObj:r,richText:a,parent:o,rect:l,parentRect:h,open:d,commentManager:c=null}){this.#_t=t;this.#ca=s;this.#ta=r;this.#fe=a;this.#sa=o;this.#Zr=e;this.#da=l;this.#na=h;this.#ia=i;this.#j=c;this.#pa=i[0];this.#ea=PDFDateString.toDateObject(n);this.trigger=i.flatMap(t=>t.getElementsToTriggerPopup());if(!c){this.#ma();this.#_t.hidden=!0;d&&this.#Jr()}}#ma(){if(this.#oa)return;this.#oa=new AbortController;const{signal:t}=this.#oa;for(const e of this.trigger){e.addEventListener("click",this.#Qr,{signal:t});e.addEventListener("pointerenter",this.#Kr,{signal:t});e.addEventListener("pointerleave",this.#Xr,{signal:t});e.classList.add("popupTriggerArea")}for(const e of this.#ia)e.container?.addEventListener("keydown",this.#$r,{signal:t})}#fa(){const t=this.#ia.find(t=>t.hasCommentButton);t&&(this.#ha=t._normalizePoint(t.commentButtonPosition))}renderCommentButton(){if(this.#la){this.#la.parentNode||this.#pa.container.after(this.#la);return}this.#ha||this.#fa();if(!this.#ha)return;const{signal:t}=this.#oa=new AbortController,e=this.#pa.hasOwnCommentButton,togglePopup=()=>{this.#j.toggleCommentPopup(this,!0,void 0,!e)},showPopup=()=>{this.#j.toggleCommentPopup(this,!1,!0,!e)},hidePopup=()=>{this.#j.toggleCommentPopup(this,!1,!1)};if(e){this.#la=this.#pa.container;for(const e of this.trigger){e.ariaHasPopup="dialog";e.ariaControls="commentPopup";e.addEventListener("keydown",this.#$r,{signal:t});e.addEventListener("click",togglePopup,{signal:t});e.addEventListener("pointerenter",showPopup,{signal:t});e.addEventListener("pointerleave",hidePopup,{signal:t});e.classList.add("popupTriggerArea")}}else{const e=this.#la=document.createElement("button");e.className="annotationCommentButton";const i=this.#pa.container;e.style.zIndex=parseInt(i.style.zIndex,10)+1;e.tabIndex=0;e.ariaHasPopup="dialog";e.ariaControls="commentPopup";e.setAttribute("data-l10n-id","pdfjs-show-comment-button");this.#ba();this.#ya();e.addEventListener("keydown",this.#$r,{signal:t});e.addEventListener("click",togglePopup,{signal:t});e.addEventListener("pointerenter",showPopup,{signal:t});e.addEventListener("pointerleave",hidePopup,{signal:t});i.after(e)}}#ya(){if(this.#pa.extraPopupElement&&!this.#pa.editor)return;this.#la||this.renderCommentButton();const[t,e]=this.#ha,{style:i}=this.#la;i.left=`calc(${t}%)`;i.top=`calc(${e}% - var(--comment-button-dim))`}#ba(){if(!this.#pa.extraPopupElement){this.#la||this.renderCommentButton();this.#la.style.backgroundColor=this.commentButtonColor||""}}get commentButtonColor(){const{color:t,opacity:e}=this.#pa.commentData;return t?this.#sa._commentManager.makeCommentColor(t,e):null}focusCommentButton(){setTimeout(()=>{this.#la?.focus()},0)}getData(){const{richText:t,color:e,opacity:i,creationDate:s,modificationDate:n}=this.#pa.commentData;return{contentsObj:{str:this.comment},richText:t,color:e,opacity:i,creationDate:s,modificationDate:n}}get elementBeforePopup(){return this.#la}get comment(){this.#ga||=this.#pa.commentText;return this.#ga}set comment(t){t!==this.comment&&(this.#pa.commentText=this.#ga=t)}focus(){this.#pa.container?.focus()}get parentBoundingClientRect(){return this.#pa.layer.getBoundingClientRect()}setCommentButtonStates({selected:t,hasPopup:e}){if(this.#la){this.#la.classList.toggle("selected",t);this.#la.ariaExpanded=e}}setSelectedCommentButton(t){this.#la.classList.toggle("selected",t)}get commentPopupPosition(){if(this.#ve)return this.#ve;const{x:t,y:e,height:i}=this.#la.getBoundingClientRect(),{x:s,y:n,width:r,height:a}=this.#pa.layer.getBoundingClientRect();return[(t-s)/r,(e+i-n)/a]}set commentPopupPosition(t){this.#ve=t}hasDefaultPopupPosition(){return null===this.#ve}get commentButtonPosition(){return this.#ha}get commentButtonWidth(){return this.#la.getBoundingClientRect().width/this.parentBoundingClientRect.width}editComment(t){const[e,i]=this.#ve||this.commentButtonPosition.map(t=>t/100),s=this.parentBoundingClientRect,{x:n,y:r,width:a,height:o}=s;this.#j.showDialog(null,this,n+e*a,r+i*o,{...t,parentDimensions:s})}render(){if(this.#aa)return;const t=this.#aa=document.createElement("div");t.className="popup";if(this.#Zr){const e=t.style.outlineColor=Util.makeHexColor(...this.#Zr);t.style.backgroundColor=`color-mix(in srgb, ${e} 30%, white)`}const e=document.createElement("span");e.className="header";if(this.#ca?.str){const t=document.createElement("span");t.className="title";e.append(t);({dir:t.dir,str:t.textContent}=this.#ca)}t.append(e);if(this.#ea){const t=document.createElement("time");t.className="popupDate";t.setAttribute("data-l10n-id","pdfjs-annotation-date-time-string");t.setAttribute("data-l10n-args",JSON.stringify({dateObj:this.#ea.valueOf()}));t.dateTime=this.#ea.toISOString();e.append(t)}renderRichText({html:this.#Aa||this.#ta.str,dir:this.#ta?.dir,className:"popupContent"},t);this.#_t.append(t)}get#Aa(){const t=this.#fe,e=this.#ta;return!t?.str||e?.str&&e.str!==t.str?null:this.#fe.html||null}get#va(){return this.#Aa?.attributes?.style?.fontSize||0}get#wa(){return this.#Aa?.attributes?.style?.color||null}#xa(t){const e=[],i={str:t,html:{name:"div",attributes:{dir:"auto"},children:[{name:"p",children:e}]}},s={style:{color:this.#wa,fontSize:this.#va?`calc(${this.#va}px * var(--total-scale-factor))`:""}};for(const i of t.split("\n"))e.push({name:"span",value:i,attributes:s});return i}#Ir(t){t.altKey||t.shiftKey||t.ctrlKey||t.metaKey||("Enter"===t.key||"Escape"===t.key&&this.#ra)&&this.#Jr()}updateEdited({rect:t,popup:e,deleted:i}){if(this.#j){if(i){this.remove();this.#ga=null}else if(e)if(e.deleted)this.remove();else{this.#ba();this.#ga=e.text}if(t){this.#ha=null;this.#fa();this.#ya()}}else if(i||e?.deleted)this.remove();else{this.#ma();this.#Nr||={contentsObj:this.#ta,richText:this.#fe};t&&(this.#F=null);if(e&&e.text){this.#fe=this.#xa(e.text);this.#ea=PDFDateString.toDateObject(e.date);this.#ta=null}this.#aa?.remove();this.#aa=null}}resetEdited(){if(this.#Nr){({contentsObj:this.#ta,richText:this.#fe}=this.#Nr);this.#Nr=null;this.#aa?.remove();this.#aa=null;this.#F=null}}remove(){this.#oa?.abort();this.#oa=null;this.#aa?.remove();this.#aa=null;this.#ua=!1;this.#ra=!1;this.#la?.remove();this.#la=null;if(this.trigger)for(const t of this.trigger)t.classList.remove("popupTriggerArea")}#Ca(){if(null!==this.#F)return;const{page:{view:t},viewport:{rawDims:{pageWidth:e,pageHeight:i,pageX:s,pageY:n}}}=this.#sa;let r=!!this.#na,a=r?this.#na:this.#da;for(const t of this.#ia)if(!a||null!==Util.intersect(t.data.rect,a)){a=t.data.rect;r=!0;break}const o=Util.normalizeRect([a[0],t[3]-a[1]+t[1],a[2],t[3]-a[3]+t[1]]),l=r?a[2]-a[0]+5:0,h=o[0]+l,d=o[1];this.#F=[100*(h-s)/e,100*(d-n)/i];const{style:c}=this.#_t;c.left=`${this.#F[0]}%`;c.top=`${this.#F[1]}%`}#Jr(){if(this.#j)this.#j.toggleCommentPopup(this,!1);else{this.#ra=!this.#ra;if(this.#ra){this.#Yr();this.#_t.addEventListener("click",this.#Qr);this.#_t.addEventListener("keydown",this.#$r)}else{this.#qr();this.#_t.removeEventListener("click",this.#Qr);this.#_t.removeEventListener("keydown",this.#$r)}}}#Yr(){this.#aa||this.render();if(this.isVisible)this.#ra&&this.#_t.classList.add("focused");else{this.#Ca();this.#_t.hidden=!1;this.#_t.style.zIndex=parseInt(this.#_t.style.zIndex,10)+1e3}}#qr(){this.#_t.classList.remove("focused");if(!this.#ra&&this.isVisible){this.#_t.hidden=!0;this.#_t.style.zIndex=parseInt(this.#_t.style.zIndex,10)-1e3}}forceHide(){this.#ua=this.isVisible;this.#ua&&(this.#_t.hidden=!0)}maybeShow(){if(!this.#j){this.#ma();if(this.#ua){this.#aa||this.#Yr();this.#ua=!1;this.#_t.hidden=!1}}}get isVisible(){return!this.#j&&!1===this.#_t.hidden}}class FreeTextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0,ignoreBorder:!0});this.textContent=t.data.textContent;this.textPosition=t.data.textPosition;this.annotationEditorType=m.FREETEXT}render(){this.container.classList.add("freeTextAnnotation");if(this.textContent){const t=this.contentElement=document.createElement("div");t.classList.add("annotationTextContent");t.setAttribute("role","comment");for(const e of this.textContent){const i=document.createElement("span");i.textContent=e;t.append(i)}this.container.append(t)}if(!this.data.popupRef&&this.hasPopupData){this.hasOwnCommentButton=!0;this._createPopup()}this._editOnDoubleClick();return this.container}}class LineAnnotationElement extends AnnotationElement{#Ea=null;constructor(t){super(t,{isRenderable:!0,ignoreBorder:!0})}render(){this.container.classList.add("lineAnnotation");const{data:t,width:e,height:i}=this,s=this.svgFactory.create(e,i,!0),n=this.#Ea=this.svgFactory.createElement("svg:line");n.setAttribute("x1",t.rect[2]-t.lineCoordinates[0]);n.setAttribute("y1",t.rect[3]-t.lineCoordinates[1]);n.setAttribute("x2",t.rect[2]-t.lineCoordinates[2]);n.setAttribute("y2",t.rect[3]-t.lineCoordinates[3]);n.setAttribute("stroke-width",t.borderStyle.width||1);n.setAttribute("stroke","transparent");n.setAttribute("fill","transparent");s.append(n);this.container.append(s);if(!t.popupRef&&this.hasPopupData){this.hasOwnCommentButton=!0;this._createPopup()}return this.container}getElementsToTriggerPopup(){return this.#Ea}addHighlightArea(){this.container.classList.add("highlightArea")}}class SquareAnnotationElement extends AnnotationElement{#Ta=null;constructor(t){super(t,{isRenderable:!0,ignoreBorder:!0})}render(){this.container.classList.add("squareAnnotation");const{data:t,width:e,height:i}=this,s=this.svgFactory.create(e,i,!0),n=t.borderStyle.width,r=this.#Ta=this.svgFactory.createElement("svg:rect");r.setAttribute("x",n/2);r.setAttribute("y",n/2);r.setAttribute("width",e-n);r.setAttribute("height",i-n);r.setAttribute("stroke-width",n||1);r.setAttribute("stroke","transparent");r.setAttribute("fill","transparent");s.append(r);this.container.append(s);if(!t.popupRef&&this.hasPopupData){this.hasOwnCommentButton=!0;this._createPopup()}return this.container}getElementsToTriggerPopup(){return this.#Ta}addHighlightArea(){this.container.classList.add("highlightArea")}}class CircleAnnotationElement extends AnnotationElement{#Sa=null;constructor(t){super(t,{isRenderable:!0,ignoreBorder:!0})}render(){this.container.classList.add("circleAnnotation");const{data:t,width:e,height:i}=this,s=this.svgFactory.create(e,i,!0),n=t.borderStyle.width,r=this.#Sa=this.svgFactory.createElement("svg:ellipse");r.setAttribute("cx",e/2);r.setAttribute("cy",i/2);r.setAttribute("rx",e/2-n/2);r.setAttribute("ry",i/2-n/2);r.setAttribute("stroke-width",n||1);r.setAttribute("stroke","transparent");r.setAttribute("fill","transparent");s.append(r);this.container.append(s);if(!t.popupRef&&this.hasPopupData){this.hasOwnCommentButton=!0;this._createPopup()}return this.container}getElementsToTriggerPopup(){return this.#Sa}addHighlightArea(){this.container.classList.add("highlightArea")}}class PolylineAnnotationElement extends AnnotationElement{#_a=null;constructor(t){super(t,{isRenderable:!0,ignoreBorder:!0});this.containerClassName="polylineAnnotation";this.svgElementName="svg:polyline"}render(){this.container.classList.add(this.containerClassName);const{data:{rect:t,vertices:e,borderStyle:i,popupRef:s},width:n,height:r}=this;if(!e)return this.container;const a=this.svgFactory.create(n,r,!0);let o=[];for(let i=0,s=e.length;i=0&&n.setAttribute("stroke-width",e||1);if(i)for(let t=0,e=this.#Da.length;t{"Enter"===t.key&&(s?t.metaKey:t.ctrlKey)&&this.#Ia()});if(!e.popupRef&&this.hasPopupData){this.hasOwnCommentButton=!0;this._createPopup()}else i.classList.add("popupTriggerArea");t.append(i);return t}getElementsToTriggerPopup(){return this.#ka}addHighlightArea(){this.container.classList.add("highlightArea")}#Ia(){this.downloadManager?.openOrDownloadData(this.content,this.filename)}}class AnnotationLayer{#Fa=null;#Ba=null;#G=null;#Ra=new Map;#Oa=null;#La=null;#ia=[];#Na=!1;zIndex=0;constructor({div:t,accessibilityManager:e,annotationCanvasMap:i,annotationEditorUIManager:s,page:n,viewport:r,structTreeLayer:a,commentManager:o,linkService:l,annotationStorage:h}){this.div=t;this.#Fa=e;this.#Ba=i;this.#Oa=a||null;this.#La=l||null;this.#G=h||new AnnotationStorage;this.page=n;this.viewport=r;this._annotationEditorUIManager=s;this._commentManager=o||null}hasEditableAnnotations(){return this.#Ra.size>0}async render(t){const{annotations:e}=t,i=this.div;setLayerDimensions(i,this.viewport);const s=new Map,n=[],r={data:null,layer:i,linkService:this.#La,downloadManager:t.downloadManager,imageResourcesPath:t.imageResourcesPath||"",renderForms:!1!==t.renderForms,svgFactory:new DOMSVGFactory,annotationStorage:this.#G,enableComment:!0===t.enableComment,enableScripting:!0===t.enableScripting,hasJSActions:t.hasJSActions,fieldObjects:t.fieldObjects,parent:this,elements:null};for(const t of e){if(t.noHTML)continue;const e=t.annotationType===T.POPUP;if(e){const e=s.get(t.id);if(!e)continue;if(!this._commentManager){n.push(t);continue}r.elements=e}else if(t.rect[2]===t.rect[0]||t.rect[3]===t.rect[1])continue;r.data=t;const i=AnnotationElementFactory.create(r);if(!i.isRenderable)continue;if(!e){this.#ia.push(i);t.popupRef&&s.getOrInsertComputed(t.popupRef,makeArr).push(i)}const a=i.render();t.hidden&&(a.style.visibility="hidden");if(i._isEditable){this.#Ra.set(i.data.id,i);this._annotationEditorUIManager?.renderAnnotationElement(i)}}await this.#Ua();for(const t of n){const e=r.elements=s.get(t.id);r.data=t;const i=AnnotationElementFactory.create(r);if(!i.isRenderable)continue;const n=i.render();i.contentElement.id=`${W}${t.id}`;t.hidden&&(n.style.visibility="hidden");e.at(-1).container.after(n)}this.#Ha()}async#Ua(){if(0===this.#ia.length)return;this.div.replaceChildren();const t=[];if(!this.#Na){this.#Na=!0;for(const{contentElement:e,data:{id:i}}of this.#ia){const s=e.id=`${W}${i}`;t.push(this.#Oa?.getAriaAttributes(s).then(t=>{if(t)for(const[i,s]of t)e.setAttribute(i,s)}))}}this.#ia.sort(({data:{rect:[t,e,i,s]}},{data:{rect:[n,r,a,o]}})=>{if(t===i&&e===s)return 1;if(n===a&&r===o)return-1;const l=(e+s)/2,h=(r+o)/2;if(l>=o&&h<=e)return-1;if(h>=s&&l<=r)return 1;return(t+i)/2-(n+a)/2});const e=document.createDocumentFragment();for(const t of this.#ia){e.append(t.container);this._commentManager?(t.extraPopupElement?.popup||t.popup)?.renderCommentButton():t.extraPopupElement&&e.append(t.extraPopupElement.render())}this.div.append(e);await Promise.all(t);if(this.#Fa)for(const t of this.#ia)this.#Fa.addPointerInTextLayer(t.contentElement,!1)}async addLinkAnnotations(t){const e={data:null,layer:this.div,linkService:this.#La,svgFactory:new DOMSVGFactory,parent:this};for(const i of t){i.borderStyle||=AnnotationLayer._defaultBorderStyle;e.data=i;const t=AnnotationElementFactory.create(e);if(t.isRenderable){t.render();t.contentElement.id=`${W}${i.id}`;this.#ia.push(t)}}await this.#Ua()}update({viewport:t}){const e=this.div;this.viewport=t;setLayerDimensions(e,{rotation:t.rotation});this.#Ha();e.hidden=!1}#Ha(){if(!this.#Ba)return;const t=this.div;for(const[e,i]of this.#Ba){const s=t.querySelector(`[data-annotation-id="${e}"]`);if(!s)continue;i.className="annotationContent";const{firstChild:n}=s;n?"CANVAS"===n.nodeName?n.replaceWith(i):n.classList.contains("annotationContent")?n.after(i):n.before(i):s.append(i);const r=this.#Ra.get(e);if(r)if(r._hasNoCanvas){this._annotationEditorUIManager?.setMissingCanvas(e,s.id,i);r._hasNoCanvas=!1}else r.canvas=i}this.#Ba.clear()}getEditableAnnotations(){return this.#Ra.values()}getEditableAnnotation(t){return this.#Ra.get(t)}addFakeAnnotation(t){const{div:e}=this,{id:i,rotation:s}=t,n=new EditorAnnotationElement({data:{id:i,rect:t.getPDFRect(),rotation:s},editor:t,layer:e,parent:this,enableComment:!!this._commentManager,linkService:this.#La,annotationStorage:this.#G});n.render();n.contentElement.id=`${W}${i}`;n.createOrUpdatePopup();this.#ia.push(n);return n}removeAnnotation(t){const e=this.#ia.findIndex(e=>e.data.id===t);if(e<0)return;const[i]=this.#ia.splice(e,1);this.#Fa?.removePointerInTextLayer(i.contentElement)}updateFakeAnnotations(t){if(0!==t.length){for(const e of t)e.updateFakeAnnotationElement(this);this.#Ua()}}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}static get _defaultBorderStyle(){return shadow(this,"_defaultBorderStyle",Object.freeze({width:1,rawWidth:1,style:S,dashArray:[3],horizontalCornerRadius:0,verticalCornerRadius:0}))}}const Nt=/\r\n?|\n/g;class FreeTextEditor extends AnnotationEditor{#za="";#Ga=`${this.id}-editor`;#Wa=null;#va;_colorPicker=null;static _freeTextDefaultContent="";static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static get _keyboardManager(){const t=FreeTextEditor.prototype,arrowChecker=t=>t.isEmpty(),e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+s","mac+meta+s","ctrl+p","mac+meta+p"],t.commitOrRemove,{bubbles:!0}],[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],t.commitOrRemove],[["ArrowLeft","mac+ArrowLeft"],t._translateEmpty,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t._translateEmpty,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t._translateEmpty,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t._translateEmpty,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t._translateEmpty,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t._translateEmpty,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t._translateEmpty,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t._translateEmpty,{args:[0,i],checker:arrowChecker}]]))}static _type="freetext";static _editorType=m.FREETEXT;constructor(t){super({...t,name:"freeTextEditor"});this.color=t.color||FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor;this.#va=t.fontSize||FreeTextEditor._defaultFontSize;this.annotationElementId||this._uiManager.a11yAlert("pdfjs-editor-freetext-added-alert");this.canAddComment=!1}static initialize(t,e){AnnotationEditor.initialize(t,e);const i=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(i.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case f.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case f.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case f.FREETEXT_SIZE:this.#Va(e);break;case f.FREETEXT_COLOR:this.#ba(e)}}static get defaultPropertiesToUpdate(){return[[f.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[f.FREETEXT_COLOR,FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[f.FREETEXT_SIZE,this.#va],[f.FREETEXT_COLOR,this.color]]}get toolbarButtons(){this._colorPicker||=new BasicColorPicker(this);return[["colorPicker",this._colorPicker]]}get colorType(){return f.FREETEXT_COLOR}#Va(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--total-scale-factor))`;this.translate(0,-(t-this.#va)*this.parentScale);this.#va=t;this.#ja()},e=this.#va;this.addCommands({cmd:setFontsize.bind(this,t),undo:setFontsize.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:f.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}onUpdatedColor(){this.editorDiv.style.color=this.color;this._colorPicker?.update(this.color);super.onUpdatedColor()}#ba(t){const setColor=t=>{this.color=t;this.onUpdatedColor()},e=this.color;this.addCommands({cmd:setColor.bind(this,t),undo:setColor.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:f.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}_translateEmpty(t,e){this._uiManager.translateSelectedEditors(t,e,!0)}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#va)*t]}rebuild(){if(this.parent){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}}enableEditMode(){if(!super.enableEditMode())return!1;this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this._isDraggable=!1;this.div.removeAttribute("aria-activedescendant");this.#Wa=new AbortController;const t=this._uiManager.combinedSignal(this.#Wa);this.editorDiv.addEventListener("keydown",this.editorDivKeydown.bind(this),{signal:t});this.editorDiv.addEventListener("focus",this.editorDivFocus.bind(this),{signal:t});this.editorDiv.addEventListener("blur",this.editorDivBlur.bind(this),{signal:t});this.editorDiv.addEventListener("input",this.editorDivInput.bind(this),{signal:t});this.editorDiv.addEventListener("paste",this.editorDivPaste.bind(this),{signal:t});return!0}disableEditMode(){if(!super.disableEditMode())return!1;this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#Ga);this._isDraggable=!0;this.#Wa?.abort();this.#Wa=null;this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freetextEditing");return!0}focusin(t){if(this._focusEventsAllowed){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}}onceAdded(t){if(!this.width){this.enableEditMode();t&&this.editorDiv.focus();this._initialOptions?.isCentered&&this.center();this._initialOptions=null}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;if(this.parent){this.parent.setEditingState(!0);this.parent.div.classList.add("freetextEditing")}super.remove()}#$a(){const t=[];this.editorDiv.normalize();let e=null;for(const i of this.editorDiv.childNodes)if(e?.nodeType!==Node.TEXT_NODE||"BR"!==i.nodeName){t.push(FreeTextEditor.#Xa(i));e=i}return t.join("\n")}#ja(){const[t,e]=this.parentDimensions;let i;if(this.isAttachedToDOM)i=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,s=e.style.display,n=e.classList.contains("hidden");e.classList.remove("hidden");e.style.display="hidden";t.div.append(this.div);i=e.getBoundingClientRect();e.remove();e.style.display=s;e.classList.toggle("hidden",n)}if(this.rotation%180==this.parentRotation%180){this.width=i.width/t;this.height=i.height/e}else{this.width=i.height/t;this.height=i.width/e}this.fixAndSetPosition()}commit(){if(!this.isInEditMode())return;super.commit();this.disableEditMode();const t=this.#za,e=this.#za=this.#$a().trimEnd();if(t===e)return;const setText=t=>{this.#za=t;if(t){this.#qa();this._uiManager.rebuild(this);this.#ja()}else this.remove()};this.addCommands({cmd:()=>{setText(e)},undo:()=>{setText(t)},mustExec:!1});this.#ja()}shouldGetKeyboardEvents(){return this.isInEditMode()}enterInEditMode(){this.enableEditMode();this.editorDiv.focus()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enterInEditMode();t.preventDefault()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freetextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}get canChangeContent(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy||this.annotationElementId){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#Ga);this.editorDiv.setAttribute("data-l10n-id","pdfjs-free-text2");this.editorDiv.setAttribute("data-l10n-attrs","default-content");this.enableEditing();this.editorDiv.contentEditable=!0;const{style:i}=this.editorDiv;i.fontSize=`calc(${this.#va}px * var(--total-scale-factor))`;i.color=this.color;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);if(this._isCopy||this.annotationElementId){const[i,s]=this.parentDimensions;if(this.annotationElementId){const{position:n}=this._initialData;let[r,a]=this.getInitialTranslation();[r,a]=this.pageTranslationToScreen(r,a);const[o,l]=this.pageDimensions,[h,d]=this.pageTranslation;let c,u;switch(this.rotation){case 0:c=t+(n[0]-h)/o;u=e+this.height-(n[1]-d)/l;break;case 90:c=t+(n[0]-h)/o;u=e-(n[1]-d)/l;[r,a]=[a,-r];break;case 180:c=t-this.width+(n[0]-h)/o;u=e-(n[1]-d)/l;[r,a]=[-r,-a];break;case 270:c=t+(n[0]-h-this.height*l)/o;u=e+(n[1]-d-this.width*o)/l;[r,a]=[-a,r]}this.setAt(c*i,u*s,r,a)}else this._moveAfterPaste(t,e);this.#qa();this._isDraggable=!0;this.editorDiv.contentEditable=!1}else{this._isDraggable=!1;this.editorDiv.contentEditable=!0}return this.div}static#Xa(t){return(t.nodeType===Node.TEXT_NODE?t.nodeValue:t.innerText).replaceAll(Nt,"")}editorDivPaste(t){const e=t.clipboardData||window.clipboardData,{types:i}=e;if(1===i.length&&"text/plain"===i[0])return;t.preventDefault();const s=FreeTextEditor.#Ka(e.getData("text")||"").replaceAll(Nt,"\n");if(!s)return;const n=window.getSelection();if(!n.rangeCount)return;this.editorDiv.normalize();n.deleteFromDocument();const r=n.getRangeAt(0);if(!s.includes("\n")){r.insertNode(document.createTextNode(s));this.editorDiv.normalize();n.collapseToStart();return}const{startContainer:a,startOffset:o}=r,l=[],h=[];if(a.nodeType===Node.TEXT_NODE){const t=a.parentElement;h.push(a.nodeValue.slice(o).replaceAll(Nt,""));if(t!==this.editorDiv){let e=l;for(const i of this.editorDiv.childNodes)i!==t?e.push(FreeTextEditor.#Xa(i)):e=h}l.push(a.nodeValue.slice(0,o).replaceAll(Nt,""))}else if(a===this.editorDiv){let t=l,e=0;for(const i of this.editorDiv.childNodes){e++===o&&(t=h);t.push(FreeTextEditor.#Xa(i))}}this.#za=`${l.join("\n")}${s}${h.join("\n")}`;this.#qa();const d=new Range;let c=Math.sumPrecise(l.map(t=>t.length));for(const{firstChild:t}of this.editorDiv.childNodes)if(t.nodeType===Node.TEXT_NODE){const e=t.nodeValue.length;if(c<=e){d.setStart(t,c);d.setEnd(t,c);break}c-=e}n.removeAllRanges();n.addRange(d)}#qa(){this.editorDiv.replaceChildren();if(this.#za)for(const t of this.#za.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}}#Ya(){return this.#za.replaceAll(" "," ")}static#Ka(t){return t.replaceAll(" "," ")}get contentDiv(){return this.editorDiv}getPDFRect(){const t=FreeTextEditor._internalPadding*this.parentScale;return this.getRect(t,t)}static async deserialize(t,e,i){let s=null;if(t instanceof FreeTextAnnotationElement){const{data:{defaultAppearanceData:{fontSize:e,fontColor:i},rect:n,rotation:r,id:a,popupRef:o,richText:l,contentsObj:h,creationDate:d,modificationDate:c},textContent:u,textPosition:p,parent:{page:{pageNumber:g}}}=t;if(!u||0===u.length)return null;s=t={annotationType:m.FREETEXT,color:Array.from(i),fontSize:e,value:u.join("\n"),position:p,pageIndex:g-1,rect:n.slice(0),rotation:r,annotationElementId:a,id:a,deleted:!1,popupRef:o,comment:h?.str||null,richText:l,creationDate:d,modificationDate:c}}const n=await super.deserialize(t,e,i);n.#va=t.fontSize;n.color=Util.makeHexColor(...t.color);n.#za=FreeTextEditor.#Ka(t.value);n._initialData=s;t.comment&&n.setCommentData(t);return n}serialize(t=!1){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const e=AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.color),i=Object.assign(super.serialize(t),{color:e,fontSize:this.#va,value:this.#Ya()});this.addComment(i);if(t){i.isCopy=!0;return i}if(this.annotationElementId&&!this.#Qa(i))return null;i.id=this.annotationElementId;return i}#Qa(t){const{value:e,fontSize:i,color:s,pageIndex:n}=this._initialData;return this.hasEditedComment||this._hasBeenMoved||t.value!==e||t.fontSize!==i||t.color.some((t,e)=>t!==s[e])||t.pageIndex!==n}renderAnnotationElement(t){const e=super.renderAnnotationElement(t);if(!e)return null;const{style:i}=e;i.fontSize=`calc(${this.#va}px * var(--total-scale-factor))`;i.color=this.color;e.replaceChildren();for(const t of this.#za.split("\n")){const i=document.createElement("div");i.append(t?document.createTextNode(t):document.createElement("br"));e.append(i)}t.updateEdited({rect:this.getPDFRect(),popup:this._uiManager.hasCommentManager()||this.hasEditedComment?this.comment:{text:this.#za}});return e}resetAnnotationElement(t){super.resetAnnotationElement(t);t.resetEdited()}}class Outline{static PRECISION=1e-4;toSVGPath(){unreachable("Abstract method `toSVGPath` must be implemented.")}get box(){unreachable("Abstract getter `box` must be implemented.")}serialize(t,e){unreachable("Abstract method `serialize` must be implemented.")}static _rescale(t,e,i,s,n,r){r||=new Float32Array(t.length);for(let a=0,o=t.length;a=6;t-=6)isNaN(e[t])?i.push(`L${e[t+4]} ${e[t+5]}`):i.push(`C${e[t]} ${e[t+1]} ${e[t+2]} ${e[t+3]} ${e[t+4]} ${e[t+5]}`);this.#bo(i);return i.join(" ")}#mo(){const[t,e,i,s]=this.#Ja,[n,r,a,o]=this.#go();return`M${(this.#so[2]-t)/i} ${(this.#so[3]-e)/s} L${(this.#so[4]-t)/i} ${(this.#so[5]-e)/s} L${n} ${r} L${a} ${o} L${(this.#so[16]-t)/i} ${(this.#so[17]-e)/s} L${(this.#so[14]-t)/i} ${(this.#so[15]-e)/s} Z`}#bo(t){const e=this.#Za;t.push(`L${e[4]} ${e[5]} Z`)}#fo(t){const[e,i,s,n]=this.#Ja,r=this.#so.subarray(4,6),a=this.#so.subarray(16,18),[o,l,h,d]=this.#go();t.push(`L${(r[0]-e)/s} ${(r[1]-i)/n} L${o} ${l} L${h} ${d} L${(a[0]-e)/s} ${(a[1]-i)/n}`)}newFreeDrawOutline(t,e,i,s,n,r){return new FreeDrawOutline(t,e,i,s,n,r)}getOutlines(){const t=this.#io,e=this.#Za,i=this.#so,[s,n,r,a]=this.#Ja,o=new Float32Array((this.#do?.length??0)+2);for(let t=0,e=o.length-2;t=6;t-=6)for(let i=0;i<6;i+=2)if(isNaN(e[t+i])){l[h]=l[h+1]=NaN;h+=2}else{l[h]=e[t+i];l[h+1]=e[t+i+1];h+=2}this.#vo(l,h);return this.newFreeDrawOutline(l,o,this.#Ja,this.#lo,this.#to,this.#eo)}#yo(t){const e=this.#so,[i,s,n,r]=this.#Ja,[a,o,l,h]=this.#go(),d=new Float32Array(36);d.set([NaN,NaN,NaN,NaN,(e[2]-i)/n,(e[3]-s)/r,NaN,NaN,NaN,NaN,(e[4]-i)/n,(e[5]-s)/r,NaN,NaN,NaN,NaN,a,o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,(e[16]-i)/n,(e[17]-s)/r,NaN,NaN,NaN,NaN,(e[14]-i)/n,(e[15]-s)/r],0);return this.newFreeDrawOutline(d,t,this.#Ja,this.#lo,this.#to,this.#eo)}#vo(t,e){const i=this.#Za;t.set([NaN,NaN,NaN,NaN,i[4],i[5]],e);return e+6}#Ao(t,e){const i=this.#so.subarray(4,6),s=this.#so.subarray(16,18),[n,r,a,o]=this.#Ja,[l,h,d,c]=this.#go();t.set([NaN,NaN,NaN,NaN,(i[0]-n)/a,(i[1]-r)/o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,d,c,NaN,NaN,NaN,NaN,(s[0]-n)/a,(s[1]-r)/o],e);return e+24}}class FreeDrawOutline extends Outline{#Ja;#wo=new Float32Array(4);#to;#eo;#do;#lo;#xo;constructor(t,e,i,s,n,r){super();this.#xo=t;this.#do=e;this.#Ja=i;this.#lo=s;this.#to=n;this.#eo=r;this.firstPoint=[NaN,NaN];this.lastPoint=[NaN,NaN];this.#Co(r);const[a,o,l,h]=this.#wo;for(let e=0,i=t.length;ep){a=u;o=p}else o===p&&(a=d(a,u));if(hc[1]){a=c[0];o=c[1]}else o===c[1]&&(a=d(a,c[0]));if(ht[0]-e[0]||t[1]-e[1]||t[2]-e[2]);const t=[];for(const e of this.#So)if(e[3]){t.push(...this.#Po(e));this.#Do(e)}else{this.#Mo(e);t.push(...this.#Po(e))}return this.#ko(t)}#ko(t){const e=[],i=new Set;for(const i of t){const[t,s,n]=i;e.push([t,s,i],[t,n,i])}e.sort((t,e)=>t[1]-e[1]||t[0]-e[0]);for(let t=0,s=e.length;t0;){const t=i.values().next().value;let[e,r,a,o,l]=t;i.delete(t);let h=e,d=r;n=[e,a];s.push(n);for(;;){let t;if(i.has(o))t=o;else{if(!i.has(l))break;t=l}i.delete(t);[e,r,a,o,l]=t;if(h!==e){n.push(h,d,e,d===r?r:a);h=e}d=d===r?a:r}n.push(h,d)}return new HighlightOutline(s,this.#Ja,this.#Eo,this.#To)}#Io(t){const e=this.#_o;let i=0,s=e.length-1;for(;i<=s;){const n=i+s>>1,r=e[n][0];if(r===t)return n;r=0;s--){const[i,n]=this.#_o[s];if(i!==t)break;if(i===t&&n===e){this.#_o.splice(s,1);return}}}#Po(t){const[e,i,s]=t,n=[[e,i,s]],r=this.#Io(s);for(let t=0;t=i)if(o>s)n[t][1]=s;else{if(1===r)return[];n.splice(t,1);t--;r--}else{n[t][2]=i;o>s&&n.push([e,s,o])}}}return n}}class HighlightOutline extends Outline{#Ja;#Fo;constructor(t,e,i,s){super();this.#Fo=t;this.#Ja=e;this.firstPoint=i;this.lastPoint=s}toSVGPath(){const t=[];for(const e of this.#Fo){let[i,s]=e;t.push(`M${i} ${s}`);for(let n=2;n-1){this.#Wo=!0;this.#$o(t);this.#Xo()}else if(this.#Oo){this.#Bo=t.anchorNode;this.#Ro=t.anchorOffset;this.#Uo=t.focusNode;this.#Ho=t.focusOffset;this.#qo();this.#Xo();this.rotate(this.rotation)}this.annotationElementId||this._uiManager.a11yAlert("pdfjs-editor-highlight-added-alert")}get telemetryInitialData(){return{action:"added",type:this.#Wo?"free_highlight":"highlight",color:this._uiManager.getNonHCMColorName(this.color),thickness:this.#ho,methodOfCreation:this.#jo}}get telemetryFinalData(){return{type:"highlight",color:this._uiManager.getNonHCMColorName(this.color)}}static computeTelemetryFinalData(t){return{numberOfColors:t.get("color").size}}#qo(){const t=new HighlightOutliner(this.#Oo,.001);this.#Go=t.getOutlines();[this.x,this.y,this.width,this.height]=this.#Go.box;const e=new HighlightOutliner(this.#Oo,.0025,.001,"ltr"===this._uiManager.direction);this.#No=e.getOutlines();const{firstPoint:i}=this.#Go;this.#Eo=[(i[0]-this.x)/this.width,(i[1]-this.y)/this.height];const{lastPoint:s}=this.#No;this.#To=[(s[0]-this.x)/this.width,(s[1]-this.y)/this.height]}#$o({highlightOutlines:t,highlightId:e,clipPathId:i}){this.#Go=t;this.#No=t.getNewOutline(this.#ho/2+1.5,.0025);if(e>=0){this.#S=e;this.#Lo=i;this.parent.drawLayer.finalizeDraw(e,{bbox:t.box,path:{d:t.toSVGPath()}});this.#Vo=this.parent.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:!0},bbox:this.#No.box,path:{d:this.#No.toSVGPath()}},!0)}else if(this.parent){const e=this.parent.viewport.rotation;this.parent.drawLayer.updateProperties(this.#S,{bbox:HighlightEditor.#Ko(this.#Go.box,(e-this.rotation+360)%360),path:{d:t.toSVGPath()}});this.parent.drawLayer.updateProperties(this.#Vo,{bbox:HighlightEditor.#Ko(this.#No.box,e),path:{d:this.#No.toSVGPath()}})}const[s,n,r,a]=t.box;switch(this.rotation){case 0:this.x=s;this.y=n;this.width=r;this.height=a;break;case 90:{const[t,e]=this.parentDimensions;this.x=n;this.y=1-s;this.width=r*e/t;this.height=a*t/e;break}case 180:this.x=1-s;this.y=1-n;this.width=r;this.height=a;break;case 270:{const[t,e]=this.parentDimensions;this.x=1-n;this.y=s;this.width=r*e/t;this.height=a*t/e;break}}const{firstPoint:o}=t;this.#Eo=[(o[0]-s)/r,(o[1]-n)/a];const{lastPoint:l}=this.#No;this.#To=[(l[0]-s)/r,(l[1]-n)/a]}static initialize(t,e){AnnotationEditor.initialize(t,e);HighlightEditor._defaultColor||=e.highlightColors?.values().next().value||"#fff066"}static updateDefaultParams(t,e){switch(t){case f.HIGHLIGHT_COLOR:HighlightEditor._defaultColor=e;break;case f.HIGHLIGHT_THICKNESS:HighlightEditor._defaultThickness=e}}translateInPage(t,e){}get toolbarPosition(){return this.#To}get commentButtonPosition(){return this.#Eo}updateParams(t,e){switch(t){case f.HIGHLIGHT_COLOR:this.#ba(e);break;case f.HIGHLIGHT_THICKNESS:this.#Yo(e)}}static get defaultPropertiesToUpdate(){return[[f.HIGHLIGHT_COLOR,HighlightEditor._defaultColor],[f.HIGHLIGHT_THICKNESS,HighlightEditor._defaultThickness]]}get propertiesToUpdate(){return[[f.HIGHLIGHT_COLOR,this.color||HighlightEditor._defaultColor],[f.HIGHLIGHT_THICKNESS,this.#ho||HighlightEditor._defaultThickness],[f.HIGHLIGHT_FREE,this.#Wo]]}onUpdatedColor(){this.parent?.drawLayer.updateProperties(this.#S,{root:{fill:this.color,"fill-opacity":this.opacity}});this.#r?.updateColor(this.color);super.onUpdatedColor()}#ba(t){const setColorAndOpacity=(t,e)=>{this.color=t;this.opacity=e;this.onUpdatedColor()},e=this.color,i=this.opacity;this.addCommands({cmd:setColorAndOpacity.bind(this,t,HighlightEditor._defaultOpacity),undo:setColorAndOpacity.bind(this,e,i),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:f.HIGHLIGHT_COLOR,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"color_changed",color:this._uiManager.getNonHCMColorName(t)},!0)}#Yo(t){const e=this.#ho,setThickness=t=>{this.#ho=t;this.#Qo(t)};this.addCommands({cmd:setThickness.bind(this,t),undo:setThickness.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:f.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"thickness_changed",thickness:t},!0)}get toolbarButtons(){if(this._uiManager.highlightColors){return[["colorPicker",this.#r=new ColorPicker({editor:this})]]}return super.toolbarButtons}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}fixAndSetPosition(){return super.fixAndSetPosition(this.#Jo())}getBaseTranslation(){return[0,0]}getRect(t,e){return super.getRect(t,e,this.#Jo())}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);t&&this.div.focus()}remove(){this.#Zo();this._reportTelemetry({action:"deleted"});super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#Xo();this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t)this.#Zo();else if(t){this.#Xo(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);this.show(this._isVisible);e&&this.select()}#Qo(t){if(this.#Wo){this.#$o({highlightOutlines:this.#Go.getNewOutline(t/2)});this.fixAndSetPosition();this.setDims()}}#Zo(){if(null!==this.#S&&this.parent){this.parent.drawLayer.remove(this.#S);this.#S=null;this.parent.drawLayer.remove(this.#Vo);this.#Vo=null}}#Xo(t=this.parent){if(null===this.#S){({id:this.#S,clipPathId:this.#Lo}=t.drawLayer.draw({bbox:this.#Go.box,root:{viewBox:"0 0 1 1",fill:this.color,"fill-opacity":this.opacity},rootClass:{highlight:!0,free:this.#Wo},path:{d:this.#Go.toSVGPath()}},!1,!0));this.#Vo=t.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:this.#Wo},bbox:this.#No.box,path:{d:this.#No.toSVGPath()}},this.#Wo);this.#zo&&(this.#zo.style.clipPath=this.#Lo)}}static#Ko([t,e,i,s],n){switch(n){case 90:return[1-e-s,t,s,i];case 180:return[1-t-i,1-e-s,i,s];case 270:return[e,1-t-i,s,i]}return[t,e,i,s]}rotate(t){const{drawLayer:e}=this.parent;let i;if(this.#Wo){t=(t-this.rotation+360)%360;i=HighlightEditor.#Ko(this.#Go.box,t)}else i=HighlightEditor.#Ko([this.x,this.y,this.width,this.height],t);e.updateProperties(this.#S,{bbox:i,root:{"data-main-rotation":t}});e.updateProperties(this.#Vo,{bbox:HighlightEditor.#Ko(this.#No.box,t),root:{"data-main-rotation":t}})}render(){if(this.div)return this.div;const t=super.render();if(this.#be){t.setAttribute("aria-label",this.#be);t.setAttribute("role","mark")}this.#Wo?t.classList.add("free"):this.div.addEventListener("keydown",this.#tl.bind(this),{signal:this._uiManager._signal});const e=this.#zo=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";e.style.clipPath=this.#Lo;this.setDims();bindEvents(this,this.#zo,["pointerover","pointerleave"]);this.enableEditing();return t}pointerover(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Vo,{rootClass:{hovered:!0}})}pointerleave(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Vo,{rootClass:{hovered:!1}})}#tl(t){HighlightEditor._keyboardManager.exec(this,t)}_moveCaret(t){this.parent.unselect(this);switch(t){case 0:case 2:this.#el(!0);break;case 1:case 3:this.#el(!1)}}#el(t){if(!this.#Bo)return;const e=window.getSelection();t?e.setPosition(this.#Bo,this.#Ro):e.setPosition(this.#Uo,this.#Ho)}select(){super.select();this.#Vo&&this.parent?.drawLayer.updateProperties(this.#Vo,{rootClass:{hovered:!1,selected:!0}})}unselect(){super.unselect();if(this.#Vo){this.parent?.drawLayer.updateProperties(this.#Vo,{rootClass:{selected:!1}});this.#Wo||this.#el(!1)}}get _mustFixPosition(){return!this.#Wo}show(t=this._isVisible){super.show(t);if(this.parent){this.parent.drawLayer.updateProperties(this.#S,{rootClass:{hidden:!t}});this.parent.drawLayer.updateProperties(this.#Vo,{rootClass:{hidden:!t}})}}#Jo(){return this.#Wo?this.rotation:0}#il(){if(this.#Wo)return null;const[t,e]=this.pageDimensions,[i,s]=this.pageTranslation,n=this.#Oo,r=new Float32Array(8*n.length);let a=0;for(const{x:o,y:l,width:h,height:d}of n){const n=o*t+i,c=(1-l)*e+s;r[a]=r[a+4]=n;r[a+1]=r[a+3]=c;r[a+2]=r[a+6]=n+h*t;r[a+5]=r[a+7]=c-d*e;a+=8}return r}#sl(t){return this.#Go.serialize(t,this.#Jo())}static startHighlighting(t,e,{target:i,x:s,y:n}){const{x:r,y:a,width:o,height:l}=i.getBoundingClientRect(),h=new AbortController,d=t.combinedSignal(h),pointerUpCallback=e=>{h.abort();this.#nl(t,e)};window.addEventListener("blur",pointerUpCallback,{signal:d});window.addEventListener("pointerup",pointerUpCallback,{signal:d});window.addEventListener("pointerdown",stopEvent,{capture:!0,passive:!1,signal:d});window.addEventListener("contextmenu",noContextMenu,{signal:d});i.addEventListener("pointermove",this.#rl.bind(this,t),{signal:d});this._freeHighlight=new FreeHighlightOutliner({x:s,y:n},[r,a,o,l],t.scale,this._defaultThickness/2,e,.001);({id:this._freeHighlightId,clipPathId:this._freeHighlightClipId}=t.drawLayer.draw({bbox:[0,0,1,1],root:{viewBox:"0 0 1 1",fill:this._defaultColor,"fill-opacity":this._defaultOpacity},rootClass:{highlight:!0,free:!0},path:{d:this._freeHighlight.toSVGPath()}},!0,!0))}static#rl(t,e){this._freeHighlight.add(e)&&t.drawLayer.updateProperties(this._freeHighlightId,{path:{d:this._freeHighlight.toSVGPath()}})}static#nl(t,e){this._freeHighlight.isEmpty()?t.drawLayer.remove(this._freeHighlightId):t.createAndAddNewEditor(e,!1,{highlightId:this._freeHighlightId,highlightOutlines:this._freeHighlight.getOutlines(),clipPathId:this._freeHighlightClipId,methodOfCreation:"main_toolbar"});this._freeHighlightId=-1;this._freeHighlight=null;this._freeHighlightClipId=""}static async deserialize(t,e,i){let s=null;if(t instanceof HighlightAnnotationElement){const{data:{quadPoints:e,rect:i,rotation:n,id:r,color:a,opacity:o,popupRef:l,richText:h,contentsObj:d,creationDate:c,modificationDate:u},parent:{page:{pageNumber:p}}}=t;s=t={annotationType:m.HIGHLIGHT,color:Array.from(a),opacity:o,quadPoints:e,boxes:null,pageIndex:p-1,rect:i.slice(0),rotation:n,annotationElementId:r,id:r,deleted:!1,popupRef:l,richText:h,comment:d?.str||null,creationDate:c,modificationDate:u}}else if(t instanceof InkAnnotationElement){const{data:{inkLists:e,rect:i,rotation:n,id:r,color:a,borderStyle:{rawWidth:o},popupRef:l,richText:h,contentsObj:d,creationDate:c,modificationDate:u},parent:{page:{pageNumber:p}}}=t;s=t={annotationType:m.HIGHLIGHT,color:Array.from(a),thickness:o,inkLists:e,boxes:null,pageIndex:p-1,rect:i.slice(0),rotation:n,annotationElementId:r,id:r,deleted:!1,popupRef:l,richText:h,comment:d?.str||null,creationDate:c,modificationDate:u}}const{color:n,quadPoints:r,inkLists:a,outlines:o,opacity:l}=t,h=await super.deserialize(t,e,i);h.color=Util.makeHexColor(...n);h.opacity=l||1;a&&(h.#ho=t.thickness);h._initialData=s;t.comment&&h.setCommentData(t);const[d,c]=h.pageDimensions,[u,p]=h.pageTranslation;if(r){const t=h.#Oo=[];for(let e=0;et!==e[i])}renderAnnotationElement(t){if(this.deleted){t.hide();return null}t.updateEdited({rect:this.getPDFRect(),popup:this.comment});return null}static canCreateNewEmptyEditor(){return!1}}class DrawingOptions{#al=Object.create(null);updateProperty(t,e){this[t]=e;this.updateSVGProperty(t,e)}updateProperties(t){if(t)for(const[e,i]of Object.entries(t))e.startsWith("_")||this.updateProperty(e,i)}updateSVGProperty(t,e){this.#al[t]=e}toSVGProperties(){const t=this.#al;this.#al=Object.create(null);return{root:t}}reset(){this.#al=Object.create(null)}updateAll(t=this){this.updateProperties(t)}clone(){unreachable("Not implemented")}}class DrawingEditor extends AnnotationEditor{#ol=null;#ll;_colorPicker=null;_drawId=null;static _currentDrawId=-1;static _currentParent=null;static#hl=null;static#dl=null;static#cl=null;static _INNER_MARGIN=3;constructor(t){super(t);this.#ll=t.mustBeCommitted||!1;this._addOutlines(t)}onUpdatedColor(){this._colorPicker?.update(this.color);super.onUpdatedColor()}onUpdatedOpacity(){this._colorPicker?.updateOpacity?.(this.opacity)}_addOutlines(t){if(t.drawOutlines){this.#ul(t);this.#Xo()}}#ul({drawOutlines:t,drawId:e,drawingOptions:i}){this.#ol=t;this._drawingOptions||=i;this.annotationElementId||this._uiManager.a11yAlert(`pdfjs-editor-${this.editorType}-added-alert`);if(e>=0){this._drawId=e;this.parent.drawLayer.finalizeDraw(e,t.defaultProperties)}else this._drawId=this.#pl(t,this.parent);this.#gl(t.box)}#pl(t,e){const{id:i}=e.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(),t.defaultSVGProperties),!1,!1);return i}static _mergeSVGProperties(t,e){const i=new Set(Object.keys(t));for(const[s,n]of Object.entries(e))i.has(s)?Object.assign(t[s],n):t[s]=n;return t}static getDefaultDrawingOptions(t){unreachable("Not implemented")}static get typesMap(){unreachable("Not implemented")}static get isDrawer(){return!0}static get supportMultipleDrawings(){return!1}static updateDefaultParams(t,e){const i=this.typesMap.get(t);i&&this._defaultDrawingOptions.updateProperty(i,e);if(this._currentParent){DrawingEditor.#hl.updateProperty(i,e);this._currentParent.drawLayer.updateProperties(this._currentDrawId,this._defaultDrawingOptions.toSVGProperties())}}updateParams(t,e){const i=this.constructor.typesMap.get(t);i&&this._updateProperty(t,i,e)}static get defaultPropertiesToUpdate(){const t=[],e=this._defaultDrawingOptions;for(const[i,s]of this.typesMap)t.push([i,e[s]]);return t}get propertiesToUpdate(){const t=[],{_drawingOptions:e}=this;for(const[i,s]of this.constructor.typesMap)t.push([i,e[s]]);return t}_updateProperty(t,e,i){const s=this._drawingOptions,n=s[e],setter=i=>{s.updateProperty(e,i);const n=this.#ol.updateProperty(e,i);n&&this.#gl(n);this.parent?.drawLayer.updateProperties(this._drawId,s.toSVGProperties());t===this.colorType?this.onUpdatedColor():t===this.opacityType&&this.onUpdatedOpacity()};this.addCommands({cmd:setter.bind(this,i),undo:setter.bind(this,n),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:t,overwriteIfSameType:!0,keepUndo:!0})}_updateColorAndOpacity(t,e){const i=this.constructor.typesMap.get(this.colorType),s=this.constructor.typesMap.get(this.opacityType),n=this._drawingOptions,r=n[i],a=n[s],setter=(t,e)=>{n.updateProperty(i,t);n.updateProperty(s,e);this.#ol.updateProperty(i,t);this.#ol.updateProperty(s,e);this.parent?.drawLayer.updateProperties(this._drawId,n.toSVGProperties());this.onUpdatedColor();this.onUpdatedOpacity()};this.addCommands({cmd:setter.bind(this,t,e),undo:setter.bind(this,r,a),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:f.INK_COLOR_AND_OPACITY,overwriteIfSameType:!0,keepUndo:!0})}_onResizing(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#ol.getPathResizingSVGProperties(this.#ml()),{bbox:this.#fl()}))}_onResized(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#ol.getPathResizedSVGProperties(this.#ml()),{bbox:this.#fl()}))}_onTranslating(t,e){this.parent?.drawLayer.updateProperties(this._drawId,{bbox:this.#fl()})}_onTranslated(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#ol.getPathTranslatedSVGProperties(this.#ml(),this.parentDimensions),{bbox:this.#fl()}))}_onStartDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!0}})}_onStopDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!1}})}commit(){super.commit();this.disableEditMode();this.disableEditing()}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}getBaseTranslation(){return[0,0]}get isResizable(){return!0}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);this._isDraggable=!0;if(this.#ll){this.#ll=!1;this.commit();this.parent.setSelected(this);t&&this.isOnScreen&&this.div.focus()}}remove(){this.#Zo();super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#Xo();this.#gl(this.#ol.box);this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t){this._uiManager.removeShouldRescale(this);this.#Zo()}else if(t){this._uiManager.addShouldRescale(this);this.#Xo(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);e&&this.select()}#Zo(){if(null!==this._drawId&&this.parent){this.parent.drawLayer.remove(this._drawId);this._drawId=null;this._drawingOptions.reset()}}#Xo(t=this.parent){if(null===this._drawId||this.parent!==t)if(null===this._drawId){this._drawingOptions.updateAll();this._drawId=this.#pl(this.#ol,t)}else this.parent.drawLayer.updateParent(this._drawId,t.drawLayer)}#bl([t,e,i,s]){const{parentDimensions:[n,r],rotation:a}=this;switch(a){case 90:return[e,1-t,i*(r/n),s*(n/r)];case 180:return[1-t,1-e,i,s];case 270:return[1-e,t,i*(r/n),s*(n/r)];default:return[t,e,i,s]}}#ml(){const{x:t,y:e,width:i,height:s,parentDimensions:[n,r],rotation:a}=this;switch(a){case 90:return[1-e,t,i*(n/r),s*(r/n)];case 180:return[1-t,1-e,i,s];case 270:return[e,1-t,i*(n/r),s*(r/n)];default:return[t,e,i,s]}}#gl(t){[this.x,this.y,this.width,this.height]=this.#bl(t);if(this.div){this.fixAndSetPosition();this.setDims()}this._onResized()}#fl(){const{x:t,y:e,width:i,height:s,rotation:n,parentRotation:r,parentDimensions:[a,o]}=this;switch((4*n+r)/90){case 1:return[1-e-s,t,s,i];case 2:return[1-t-i,1-e-s,i,s];case 3:return[e,1-t-i,s,i];case 4:return[t,e-i*(a/o),s*(o/a),i*(a/o)];case 5:return[1-e,t,i*(a/o),s*(o/a)];case 6:return[1-t-s*(o/a),1-e,s*(o/a),i*(a/o)];case 7:return[e-i*(a/o),1-t-s*(o/a),i*(a/o),s*(o/a)];case 8:return[t-i,e-s,i,s];case 9:return[1-e,t-i,s,i];case 10:return[1-t,1-e,i,s];case 11:return[e-s,1-t,s,i];case 12:return[t-s*(o/a),e,s*(o/a),i*(a/o)];case 13:return[1-e-i*(a/o),t-s*(o/a),i*(a/o),s*(o/a)];case 14:return[1-t,1-e-i*(a/o),s*(o/a),i*(a/o)];case 15:return[e,1-t,i*(a/o),s*(o/a)];default:return[t,e,i,s]}}rotate(){this.parent&&this.parent.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties({bbox:this.#fl()},this.#ol.updateRotation((this.parentRotation-this.rotation+360)%360)))}onScaleChanging(){this.parent&&this.#gl(this.#ol.updateParentDimensions(this.parentDimensions,this.parent.scale))}static onScaleChangingWhenDrawing(){}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}const i=super.render();i.classList.add("draw");const s=document.createElement("div");i.append(s);s.setAttribute("aria-hidden","true");s.className="internal";this.setDims();this._uiManager.addShouldRescale(this);this.disableEditing();this._isCopy&&this._moveAfterPaste(t,e);return i}static createDrawerInstance(t,e,i,s,n){unreachable("Not implemented")}static startDrawing(t,e,i,s){const{target:n,offsetX:r,offsetY:a,pointerId:o,pointerType:l}=s;if(CurrentPointers.isInitializedAndDifferentPointerType(l))return;const{viewport:{rotation:h}}=t,{width:d,height:c}=n.getBoundingClientRect(),u=DrawingEditor.#dl=new AbortController,p=t.combinedSignal(u);CurrentPointers.setPointer(l,o);window.addEventListener("pointerup",t=>{CurrentPointers.isSamePointerIdOrRemove(t.pointerId)&&this._endDraw(t)},{signal:p});window.addEventListener("pointercancel",t=>{CurrentPointers.isSamePointerIdOrRemove(t.pointerId)&&this._currentParent.endDrawingSession()},{signal:p});window.addEventListener("pointerdown",t=>{if(CurrentPointers.isSamePointerType(t.pointerType)){CurrentPointers.initializeAndAddPointerId(t.pointerId);if(DrawingEditor.#hl.isCancellable()){DrawingEditor.#hl.removeLastElement();DrawingEditor.#hl.isEmpty()?this._currentParent.endDrawingSession(!0):this._endDraw(null)}}},{capture:!0,passive:!1,signal:p});window.addEventListener("contextmenu",noContextMenu,{signal:p});n.addEventListener("pointermove",this._drawMove.bind(this),{signal:p});n.addEventListener("touchmove",t=>{CurrentPointers.isSameTimeStamp(t.timeStamp)&&stopEvent(t)},{signal:p});t.toggleDrawing();e._editorUndoBar?.hide();if(DrawingEditor.#hl)t.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#hl.startNew(r,a,d,c,h));else{e.updateUIForDefaultProperties(this);DrawingEditor.#hl=this.createDrawerInstance(r,a,d,c,h);DrawingEditor.#cl=this.getDefaultDrawingOptions();this._currentParent=t;({id:this._currentDrawId}=t.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#cl.toSVGProperties(),DrawingEditor.#hl.defaultSVGProperties),!0,!1))}}static _drawMove(t){CurrentPointers.isSameTimeStamp(t.timeStamp);if(!DrawingEditor.#hl)return;const{offsetX:e,offsetY:i,pointerId:s}=t;if(CurrentPointers.isSamePointerId(s))if(CurrentPointers.isUsingMultiplePointers())this._endDraw(t);else{this._currentParent.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#hl.add(e,i));CurrentPointers.setTimeStamp(t.timeStamp);stopEvent(t)}}static _cleanup(t){if(t){this._currentDrawId=-1;this._currentParent=null;DrawingEditor.#hl=null;DrawingEditor.#cl=null;CurrentPointers.clearTimeStamp()}if(DrawingEditor.#dl){DrawingEditor.#dl.abort();DrawingEditor.#dl=null;CurrentPointers.clearPointerIds()}}static _endDraw(t){const e=this._currentParent;if(e){e.toggleDrawing(!0);this._cleanup(!1);t?.target===e.div&&e.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#hl.end(t.offsetX,t.offsetY));if(this.supportMultipleDrawings){const t=DrawingEditor.#hl,i=this._currentDrawId,s=t.getLastElement();e.addCommands({cmd:()=>{e.drawLayer.updateProperties(i,t.setLastElement(s))},undo:()=>{e.drawLayer.updateProperties(i,t.removeLastElement())},mustExec:!1,type:f.DRAW_STEP});return}this.endDrawing(!1)}}static endDrawing(t){const e=this._currentParent;if(!e)return null;e.toggleDrawing(!0);e.cleanUndoStack(f.DRAW_STEP);if(!DrawingEditor.#hl.isEmpty()){const{pageDimensions:[i,s],scale:n}=e,r=e.createAndAddNewEditor({offsetX:0,offsetY:0},!1,{drawId:this._currentDrawId,drawOutlines:DrawingEditor.#hl.getOutlines(i*n,s*n,n,this._INNER_MARGIN),drawingOptions:DrawingEditor.#cl,mustBeCommitted:!t});this._cleanup(!0);return r}e.drawLayer.remove(this._currentDrawId);this._cleanup(!0);return null}createDrawingOptions(t){}static deserializeDraw(t,e,i,s,n,r){unreachable("Not implemented")}static async deserialize(t,e,i){const{rawDims:{pageWidth:s,pageHeight:n,pageX:r,pageY:a}}=e.viewport,o=this.deserializeDraw(r,a,s,n,this._INNER_MARGIN,t),l=await super.deserialize(t,e,i);l.createDrawingOptions(t);l.#ul({drawOutlines:o});l.#Xo();l.onScaleChanging();l.rotate();return l}serializeDraw(t){const[e,i]=this.pageTranslation,[s,n]=this.pageDimensions;return this.#ol.serialize([e,i,s,n],t)}renderAnnotationElement(t){t.updateEdited({rect:this.getPDFRect()});return null}static canCreateNewEmptyEditor(){return!1}}class InkDrawOutliner{#so=new Float64Array(6);#Ea;#yl;#In;#ho;#do;#Al="";#vl=0;#Fo=new InkDrawOutline;#wl;#xl;constructor(t,e,i,s,n,r){this.#wl=i;this.#xl=s;this.#In=n;this.#ho=r;[t,e]=this.#Cl(t,e);const a=this.#Ea=[NaN,NaN,NaN,NaN,t,e];this.#do=[t,e];this.#yl=[{line:a,points:this.#do}];this.#so.set(a,0)}updateProperty(t,e){"stroke-width"===t&&(this.#ho=e)}#Cl(t,e){return Outline._normalizePoint(t,e,this.#wl,this.#xl,this.#In)}isEmpty(){return!this.#yl||0===this.#yl.length}isCancellable(){return this.#do.length<=10}add(t,e){[t,e]=this.#Cl(t,e);const[i,s,n,r]=this.#so.subarray(2,6),a=t-n,o=e-r;if(Math.hypot(this.#wl*a,this.#xl*o)<=2)return null;this.#do.push(t,e);if(isNaN(i)){this.#so.set([n,r,t,e],2);this.#Ea.push(NaN,NaN,NaN,NaN,t,e);return{path:{d:this.toSVGPath()}}}isNaN(this.#so[0])&&this.#Ea.splice(6,6);this.#so.set([i,s,n,r,t,e],0);this.#Ea.push(...Outline.createBezierPoints(i,s,n,r,t,e));return{path:{d:this.toSVGPath()}}}end(t,e){const i=this.add(t,e);return i||(2===this.#do.length?{path:{d:this.toSVGPath()}}:null)}startNew(t,e,i,s,n){this.#wl=i;this.#xl=s;this.#In=n;[t,e]=this.#Cl(t,e);const r=this.#Ea=[NaN,NaN,NaN,NaN,t,e];this.#do=[t,e];const a=this.#yl.at(-1);if(a){a.line=new Float32Array(a.line);a.points=new Float32Array(a.points)}this.#yl.push({line:r,points:this.#do});this.#so.set(r,0);this.#vl=0;this.toSVGPath();return null}getLastElement(){return this.#yl.at(-1)}setLastElement(t){if(!this.#yl)return this.#Fo.setLastElement(t);this.#yl.push(t);this.#Ea=t.line;this.#do=t.points;this.#vl=0;return{path:{d:this.toSVGPath()}}}removeLastElement(){if(!this.#yl)return this.#Fo.removeLastElement();this.#yl.pop();this.#Al="";for(let t=0,e=this.#yl.length;tt??NaN),d,c,u,p),points:g(a[t].map(t=>t??NaN),d,c,u,p)});const m=new this.prototype.constructor;m.build(h,i,s,1,o,l,n);return m}#Pl(t=this.#ho){const e=this.#to+t/2*this.#Tl;return this.#In%180==0?[e/this.#wl,e/this.#xl]:[e/this.#xl,e/this.#wl]}#_l(){const[t,e,i,s]=this.#wo,[n,r]=this.#Pl(0);return[t+n,e+r,i-2*n,s-2*r]}#Sl(){const t=this.#wo=i.slice();for(const{line:e}of this.#yl){if(e.length<=12){for(let i=4,s=e.length;it!==e[i])||t.thickness!==i||t.opacity!==s||t.pageIndex!==n}renderAnnotationElement(t){if(this.deleted){t.hide();return null}const{points:e,rect:i}=this.serializeDraw(!1);t.updateEdited({rect:i,thickness:this._drawingOptions["stroke-width"],points:e,popup:this.comment});return null}}class ContourDrawOutline extends InkDrawOutline{toSVGPath(){let t=super.toSVGPath();t.endsWith("Z")||(t+="Z");return t}}class SignatureExtractor{static#Dl={maxDim:512,sigmaSFactor:.02,sigmaR:25,kernelSize:16};static#Ml(t,e,i,s){s-=e;return 0===(i-=t)?s>0?0:4:1===i?s+6:2-s}static#kl=new Int32Array([0,1,-1,1,-1,0,-1,-1,0,-1,1,-1,1,0,1,1]);static#Il(t,e,i,s,n,r,a){const o=this.#Ml(i,s,n,r);for(let n=0;n<8;n++){const r=(-n+o-a+16)%8;if(0!==t[(i+this.#kl[2*r])*e+(s+this.#kl[2*r+1])])return r}return-1}static#Fl(t,e,i,s,n,r,a){const o=this.#Ml(i,s,n,r);for(let n=0;n<8;n++){const r=(n+o+a+16)%8;if(0!==t[(i+this.#kl[2*r])*e+(s+this.#kl[2*r+1])])return r}return-1}static#Bl(t,e,i,s){const n=t.length,r=new Int32Array(n);for(let e=0;e=1&&0===r[s+1])){1!==n&&(a=Math.abs(n));continue}o+=1;d+=1;n>1&&(a=n)}const c=[i,t],u=d===i+1,p={isHole:u,points:c,id:o,parent:0};l.push(p);let g;for(const t of l)if(t.id===a){g=t;break}g?g.isHole?p.parent=u?g.parent:a:p.parent=u?a:g.parent:p.parent=u?a:0;const m=this.#Il(r,e,t,i,h,d,0);if(-1===m){r[s]=-o;1!==r[s]&&(a=Math.abs(r[s]));continue}let f=this.#kl[2*m],b=this.#kl[2*m+1];const y=t+f,A=i+b;h=y;d=A;let v=t,w=i;for(;;){const n=this.#Fl(r,e,v,w,h,d,1);f=this.#kl[2*n];b=this.#kl[2*n+1];const l=v+f,u=w+b;c.push(u,l);const p=v*e+w;0===r[p+1]?r[p]=-o:1===r[p]&&(r[p]=o);if(l===t&&u===i&&v===y&&w===A){1!==r[s]&&(a=Math.abs(r[s]));break}h=v;d=w;v=l;w=u}}}return l}static#Rl(t,e,i,s){if(i-e<=4){for(let n=e;nv){w=s;v=e}}if(v>(l*A)**2){this.#Rl(t,e,w+2,s);this.#Rl(t,w,i,s)}else s.push(n,r)}static#Ol(t){const e=[],i=t.length;this.#Rl(t,0,i,e);e.push(t[i-2],t[i-1]);return e.length<=4?null:e}static#Ll(t,e,i,s,n,r){const a=new Float32Array(r**2),o=-2*s**2,l=r>>1;for(let t=0;t=i))for(let i=0;i=e)continue;const p=t[u*e+s],m=a[o*r+i]*h[Math.abs(p-d)];c+=p*m;g+=m}}p[u[o]=Math.round(c/g)]++}return[u,p]}static#Nl(t){const e=new Uint32Array(256);for(const i of t)e[i]++;return e}static#Ul(t){const e=t.length,i=new Uint8ClampedArray(e>>2);let s=-1/0,n=1/0;for(let e=0,r=i.length;e0!==t);let r=n,a=n;for(e=n;e<256;e++){const n=t[e];if(n>i){if(e-r>s){s=e-r;a=e-1}i=n;r=e}}for(e=a-1;e>=0&&!(t[e]>t[e+1]);e--);return e}static#zl(t){const e=t,{width:i,height:s}=t,{maxDim:n}=this.#Dl;let r=i,a=s;if(i>n||s>n){let o=i,l=s,h=Math.log2(Math.max(i,s)/n);const d=Math.floor(h);h=h===d?d-1:d;for(let i=0;i=-128&&o<=127?Int8Array:a>=-32768&&o<=32767?Int16Array:Int32Array;const h=t.length,d=8+3*h,c=new Uint32Array(d);let u=0;c[u++]=d*Uint32Array.BYTES_PER_ELEMENT+(l-2*h)*r.BYTES_PER_ELEMENT;c[u++]=0;c[u++]=s;c[u++]=n;c[u++]=e?0:1;c[u++]=Math.max(0,Math.floor(i??0));c[u++]=h;c[u++]=r.BYTES_PER_ELEMENT;for(const e of t){c[u++]=e.length-2;c[u++]=e[0];c[u++]=e[1]}const p=new CompressionStream("deflate-raw"),g=p.writable.getWriter();await g.ready;g.write(c);const m=r.prototype.constructor;for(const e of t){const t=new m(e.length-2);for(let i=2,s=e.length;i{await n.ready;await n.close()}).catch(()=>{});let r=null,a=0;for await(const t of i){r||=new Uint8Array(new Uint32Array(t.buffer,0,4)[0]);r.set(t,a);a+=t.length}const o=new Uint32Array(r.buffer,0,r.length>>2),l=o[1];if(0!==l)throw new Error(`Invalid version: ${l}`);const h=o[2],d=o[3],c=0===o[4],u=o[5],p=o[6],g=o[7],m=[],f=(8+3*p)*Uint32Array.BYTES_PER_ELEMENT;let b;switch(g){case Int8Array.BYTES_PER_ELEMENT:b=new Int8Array(r.buffer,f);break;case Int16Array.BYTES_PER_ELEMENT:b=new Int16Array(r.buffer,f);break;case Int32Array.BYTES_PER_ELEMENT:b=new Int32Array(r.buffer,f)}a=0;for(let t=0;t{e?.updateEditSignatureButton(t)})}}getSignaturePreview(){const{newCurves:t,areContours:e,thickness:i,width:s,height:n}=this.#Vl,r=Math.max(s,n);return{areContours:e,outline:SignatureExtractor.processDrawnLines({lines:{curves:t.map(t=>({points:t})),thickness:i,width:s,height:n},pageWidth:r,pageHeight:r,rotation:0,innerMargin:0,mustSmooth:!1,areContours:e}).outline}}get toolbarButtons(){return this._uiManager.signatureManager?[["editSignature",this._uiManager.signatureManager]]:super.toolbarButtons}addSignature(t,e,i,s){const{x:n,y:r}=this,{outline:a}=this.#Vl=t;this.#Gl=a instanceof ContourDrawOutline;this.description=i;let o;if(this.#Gl)o=SignatureEditor.getDefaultDrawingOptions();else{o=SignatureEditor._defaultDrawnSignatureOptions.clone();o.updateProperties({"stroke-width":a.thickness})}this._addOutlines({drawOutlines:a,drawingOptions:o});const[,l]=this.pageDimensions;let h=e/l;h=h>=1?.5:h;this.width*=h/this.height;if(this.width>=1){h*=.9/this.width;this.width=.9}this.height=h;this.setDims();this.x=n;this.y=r;this.center();this._onResized();this.onScaleChanging();this.rotate();this._uiManager.addToAnnotationStorage(this);this.setUuid(s);this._reportTelemetry({action:"pdfjs.signature.inserted",data:{hasBeenSaved:!!s,hasDescription:!!i}});this.div.hidden=!1}getFromImage(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.process(t,e,i,s,SignatureEditor._INNER_MARGIN)}getFromText(t,e){const{rawDims:{pageWidth:i,pageHeight:s},rotation:n}=this.parent.viewport;return SignatureExtractor.extractContoursFromText(t,e,i,s,n,SignatureEditor._INNER_MARGIN)}getDrawnSignature(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.processDrawnLines({lines:t,pageWidth:e,pageHeight:i,rotation:s,innerMargin:SignatureEditor._INNER_MARGIN,mustSmooth:!1,areContours:!1})}createDrawingOptions({areContours:t,thickness:e}){if(t)this._drawingOptions=SignatureEditor.getDefaultDrawingOptions();else{this._drawingOptions=SignatureEditor._defaultDrawnSignatureOptions.clone();this._drawingOptions.updateProperties({"stroke-width":e})}}serialize(t=!1){if(this.isEmpty())return null;const{lines:e,points:i}=this.serializeDraw(t),{_drawingOptions:{"stroke-width":s}}=this,n=Object.assign(super.serialize(t),{isSignature:!0,areContours:this.#Gl,color:[0,0,0],thickness:this.#Gl?0:s});this.addComment(n);if(t){n.paths={lines:e,points:i};n.uuid=this.#jl;n.isCopy=!0}else n.lines=e;this.#Wl&&(n.accessibilityData={type:"Figure",alt:this.#Wl});return n}static deserializeDraw(t,e,i,s,n,r){return r.areContours?ContourDrawOutline.deserialize(t,e,i,s,n,r):InkDrawOutline.deserialize(t,e,i,s,n,r)}static async deserialize(t,e,i){const s=await super.deserialize(t,e,i);s.#Gl=t.areContours;s.description=t.accessibilityData?.alt||"";s.#jl=t.uuid;return s}}class StampEditor extends AnnotationEditor{#$l=null;#Xl=null;#ql=null;#Kl=null;#Yl=null;#Ql="";#Jl=null;#Zl=!1;#th=null;#eh=!1;#ih=!1;static _type="stamp";static _editorType=m.STAMP;constructor(t){super({...t,name:"stampEditor"});this.#Kl=t.bitmapUrl;this.#Yl=t.bitmapFile;this.defaultL10nId="pdfjs-editor-stamp-editor"}static initialize(t,e){AnnotationEditor.initialize(t,e)}static isHandlingMimeForPasting(t){return j.includes(t)}static paste(t,e){e.pasteEditor({mode:m.STAMP},{bitmapFile:t.getAsFile()})}altTextFinish(){this._uiManager.useNewAltTextFlow&&(this.div.hidden=!1);super.altTextFinish()}get telemetryFinalData(){return{type:"stamp",hasAltText:!!this.altTextData?.altText}}static computeTelemetryFinalData(t){const e=t.get("hasAltText");return{hasAltText:e.get(!0)??0,hasNoAltText:e.get(!1)??0}}#sh(t,e=!1){if(t){this.#$l=t.bitmap;if(!e){this.#Xl=t.id;this.#eh=t.isSvg}t.file&&(this.#Ql=t.file.name);this.#nh()}else this.remove()}#rh(){this.#ql=null;this._uiManager.enableWaiting(!1);if(this.#Jl)if(this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#$l)this.addEditToolbar().then(()=>{this._editToolbar.hide();this._uiManager.editAltText(this,!0)});else{if(!this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#$l){this._reportTelemetry({action:"pdfjs.image.image_added",data:{alt_text_modal:!1,alt_text_type:"empty"}});try{this.mlGuessAltText()}catch{}}this.div.focus()}}async mlGuessAltText(t=null,e=!0){if(this.hasAltTextData())return null;const{mlManager:i}=this._uiManager;if(!i)throw new Error("No ML.");if(!await i.isEnabledFor("altText"))throw new Error("ML isn't enabled for alt text.");const{data:s,width:n,height:r}=t||this.copyCanvas(null,null,!0).imageData,a=await i.guess({name:"altText",request:{data:s,width:n,height:r,channels:s.length/(n*r)}});if(!a)throw new Error("No response from the AI service.");if(a.error)throw new Error("Error from the AI service.");if(a.cancel)return null;if(!a.output)throw new Error("No valid response from the AI service.");const o=a.output;await this.setGuessedAltText(o);e&&!this.hasAltTextData()&&(this.altTextData={alt:o,decorative:!1});return o}#ah(){if(this.#Xl){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#Xl).then(t=>this.#sh(t,!0)).finally(()=>this.#rh());return}if(this.#Kl){const t=this.#Kl;this.#Kl=null;this._uiManager.enableWaiting(!0);this.#ql=this._uiManager.imageManager.getFromUrl(t).then(t=>this.#sh(t)).finally(()=>this.#rh());return}if(this.#Yl){const t=this.#Yl;this.#Yl=null;this._uiManager.enableWaiting(!0);this.#ql=this._uiManager.imageManager.getFromFile(t).then(t=>this.#sh(t)).finally(()=>this.#rh());return}const t=document.createElement("input");t.type="file";t.accept=j.join(",");const e=this._uiManager._signal;this.#ql=new Promise(i=>{t.addEventListener("change",async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this._reportTelemetry({action:"pdfjs.image.image_selected",data:{alt_text_modal:this._uiManager.useNewAltTextFlow}});this.#sh(e)}else this.remove();i()},{signal:e});t.addEventListener("cancel",()=>{this.remove();i()},{signal:e})}).finally(()=>this.#rh());t.click()}remove(){if(this.#Xl){this.#$l=null;this._uiManager.imageManager.deleteId(this.#Xl);this.#Jl?.remove();this.#Jl=null;if(this.#th){clearTimeout(this.#th);this.#th=null}}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#Xl&&null===this.#Jl&&this.#ah();this.isAttachedToDOM||this.parent.add(this)}}else this.#Xl&&this.#ah()}onceAdded(t){this._isDraggable=!0;t&&this.div.focus()}isEmpty(){return!(this.#ql||this.#$l||this.#Kl||this.#Yl||this.#Xl||this.#Zl)}get toolbarButtons(){return[["altText",this.createAltText()]]}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.createAltText();this.#Zl||(this.#$l?this.#nh():this.#ah());this._isCopy&&this._moveAfterPaste(t,e);this._uiManager.addShouldRescale(this);return this.div}setCanvas(t,e){const{id:i,bitmap:s}=this._uiManager.imageManager.getFromCanvas(t,e);e.remove();if(i&&this._uiManager.imageManager.isValidId(i)){this.#Xl=i;s&&(this.#$l=s);this.#Zl=!1;this.#nh()}}_onResized(){this.onScaleChanging()}onScaleChanging(){if(!this.parent)return;null!==this.#th&&clearTimeout(this.#th);this.#th=setTimeout(()=>{this.#th=null;this.#oh()},200)}#nh(){const{div:t}=this;let{width:e,height:i}=this.#$l;const[s,n]=this.pageDimensions,r=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>r*s||i>r*n){const t=Math.min(r*s/e,r*n/i);e*=t;i*=t}this._uiManager.enableWaiting(!1);const a=this.#Jl=document.createElement("canvas");a.setAttribute("role","img");this.addContainer(a);this.width=e/s;this.height=i/n;this.setDims();this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&!this.annotationElementId||(t.hidden=!1);this.#oh();if(!this.#ih){this.parent.addUndoableEditor(this);this.#ih=!0}this._reportTelemetry({action:"inserted_image"});this.#Ql&&this.div.setAttribute("aria-description",this.#Ql);this.annotationElementId||this._uiManager.a11yAlert("pdfjs-editor-stamp-added-alert")}copyCanvas(t,e,i=!1){t||(t=224);const{width:s,height:n}=this.#$l,r=new OutputScale;let a=this.#$l,o=s,l=n,h=null;if(e){if(s>e||n>e){const t=Math.min(e/s,e/n);o=Math.floor(s*t);l=Math.floor(n*t)}h=document.createElement("canvas");const t=h.width=Math.ceil(o*r.sx),i=h.height=Math.ceil(l*r.sy);this.#eh||(a=this.#lh(t,i));const d=h.getContext("2d");d.filter=this._uiManager.hcmFilter;let c="white",u="#cfcfd8";if("none"!==this._uiManager.hcmFilter)u="black";else if(ColorScheme.isDarkMode){c="#8f8f9d";u="#42414d"}const p=15,g=p*r.sx,m=p*r.sy,f=new OffscreenCanvas(2*g,2*m),b=f.getContext("2d");b.fillStyle=c;b.fillRect(0,0,2*g,2*m);b.fillStyle=u;b.fillRect(0,0,g,m);b.fillRect(g,m,g,m);d.fillStyle=d.createPattern(f,"repeat");d.fillRect(0,0,t,i);d.drawImage(a,0,0,a.width,a.height,0,0,t,i)}let d=null;if(i){let e,i;if(r.symmetric&&a.widtht||n>t){const r=Math.min(t/s,t/n);e=Math.floor(s*r);i=Math.floor(n*r);this.#eh||(a=this.#lh(e,i))}}const o=new OffscreenCanvas(e,i).getContext("2d",{willReadFrequently:!0});o.drawImage(a,0,0,a.width,a.height,0,0,e,i);d={width:e,height:i,data:o.getImageData(0,0,e,i).data}}return{canvas:h,width:o,height:l,imageData:d}}#lh(t,e){const{width:i,height:s}=this.#$l;let n=i,r=s,a=this.#$l;for(;n>2*t||r>2*e;){const i=n,s=r;n>2*t&&(n=Math.ceil(n/2));r>2*e&&(r=Math.ceil(r/2));const o=new OffscreenCanvas(n,r);o.getContext("2d").drawImage(a,0,0,i,s,0,0,n,r);a=o.transferToImageBitmap()}return a}#oh(){const[t,e]=this.parentDimensions,{width:i,height:s}=this,n=new OutputScale,r=Math.ceil(i*t*n.sx),a=Math.ceil(s*e*n.sy),o=this.#Jl;if(!o||o.width===r&&o.height===a)return;o.width=r;o.height=a;const l=this.#eh?this.#$l:this.#lh(r,a),h=o.getContext("2d");h.filter=this._uiManager.hcmFilter;h.drawImage(l,0,0,l.width,l.height,0,0,r,a)}#hh(t){if(t){if(this.#eh){const t=this._uiManager.imageManager.getSvgUrl(this.#Xl);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#$l);t.getContext("2d").drawImage(this.#$l,0,0);return t.toDataURL()}if(this.#eh){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#$l,0,0,this.#$l.width,this.#$l.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#$l)}static async deserialize(t,e,i){let s=null,n=!1;if(t instanceof StampAnnotationElement){const{data:{rect:r,rotation:a,id:o,structParent:l,popupRef:h,richText:d,contentsObj:c,creationDate:u,modificationDate:p},container:g,parent:{page:{pageNumber:f}},canvas:b}=t;let y,A;if(b){delete t.canvas;({id:y,bitmap:A}=i.imageManager.getFromCanvas(g.id,b));b.remove()}else{n=!0;t._hasNoCanvas=!0}const v=(await e._structTree.getAriaAttributes(`${W}${o}`))?.get("aria-label")||"";s=t={annotationType:m.STAMP,bitmapId:y,bitmap:A,pageIndex:f-1,rect:r.slice(0),rotation:a,annotationElementId:o,id:o,deleted:!1,accessibilityData:{decorative:!1,altText:v},isSvg:!1,structParent:l,popupRef:h,richText:d,comment:c?.str||null,creationDate:u,modificationDate:p}}const r=await super.deserialize(t,e,i),{rect:a,bitmap:o,bitmapUrl:l,bitmapId:h,isSvg:d,accessibilityData:c}=t;if(n){i.addMissingCanvas(t.id,r);r.#Zl=!0}else if(h&&i.imageManager.isValidId(h)){r.#Xl=h;o&&(r.#$l=o)}else r.#Kl=l;r.#eh=d;const[u,p]=r.pageDimensions;r.width=(a[2]-a[0])/u;r.height=(a[3]-a[1])/p;c&&(r.altTextData=c);r._initialData=s;t.comment&&r.setCommentData(t);r.#ih=!!s;return r}serialize(t=!1,e=null){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const i=Object.assign(super.serialize(t),{bitmapId:this.#Xl,isSvg:this.#eh});this.addComment(i);if(t){i.bitmapUrl=this.#hh(!0);i.accessibilityData=this.serializeAltText(!0);i.isCopy=!0;return i}const{decorative:s,altText:n}=this.serializeAltText(!1);!s&&n&&(i.accessibilityData={type:"Figure",alt:n});if(this.annotationElementId){const t=this.#Qa(i);if(t.isSame)return null;t.isSameAltText?delete i.accessibilityData:i.accessibilityData.structParent=this._initialData.structParent??-1;i.id=this.annotationElementId;delete i.bitmapId;return i}if(null===e)return i;e.stamps||=new Map;const r=this.#eh?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#Xl)){if(this.#eh){const t=e.stamps.get(this.#Xl);if(r>t.area){t.area=r;t.serialized.bitmap.close();t.serialized.bitmap=this.#hh(!1)}}}else{e.stamps.set(this.#Xl,{area:r,serialized:i});i.bitmap=this.#hh(!1)}return i}#Qa(t){const{pageIndex:e,accessibilityData:{altText:i}}=this._initialData,s=t.pageIndex===e,n=(t.accessibilityData?.alt||"")===i;return{isSame:!this.hasEditedComment&&!this._hasBeenMoved&&!this._hasBeenResized&&s&&n,isSameAltText:n}}renderAnnotationElement(t){if(this.deleted){t.hide();return null}t.updateEdited({rect:this.getPDFRect(),popup:this.comment});return null}}class AnnotationEditorLayer{#Fa;#dh=!1;#ch=null;#uh=null;#ph=null;#gh=new Map;#mh=!1;#fh=!1;#bh=!1;#yh=null;#Ah=null;#vh=null;#wh=null;#xh=null;#Ch=-1;#y;static _initialized=!1;static#Q=new Map([FreeTextEditor,InkEditor,StampEditor,HighlightEditor,SignatureEditor].map(t=>[t._editorType,t]));constructor({uiManager:t,pageIndex:e,div:i,structTreeLayer:s,accessibilityManager:n,annotationLayer:r,drawLayer:a,textLayer:o,viewport:l,l10n:h}){const d=[...AnnotationEditorLayer.#Q.values()];if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;for(const e of d)e.initialize(h,t)}t.registerEditorTypes(d);this.#y=t;this.pageIndex=e;this.div=i;this.#Fa=n;this.#ch=r;this.viewport=l;this.#vh=o;this.drawLayer=a;this._structTree=s;this.#y.addLayer(this)}get isEmpty(){return 0===this.#gh.size}get isInvisible(){return this.isEmpty&&this.#y.getMode()===m.NONE}updateToolbar(t){this.#y.updateToolbar(t)}updateMode(t=this.#y.getMode()){this.#Eh();switch(t){case m.NONE:this.div.classList.toggle("nonEditing",!0);this.disableTextSelection();this.togglePointerEvents(!1);this.toggleAnnotationLayerPointerEvents(!0);this.disableClick();return;case m.INK:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick();break;case m.HIGHLIGHT:this.enableTextSelection();this.togglePointerEvents(!1);this.disableClick();break;default:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick()}this.toggleAnnotationLayerPointerEvents(!1);const{classList:e}=this.div;e.toggle("nonEditing",!1);if(t===m.POPUP)e.toggle("commentEditing",!0);else{e.toggle("commentEditing",!1);for(const i of AnnotationEditorLayer.#Q.values())e.toggle(`${i._type}Editing`,t===i._editorType)}this.div.hidden=!1}hasTextLayer(t){return t===this.#vh?.div}setEditingState(t){this.#y.setEditingState(t)}addCommands(t){this.#y.addCommands(t)}cleanUndoStack(t){this.#y.cleanUndoStack(t)}toggleDrawing(t=!1){this.div.classList.toggle("drawing",!t)}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}toggleAnnotationLayerPointerEvents(t=!1){this.#ch?.togglePointerEvents(t)}get#Th(){return 0!==this.#gh.size?this.#gh.values():this.#y.getEditors(this.pageIndex)}async enable(){this.#bh=!0;this.div.tabIndex=0;this.togglePointerEvents(!0);this.div.classList.toggle("nonEditing",!1);this.#xh?.abort();this.#xh=null;const t=new Set;for(const e of this.#Th){e.enableEditing();e.show(!0);if(e.annotationElementId){this.#y.removeChangedExistingAnnotation(e);t.add(e.annotationElementId)}}const e=this.#ch;if(e)for(const i of e.getEditableAnnotations()){i.hide();if(this.#y.isDeletedAnnotationElement(i.data.id))continue;if(t.has(i.data.id))continue;const e=await this.deserialize(i);if(e){this.addOrRebuild(e);e.enableEditing()}}this.#bh=!1;this.#y._eventBus.dispatch("editorsrendered",{source:this,pageNumber:this.pageIndex+1})}disable(){this.#fh=!0;this.div.tabIndex=-1;this.togglePointerEvents(!1);this.div.classList.toggle("nonEditing",!0);if(this.#vh&&!this.#xh){this.#xh=new AbortController;const t=this.#y.combinedSignal(this.#xh);this.#vh.div.addEventListener("pointerdown",t=>{const{clientX:e,clientY:i,timeStamp:s}=t;if(s-this.#Ch>500){this.#Ch=s;return}this.#Ch=-1;const{classList:n}=this.div;n.toggle("getElements",!0);const r=document.elementsFromPoint(e,i);n.toggle("getElements",!1);if(!this.div.contains(r[0]))return;let a;const o=new RegExp(`^${g}[0-9]+$`);for(const t of r)if(o.test(t.id)){a=t.id;break}if(!a)return;const l=this.#gh.get(a);if(null===l?.annotationElementId){stopEvent(t);l.dblclick(t)}},{signal:t,capture:!0})}const t=this.#ch,e=[];if(t){const i=new Map,s=new Map;for(const t of this.#Th){t.disableEditing();if(t.annotationElementId)if(null===t.serialize()){s.set(t.annotationElementId,t);this.getEditableAnnotation(t.annotationElementId)?.show();t.remove()}else i.set(t.annotationElementId,t);else e.push(t)}for(const e of t.getEditableAnnotations()){const{id:t}=e.data;if(this.#y.isDeletedAnnotationElement(t)){e.updateEdited({deleted:!0});continue}let n=s.get(t);if(n){n.resetAnnotationElement(e);n.show(!1);e.show()}else{n=i.get(t);if(n){this.#y.addChangedExistingAnnotation(n);n.renderAnnotationElement(e)&&n.show(!1)}e.show()}}}this.#Eh();this.isEmpty&&(this.div.hidden=!0);const{classList:i}=this.div;for(const t of AnnotationEditorLayer.#Q.values())i.remove(`${t._type}Editing`);this.disableTextSelection();this.toggleAnnotationLayerPointerEvents(!0);t?.updateFakeAnnotations(e);this.#fh=!1}getEditableAnnotation(t){return this.#ch?.getEditableAnnotation(t)||null}setActiveEditor(t){this.#y.getActive()!==t&&this.#y.setActiveEditor(t)}enableTextSelection(){this.div.tabIndex=-1;if(this.#vh?.div&&!this.#wh){this.#wh=new AbortController;const t=this.#y.combinedSignal(this.#wh);this.#vh.div.addEventListener("pointerdown",this.#Sh.bind(this),{signal:t});this.#vh.div.classList.add("highlighting")}}disableTextSelection(){this.div.tabIndex=0;if(this.#vh?.div&&this.#wh){this.#wh.abort();this.#wh=null;this.#vh.div.classList.remove("highlighting")}}#Sh(t){this.#y.unselectAll();const{target:e}=t;if(e===this.#vh.div||("img"===e.getAttribute("role")||e.classList.contains("endOfContent")||e.classList.contains("textLayerImagePlaceholder"))&&this.#vh.div.contains(e)){const{isMac:e}=FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;this.#y.showAllEditors("highlight",!0,!0);this.#vh.div.classList.add("free");this.toggleDrawing();HighlightEditor.startHighlighting(this,"ltr"===this.#y.direction,{target:this.#vh.div,x:t.x,y:t.y});this.#vh.div.addEventListener("pointerup",()=>{this.#vh.div.classList.remove("free");this.toggleDrawing(!0)},{once:!0,signal:this.#y._signal});t.preventDefault()}}enableClick(){if(this.#uh)return;this.#uh=new AbortController;const t=this.#y.combinedSignal(this.#uh);this.div.addEventListener("pointerdown",this.pointerdown.bind(this),{signal:t});const e=this.pointerup.bind(this);this.div.addEventListener("pointerup",e,{signal:t});this.div.addEventListener("pointercancel",e,{signal:t})}disableClick(){this.#uh?.abort();this.#uh=null}attach(t){this.#gh.set(t.id,t);const{annotationElementId:e}=t;e&&this.#y.isDeletedAnnotationElement(e)&&this.#y.removeDeletedAnnotationElement(t)}detach(t){this.#gh.delete(t.id);this.#Fa?.removePointerInTextLayer(t.contentDiv);!this.#fh&&t.annotationElementId&&this.#y.addDeletedAnnotationElement(t)}remove(t){this.detach(t);this.#y.removeEditor(t);t.div.remove();t.isAttachedToDOM=!1}changeParent(t){if(t.parent!==this){if(t.parent&&t.annotationElementId){this.#y.addDeletedAnnotationElement(t);AnnotationEditor.deleteAnnotationElement(t);t.annotationElementId=null}this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){if(t.parent!==this||!t.isAttachedToDOM){this.changeParent(t);this.#y.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}t.fixAndSetPosition();t.onceAdded(!this.#bh);this.#y.addToAnnotationStorage(t);t._reportTelemetry(t.telemetryInitialData)}}moveEditorInDOM(t){if(!t.isAttachedToDOM)return;const{activeElement:e}=document;if(t.div.contains(e)&&!this.#ph){t._focusEventsAllowed=!1;this.#ph=setTimeout(()=>{this.#ph=null;if(t.div.contains(document.activeElement))t._focusEventsAllowed=!0;else{t.div.addEventListener("focusin",()=>{t._focusEventsAllowed=!0},{once:!0,signal:this.#y._signal});e.focus()}},0)}t._structTreeParentId=this.#Fa?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){if(t.needsToBeRebuilt()){t.parent||=this;t.rebuild();t.show()}else this.add(t)}addUndoableEditor(t){this.addCommands({cmd:()=>t._uiManager.rebuild(t),undo:()=>{t.remove()},mustExec:!1})}getEditorByUID(t){for(const e of this.#gh.values())if(e.uid===t)return e;return null}get#_h(){return AnnotationEditorLayer.#Q.get(this.#y.getMode())}combinedSignal(t){return this.#y.combinedSignal(t)}#Ph(t){const e=this.#_h;return e?new e.prototype.constructor(t):null}canCreateNewEmptyEditor(){return this.#_h?.canCreateNewEmptyEditor()}async pasteEditor(t,e){this.updateToolbar(t);await this.#y.updateMode(t.mode);const{offsetX:i,offsetY:s}=this.#Dh(),n=this.#y.getId(),r=this.#Ph({parent:this,id:n,x:i,y:s,uiManager:this.#y,isCentered:!0,...e});r&&this.add(r)}async deserialize(t){return await(AnnotationEditorLayer.#Q.get(t.annotationType??t.annotationEditorType)?.deserialize(t,this,this.#y))||null}createAndAddNewEditor(t,e,i={}){const s=this.#y.getId(),n=this.#Ph({parent:this,id:s,x:t.offsetX,y:t.offsetY,uiManager:this.#y,isCentered:e,...i});n&&this.add(n);return n}get boundingClientRect(){return this.div.getBoundingClientRect()}#Dh(){const{x:t,y:e,width:i,height:s}=this.boundingClientRect,n=Math.max(0,t),r=Math.max(0,e),a=(n+Math.min(window.innerWidth,t+i))/2-t,o=(r+Math.min(window.innerHeight,e+s))/2-e,[l,h]=this.viewport.rotation%180==0?[a,o]:[o,a];return{offsetX:l,offsetY:h}}addNewEditor(t={}){this.createAndAddNewEditor(this.#Dh(),!0,t)}setSelected(t){this.#y.setSelected(t)}toggleSelected(t){this.#y.toggleSelected(t)}unselect(t){this.#y.unselect(t)}pointerup(t){const{isMac:e}=FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;if(!this.#mh)return;this.#mh=!1;if(this.#_h?.isDrawer&&this.#_h.supportMultipleDrawings)return;if(!this.#dh){this.#dh=!0;return}const i=this.#y.getMode();i!==m.STAMP&&i!==m.POPUP&&i!==m.SIGNATURE?this.createAndAddNewEditor(t,!1):this.#y.unselectAll()}pointerdown(t){this.#y.getMode()===m.HIGHLIGHT&&this.enableTextSelection();if(this.#mh){this.#mh=!1;return}const{isMac:e}=FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#mh=!0;if(this.#_h?.isDrawer){this.startDrawingSession(t);return}const i=this.#y.getActive();this.#dh=!i||i.isEmpty()}startDrawingSession(t){this.div.focus({preventScroll:!0});if(this.#yh){this.#_h.startDrawing(this,this.#y,!1,t);return}this.#y.setCurrentDrawingSession(this);this.#yh=new AbortController;const e=this.#y.combinedSignal(this.#yh);this.div.addEventListener("blur",({relatedTarget:t})=>{if(t&&!this.div.contains(t)){this.#Ah=null;this.commitOrRemove()}},{signal:e});this.#_h.startDrawing(this,this.#y,!1,t)}pause(t){if(t){const{activeElement:t}=document;this.div.contains(t)&&(this.#Ah=t);return}this.#Ah&&setTimeout(()=>{this.#Ah?.focus();this.#Ah=null},0)}endDrawingSession(t=!1){if(!this.#yh)return null;this.#y.setCurrentDrawingSession(null);this.#yh.abort();this.#yh=null;this.#Ah=null;return this.#_h.endDrawing(t)}findNewParent(t,e,i){const s=this.#y.findParent(e,i);if(null===s||s===this)return!1;s.changeParent(t);return!0}commitOrRemove(){if(this.#yh){this.endDrawingSession();return!0}return!1}onScaleChanging(){this.#yh&&this.#_h.onScaleChangingWhenDrawing(this)}destroy(){this.commitOrRemove();if(this.#y.getActive()?.parent===this){this.#y.commitOrRemove();this.#y.setActiveEditor(null)}if(this.#ph){clearTimeout(this.#ph);this.#ph=null}for(const t of this.#gh.values()){this.#Fa?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#gh.clear();this.#y.removeLayer(this)}#Eh(){for(const t of this.#gh.values())t.isEmpty()&&t.remove()}async render({viewport:t}){this.viewport=t;setLayerDimensions(this.div,t);for(const t of this.#y.getEditors(this.pageIndex)){this.add(t);t.rebuild()}await this.#y.findClonesForPage(this);this.div.hidden=this.isEmpty;this.updateMode()}update({viewport:t}){this.#y.commitOrRemove();this.#Eh();const e=this.viewport.rotation,i=t.rotation;this.viewport=t;setLayerDimensions(this.div,{rotation:i});if(e!==i)for(const t of this.#gh.values())t.rotate(i)}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}get scale(){return this.#y.viewParameters.realScale}}class DrawLayer{#sa=null;#Mh=new Map;#kh=new Map;static#S=0;setParent(t){if(this.#sa){if(this.#sa!==t){if(this.#Mh.size>0)for(const e of this.#Mh.values()){e.remove();t.append(e)}this.#sa=t}}else this.#sa=t}static get _svgFactory(){return shadow(this,"_svgFactory",new DOMSVGFactory)}static#Ih(t,[e,i,s,n]){const{style:r}=t;r.top=100*i+"%";r.left=100*e+"%";r.width=100*s+"%";r.height=100*n+"%"}#Fh(){const t=DrawLayer._svgFactory.create(1,1,!0);this.#sa.append(t);t.setAttribute("aria-hidden",!0);return t}#Bh(t,e){const i=DrawLayer._svgFactory.createElement("clipPath");t.append(i);const s=`clip_${e}`;i.setAttribute("id",s);i.setAttribute("clipPathUnits","objectBoundingBox");const n=DrawLayer._svgFactory.createElement("use");i.append(n);n.setAttribute("href",`#${e}`);n.classList.add("clip");return s}#Rh(t,e){for(const[i,s]of Object.entries(e))null===s?t.removeAttribute(i):t.setAttribute(i,s)}draw(t,e=!1,i=!1){const s=DrawLayer.#S++,n=this.#Fh(),r=DrawLayer._svgFactory.createElement("defs");n.append(r);const a=DrawLayer._svgFactory.createElement("path");r.append(a);const o=`path_${s}`;a.setAttribute("id",o);a.setAttribute("vector-effect","non-scaling-stroke");e&&this.#kh.set(s,a);const l=i?this.#Bh(r,o):null,h=DrawLayer._svgFactory.createElement("use");n.append(h);h.setAttribute("href",`#${o}`);this.updateProperties(n,t);this.#Mh.set(s,n);return{id:s,clipPathId:`url(#${l})`}}drawOutline(t,e){const i=DrawLayer.#S++,s=this.#Fh(),n=DrawLayer._svgFactory.createElement("defs");s.append(n);const r=DrawLayer._svgFactory.createElement("path");n.append(r);const a=`path_${i}`;r.setAttribute("id",a);r.setAttribute("vector-effect","non-scaling-stroke");let o;if(e){const t=DrawLayer._svgFactory.createElement("mask");n.append(t);o=`mask_${i}`;t.setAttribute("id",o);t.setAttribute("maskUnits","objectBoundingBox");const e=DrawLayer._svgFactory.createElement("rect");t.append(e);e.setAttribute("width","1");e.setAttribute("height","1");e.setAttribute("fill","white");const s=DrawLayer._svgFactory.createElement("use");t.append(s);s.setAttribute("href",`#${a}`);s.setAttribute("stroke","none");s.setAttribute("fill","black");s.setAttribute("fill-rule","nonzero");s.classList.add("mask")}const l=DrawLayer._svgFactory.createElement("use");s.append(l);l.setAttribute("href",`#${a}`);o&&l.setAttribute("mask",`url(#${o})`);const h=l.cloneNode();s.append(h);l.classList.add("mainOutline");h.classList.add("secondaryOutline");this.updateProperties(s,t);this.#Mh.set(i,s);return i}finalizeDraw(t,e){this.#kh.delete(t);this.updateProperties(t,e)}updateProperties(t,e){if(!e)return;const{root:i,bbox:s,rootClass:n,path:r}=e,a="number"==typeof t?this.#Mh.get(t):t;if(a){i&&this.#Rh(a,i);s&&DrawLayer.#Ih(a,s);if(n){const{classList:t}=a;for(const[e,i]of Object.entries(n))t.toggle(e,i)}if(r){const t=a.firstElementChild.firstElementChild;this.#Rh(t,r)}}}updateParent(t,e){if(e===this)return;const i=this.#Mh.get(t);if(i){e.#sa.append(i);this.#Mh.delete(t);e.#Mh.set(t,i)}}remove(t){this.#kh.delete(t);if(null!==this.#sa){this.#Mh.get(t).remove();this.#Mh.delete(t)}}destroy(){this.#sa=null;for(const t of this.#Mh.values())t.remove();this.#Mh.clear();this.#kh.clear()}}function percentage(t){return`${(100*t).toFixed(2)}%`}class TextLayerImages{#Oh=[];#Lh=new Map;#Nh=null;#Uh=0;#Dn=0;#Pn=0;static#Hh=null;constructor(t,e,i,s){this.#Uh=t;this.#Oh=e;this.#Dn=i.rawDims.pageWidth;this.#Pn=i.rawDims.pageHeight;this.#Nh=s}render(){const t=document.createElement("div");t.className="textLayerImages";for(let e=0;e{if(!(t.target instanceof HTMLCanvasElement))return;const e=t.target,i=this.#Lh.get(e);if(!i)return;const s=TextLayerImages.#Hh?.deref();if(s===e)return;if(s){s.width=0;s.height=0}TextLayerImages.#Hh=new WeakRef(e);const{inverseTransform:n,x1:r,y1:a,width:o,height:l}=i,h=this.#Nh(),d=Math.ceil(r*h.width),c=Math.ceil(a*h.height),u=Math.floor((r+o/this.#Dn)*h.width),p=Math.floor((a+l/this.#Pn)*h.height);e.width=u-d;e.height=p-c;const g=e.getContext("2d");g.setTransform(...n);g.translate(-d,-c);g.drawImage(h,0,0)});return t}#zh([t,e,i,s,n,r]){const a=Math.hypot((n-t)*this.#Dn,(r-e)*this.#Pn),o=Math.hypot((i-t)*this.#Dn,(s-e)*this.#Pn);if(a=ce&&console.info(`Info: ${e}`)}function warn(e){ta>=fe&&console.warn(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,a=null){if(!e)return null;if(a&&"string"==typeof e){if(a.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(a.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const n=t?URL.parse(e,t):URL.parse(e);return function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(n)?n:null}function shadow(e,t,a,n=!1){Object.defineProperty(e,t,{value:a,enumerable:!n,configurable:!0,writable:!1});return a}const aa=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends aa{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends aa{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends aa{constructor(e){super(e,"InvalidPDFException")}}class ResponseException extends aa{constructor(e,t,a){super(e,"ResponseException");this.status=t;this.missing=a}}class FormatError extends aa{constructor(e){super(e,"FormatError")}}class AbortException extends aa{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,a=8192;if(t{if("undefined"==typeof document)return!1;const e=document.createElement("input");e.type="color";e.setAttribute("alpha","");e.value="#ff000080";return"#ff0000"!==e.value})())}}class Util{static get hexNums(){return shadow(this,"hexNums",Array.from(Array(256).keys(),e=>e.toString(16).padStart(2,"0")))}static makeHexColor(e,t,a){return`#${this.hexNums[e]}${this.hexNums[t]}${this.hexNums[a]}`}static domMatrixToTransform(e){return[e.a,e.b,e.c,e.d,e.e,e.f]}static scaleMinMax(e,t){let a;if(e[0]){if(e[0]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[3];t[3]*=e[3]}else{a=t[0];t[0]=t[1];t[1]=a;a=t[2];t[2]=t[3];t[3]=a;if(e[1]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[1];t[3]*=e[1];if(e[2]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[2];t[2]*=e[2]}t[0]+=e[4];t[1]+=e[5];t[2]+=e[4];t[3]+=e[5]}static transform(e,t){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]]}static multiplyByDOMMatrix(e,t){return[e[0]*t.a+e[2]*t.b,e[1]*t.a+e[3]*t.b,e[0]*t.c+e[2]*t.d,e[1]*t.c+e[3]*t.d,e[0]*t.e+e[2]*t.f+e[4],e[1]*t.e+e[3]*t.f+e[5]]}static applyTransform(e,t,a=0){const n=e[a],s=e[a+1];e[a]=n*t[0]+s*t[2]+t[4];e[a+1]=n*t[1]+s*t[3]+t[5]}static applyTransformToBezier(e,t,a=0){const n=t[0],s=t[1],r=t[2],i=t[3],o=t[4],l=t[5];for(let t=0;t<6;t+=2){const f=e[a+t],c=e[a+t+1];e[a+t]=f*n+c*r+o;e[a+t+1]=f*s+c*i+l}}static applyInverseTransform(e,t){const a=e[0],n=e[1],s=t[0]*t[3]-t[1]*t[2];e[0]=(a*t[3]-n*t[2]+t[2]*t[5]-t[4]*t[3])/s;e[1]=(-a*t[1]+n*t[0]+t[4]*t[1]-t[5]*t[0])/s}static axialAlignedBoundingBox(e,t,a){const n=t[0],s=t[1],r=t[2],i=t[3],o=t[4],l=t[5],f=e[0],c=e[1],h=e[2],u=e[3];let m=n*f+o,p=m,d=n*h+o,g=d,b=i*c+l,w=b,j=i*u+l,k=j;if(0!==s||0!==r){const e=s*f,t=s*h,a=r*c,n=r*u;m+=a;g+=a;d+=n;p+=n;b+=e;k+=e;j+=t;w+=t}a[0]=Math.min(a[0],m,d,p,g);a[1]=Math.min(a[1],b,j,w,k);a[2]=Math.max(a[2],m,d,p,g);a[3]=Math.max(a[3],b,j,w,k)}static inverseTransform(e){const t=e[0]*e[3]-e[1]*e[2];return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[4]*e[3])/t,(e[4]*e[1]-e[5]*e[0])/t]}static singularValueDecompose2dScale(e,t){const a=e[0],n=e[1],s=e[2],r=e[3],i=a**2+n**2,o=a*s+n*r,l=s**2+r**2,f=(i+l)/2,c=Math.sqrt(f**2-(i*l-o**2));t[0]=Math.sqrt(f+c||1);t[1]=Math.sqrt(f-c||1)}static normalizeRect(e){const t=e.slice(0);if(e[0]>e[2]){t[0]=e[2];t[2]=e[0]}if(e[1]>e[3]){t[1]=e[3];t[3]=e[1]}return t}static intersect(e,t){const a=Math.max(Math.min(e[0],e[2]),Math.min(t[0],t[2])),n=Math.min(Math.max(e[0],e[2]),Math.max(t[0],t[2]));if(a>n)return null;const s=Math.max(Math.min(e[1],e[3]),Math.min(t[1],t[3])),r=Math.min(Math.max(e[1],e[3]),Math.max(t[1],t[3]));return s>r?null:[a,s,n,r]}static pointBoundingBox(e,t,a){a[0]=Math.min(a[0],e);a[1]=Math.min(a[1],t);a[2]=Math.max(a[2],e);a[3]=Math.max(a[3],t)}static rectBoundingBox(e,t,a,n,s){s[0]=Math.min(s[0],e,a);s[1]=Math.min(s[1],t,n);s[2]=Math.max(s[2],e,a);s[3]=Math.max(s[3],t,n)}static#e(e,t,a,n,s,r,i,o,l,f){if(l<=0||l>=1)return;const c=1-l,h=l*l,u=h*l,m=c*(c*(c*e+3*l*t)+3*h*a)+u*n,p=c*(c*(c*s+3*l*r)+3*h*i)+u*o;f[0]=Math.min(f[0],m);f[1]=Math.min(f[1],p);f[2]=Math.max(f[2],m);f[3]=Math.max(f[3],p)}static#t(e,t,a,n,s,r,i,o,l,f,c,h){if(Math.abs(l)<1e-12){Math.abs(f)>=1e-12&&this.#e(e,t,a,n,s,r,i,o,-c/f,h);return}const u=f**2-4*c*l;if(u<0)return;const m=Math.sqrt(u),p=2*l;this.#e(e,t,a,n,s,r,i,o,(-f+m)/p,h);this.#e(e,t,a,n,s,r,i,o,(-f-m)/p,h)}static bezierBoundingBox(e,t,a,n,s,r,i,o,l){l[0]=Math.min(l[0],e,i);l[1]=Math.min(l[1],t,o);l[2]=Math.max(l[2],e,i);l[3]=Math.max(l[3],t,o);this.#t(e,a,s,i,t,n,r,o,3*(3*(a-s)-e+i),6*(e-2*a+s),3*(a-e),l);this.#t(e,a,s,i,t,n,r,o,3*(3*(n-r)-t+o),6*(t-2*n+r),3*(n-t),l)}}const na=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,728,711,710,729,733,731,730,732,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8226,8224,8225,8230,8212,8211,402,8260,8249,8250,8722,8240,8222,8220,8221,8216,8217,8218,8482,64257,64258,321,338,352,376,381,305,322,339,353,382,0,8364];function stringToPDFString(e,t=!1){if(e[0]>="ï"){let a;if("þ"===e[0]&&"ÿ"===e[1]){a="utf-16be";e.length%2==1&&(e=e.slice(0,-1))}else if("ÿ"===e[0]&&"þ"===e[1]){a="utf-16le";e.length%2==1&&(e=e.slice(0,-1))}else"ï"===e[0]&&"»"===e[1]&&"¿"===e[2]&&(a="utf-8");if(a)try{const n=new TextDecoder(a,{fatal:!0}),s=stringToBytes(e),r=n.decode(s);return t||!r.includes("")?r:r.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g,"")}catch(e){warn(`stringToPDFString: "${e}".`)}}const a=[];for(let n=0,s=e.length;n[],makeMap=()=>new Map,makeObj=()=>Object.create(null),ia=Symbol("CIRCULAR_REF"),oa=Symbol("EOF");let la=Object.create(null),fa=Object.create(null),ca=Object.create(null);class Name{constructor(e){this.name=e}static get(e){return fa[e]||=new Name(e)}}class Cmd{constructor(e){this.cmd=e}static get(e){return la[e]||=new Cmd(e)}}const ha=function nonSerializableClosure(){return ha};class Dict{__nonSerializable__=ha;#a=new Map;objId=null;suppressEncryption=!1;xref;constructor(e=null){this.xref=e}assignXref(e){this.xref=e}get size(){return this.#a.size}#n(e,t,a,n){let s=this.#a.get(t);if(void 0===s&&void 0!==a){s=this.#a.get(a);void 0===s&&void 0!==n&&(s=this.#a.get(n))}return s instanceof Ref&&this.xref?e?this.xref.fetchAsync(s,this.suppressEncryption):this.xref.fetch(s,this.suppressEncryption):s}get(e,t,a){return this.#n(!1,e,t,a)}async getAsync(e,t,a){return this.#n(!0,e,t,a)}getArray(e,t,a){let n=this.#n(!1,e,t,a);if(Array.isArray(n)){n=n.slice();for(let e=0,t=n.length;e{unreachable("Should not call `set` on the empty dictionary.")};return shadow(this,"empty",e)}static merge({xref:e,dictArray:t,mergeSubDicts:a=!1}){const n=new Dict(e),s=new Map;for(const e of t)if(e instanceof Dict)for(const[t,n]of e.getRawEntries()){let e=s.get(t);if(void 0===e){e=[];s.set(t,e)}else if(!(a&&n instanceof Dict))continue;e.push(n)}for(const[t,a]of s){if(1===a.length||!(a[0]instanceof Dict)){n.set(t,a[0]);continue}const s=new Dict(e);for(const e of a)for(const[t,a]of e.getRawEntries())s.setIfNotExists(t,a);s.size>0&&n.set(t,s)}s.clear();return n.size>0?n:Dict.empty}clone(){const e=new Dict(this.xref);for(const[t,a]of this.#a)e.set(t,a);return e}delete(e){this.#a.delete(e)}}class Ref{constructor(e,t){this.num=e;this.gen=t}toString(){return 0===this.gen?`${this.num}R`:`${this.num}R${this.gen}`}static fromString(e){const t=ca[e];if(t)return t;const a=/^(\d+)R(\d*)$/.exec(e);return a&&"0"!==a[1]?ca[e]=new Ref(parseInt(a[1],10),a[2]?parseInt(a[2],10):0):null}static get(e,t){const a=0===t?`${e}R`:`${e}R${t}`;return ca[a]||=new Ref(e,t)}}class RefSet{constructor(e=null){this._set=new Set(e?._set)}has(e){return this._set.has(e.toString())}put(e){this._set.add(e.toString())}remove(e){this._set.delete(e.toString())}[Symbol.iterator](){return this._set.values()}clear(){this._set.clear()}}class RefSetCache{_map=new Map;get size(){return this._map.size}get(e){return this._map.get(e.toString())}has(e){return this._map.has(e.toString())}put(e,t){this._map.set(e.toString(),t)}putAlias(e,t){this._map.set(e.toString(),this.get(t))}[Symbol.iterator](){return this._map.values()}clear(){this._map.clear()}*values(){yield*this._map.values()}*items(){for(const[e,t]of this._map)yield[Ref.fromString(e),t]}*keys(){for(const e of this._map.keys())yield Ref.fromString(e)}}function isName(e,t){return e instanceof Name&&(void 0===t||e.name===t)}function isCmd(e,t){return e instanceof Cmd&&(void 0===t||e.cmd===t)}function isDict(e,t){return e instanceof Dict&&(void 0===t||isName(e.get("Type"),t))}function isRefsEqual(e,t){return e.num===t.num&&e.gen===t.gen}class BaseStream{get length(){unreachable("Abstract getter `length` accessed")}get isEmpty(){unreachable("Abstract getter `isEmpty` accessed")}get isDataLoaded(){return shadow(this,"isDataLoaded",!0)}getByte(){unreachable("Abstract method `getByte` called")}getBytes(e){unreachable("Abstract method `getBytes` called")}async getImageData(e,t){return this.getBytes(e,t)}async asyncGetBytes(){unreachable("Abstract method `asyncGetBytes` called")}get isAsync(){return!1}get isAsyncDecoder(){return!1}get isImageStream(){return!1}get canAsyncDecodeImageFromBuffer(){return!1}async getTransferableImage(){return null}peekByte(){const e=this.getByte();-1!==e&&this.pos--;return e}peekBytes(e){const t=this.getBytes(e);this.pos-=t.length;return t}getUint16(){const e=this.getByte(),t=this.getByte();return-1===e||-1===t?-1:(e<<8)+t}getInt32(){return(this.getByte()<<24)+(this.getByte()<<16)+(this.getByte()<<8)+this.getByte()}getByteRange(e,t){unreachable("Abstract method `getByteRange` called")}getString(e){return bytesToString(this.getBytes(e))}skip(e){this.pos+=e||1}reset(){unreachable("Abstract method `reset` called")}moveStart(){unreachable("Abstract method `moveStart` called")}makeSubStream(e,t,a=null){unreachable("Abstract method `makeSubStream` called")}getBaseStreams(){return null}getOriginalStream(){return this.stream?.getOriginalStream()||this}}const ua=/^[1-9]\.\d$/,ma=2**31-1,pa=[1,0,0,1,0,0],da=["ColorSpace","ExtGState","Font","Pattern","Properties","Shading","XObject"],ga=["ExtGState","Font","Properties","XObject"];function getLookupTableFactory(e){let t;return function(){if(e){t=Object.create(null);e(t);e=null}return t}}class MissingDataException extends aa{constructor(e,t){super(`Missing data [${e}, ${t})`,"MissingDataException");this.begin=e;this.end=t}}class ParserEOFException extends aa{constructor(e){super(e,"ParserEOFException")}}class XRefEntryException extends aa{constructor(e){super(e,"XRefEntryException")}}class XRefParseException extends aa{constructor(e){super(e,"XRefParseException")}}function arrayBuffersToBytes(e){const t=e.length;if(0===t)return new Uint8Array(0);if(1===t)return new Uint8Array(e[0]);let a=0;for(let n=0;n0,"The number should be a positive integer.");const a="M".repeat(e/1e3|0)+ba[e%1e3/100|0]+ba[10+(e%100/10|0)]+ba[20+e%10];return t?a.toLowerCase():a}function isWhiteSpace(e){return 32===e||9===e||13===e||10===e}function isNumberArray(e,t){return Array.isArray(e)?(null===t||e.length===t)&&e.every(e=>"number"==typeof e):ArrayBuffer.isView(e)&&!(e instanceof BigInt64Array||e instanceof BigUint64Array)&&(null===t||e.length===t)}function lookupMatrix(e,t){return isNumberArray(e,6)?e:t}function lookupRect(e,t){return isNumberArray(e,4)?e:t}function lookupNormalRect(e,t){return isNumberArray(e,4)?Util.normalizeRect(e):t}function parseXFAPath(e){const t=/(.+)\[(\d+)\]$/;return e.split(".").map(e=>{const a=e.match(t);return a?{name:a[1],pos:parseInt(a[2],10)}:{name:e,pos:0}})}function escapePDFName(e){const t=[];let a=0;for(let n=0,s=e.length;n126||35===s||40===s||41===s||60===s||62===s||91===s||93===s||123===s||125===s||47===s||37===s){a"\n"===e?"\\n":"\r"===e?"\\r":`\\${e}`)}function _collectJS(e,t,a,n){if(!e)return;let s=null;if(e instanceof Ref){if(n.has(e))return;s=e;n.put(s);e=t.fetch(e)}if(Array.isArray(e))for(const s of e)_collectJS(s,t,a,n);else if(e instanceof Dict){if(isName(e.get("S"),"JavaScript")){const t=e.get("JS");let n;t instanceof BaseStream?n=t.getString():"string"==typeof t&&(n=t);n&&=stringToPDFString(n,!0).replaceAll("\0","");n&&a.push(n.trim())}_collectJS(e.getRaw("Next"),t,a,n)}s&&n.remove(s)}function collectActions(e,t,a){const n=Object.create(null),s=getInheritableProperty({dict:t,key:"AA",stopWhenFound:!1});if(s)for(let t=s.length-1;t>=0;t--){const r=s[t];if(r instanceof Dict)for(const[t,s]of r.getRawEntries()){const r=a[t];if(!r)continue;const i=[];_collectJS(s,e,i,new RefSet);i.length>0&&(n[r]=i)}}if(t.has("A")){const a=[];_collectJS(t.get("A"),e,a,new RefSet);a.length>0&&(n.Action=a)}return objectSize(n)>0?n:null}const wa={60:"<",62:">",38:"&",34:""",39:"'"};function*codePointIter(e){for(let t=0,a=e.length;t55295&&(a<57344||a>65533)&&t++;yield a}}function encodeToXmlString(e){const t=[];let a=0;for(let n=0,s=e.length;n55295&&(s<57344||s>65533)&&n++;a=n+1}}if(0===t.length)return e;a: ${e}.`);return!1}return!0}function validateCSSFont(e){const t=new Set(["100","200","300","400","500","600","700","800","900","1000","normal","bold","bolder","lighter"]),{fontFamily:a,fontWeight:n,italicAngle:s}=e;if(!validateFontName(a,!0))return!1;const r=n?n.toString():"";e.fontWeight=t.has(r)?r:"400";const i=parseFloat(s);e.italicAngle=isNaN(i)||i<-90||i>90?"14":s.toString();return!0}function recoverJsURL(e){const t=new RegExp("^\\s*("+["app.launchURL","window.open","xfa.host.gotoURL"].join("|").replaceAll(".","\\.")+")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))","i").exec(e);return t?.[2]?{url:t[2],newWindow:"app.launchURL"===t[1]&&"true"===t[3]}:null}function numberToString(e){if(Number.isInteger(e))return e.toString();const t=Math.round(100*e);return t%100==0?(t/100).toString():t%10==0?e.toFixed(1):e.toFixed(2)}function getNewAnnotationsMap(e){if(!e)return null;const t=new Map;for(const[a,n]of e)a.startsWith(d)&&t.getOrInsertComputed(n.pageIndex,makeArr).push(n);return t.size>0?t:null}function stringToAsciiOrUTF16BE(e){return null==e||function isAscii(e){if("string"!=typeof e)return!1;return!e||/^[\x00-\x7F]*$/.test(e)}(e)?e:stringToUTF16String(e,!0)}function stringToUTF16HexString(e){const t=[];for(let a=0,n=e.length;a>8&255],Util.hexNums[255&n])}return t.join("")}function stringToUTF16String(e,t=!1){const a=[];t&&a.push("þÿ");for(let t=0,n=e.length;t>8&255),String.fromCharCode(255&n))}return a.join("")}function getRotationMatrix(e,t,a){switch(e){case 90:return[0,1,-1,0,t,0];case 180:return[-1,0,0,-1,t,a];case 270:return[0,-1,1,0,0,a];default:throw new Error("Invalid rotation")}}function getSizeInBytes(e){return Math.ceil(Math.ceil(Math.log2(1+e))/8)}class QCMS{static#s=null;static _memory=null;static _mustAddAlpha=!1;static _destBuffer=null;static _destOffset=0;static _destLength=0;static _cssColor="";static _makeHexColor=null;static get _memoryArray(){const e=this.#s;return e?.byteLength?e:this.#s=new Uint8Array(this._memory.buffer)}}const ja=Object.freeze({RGB8:0,0:"RGB8",RGBA8:1,1:"RGBA8",BGRA8:2,2:"BGRA8",Gray8:3,3:"Gray8",GrayA8:4,4:"GrayA8",CMYK:5,5:"CMYK"}),ka=Object.freeze({Perceptual:0,0:"Perceptual",RelativeColorimetric:1,1:"RelativeColorimetric",Saturation:2,2:"Saturation",AbsoluteColorimetric:3,3:"AbsoluteColorimetric"});function __wbg_get_imports(){return{__proto__:null,"./qcms_bg.js":{__proto__:null,__wbg___wbindgen_throw_6b64449b9b9ed33c:function(e,t){throw new Error(function getStringFromWasm0(e,t){return function decodeText(e,t){Sa+=t;if(Sa>=va){qa=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});qa.decode();Sa=t}return qa.decode(getUint8ArrayMemory0().subarray(e,e+t))}(e>>>=0,t)}(e,t))},__wbg_copy_result_0d15f3bf9d9012ae:function(e,t){!function copy_result(e,t){const{_mustAddAlpha:a,_destBuffer:n,_destOffset:s,_destLength:r,_memoryArray:i}=QCMS;if(t!==r)if(a)for(let a=e,r=e+t,o=s;a>>0,t>>>0)},__wbg_copy_rgb_0106d9d9464fce43:function(e){!function copy_rgb(e){const{_destBuffer:t,_destOffset:a,_memoryArray:n}=QCMS;t[a]=n[e];t[a+1]=n[e+1];t[a+2]=n[e+2]}(e>>>0)},__wbg_make_cssRGB_8e24b34f71f5363e:function(e){!function make_cssRGB(e){const{_memoryArray:t}=QCMS;QCMS._cssColor=QCMS._makeHexColor(t[e],t[e+1],t[e+2])}(e>>>0)},__wbindgen_init_externref_table:function(){const e=xa.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0);e.set(t+0,void 0);e.set(t+1,null);e.set(t+2,!0);e.set(t+3,!1)}}}}let ya=null;function getUint8ArrayMemory0(){null!==ya&&0!==ya.byteLength||(ya=new Uint8Array(xa.memory.buffer));return ya}function passArray8ToWasm0(e,t){const a=t(1*e.length,1)>>>0;getUint8ArrayMemory0().set(e,a/1);Ca=e.length;return a}let qa=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});qa.decode();const va=2146435072;let Sa=0;let Aa,xa,Ca=0;function __wbg_finalize_init(e,t){xa=e.exports;Aa=t;ya=null;xa.__wbindgen_start();return xa}function MathClamp(e,t,a){return Math.min(Math.max(e,t),a)}function isDefaultDecodeHelper(e,t){if(!Array.isArray(e))return!0;const a=e.length;if(at){info("Truncating too long decode map.");e.length=t}return!1}class ColorSpace{static#r=new Uint8ClampedArray(3);constructor(e,t){this.name=e;this.numComps=t}getRgb(e,t,a=new Uint8ClampedArray(3)){this.getRgbItem(e,t,a,0);return a}getRgbHex(e,t){const a=this.getRgb(e,t,ColorSpace.#r);return Util.makeHexColor(a[0],a[1],a[2])}getRgbItem(e,t,a,n){unreachable("Should not call ColorSpace.getRgbItem")}getRgbBuffer(e,t,a,n,s,r,i){unreachable("Should not call ColorSpace.getRgbBuffer")}getOutputLength(e,t){unreachable("Should not call ColorSpace.getOutputLength")}isPassthrough(e){return!1}isDefaultDecode(e,t){return ColorSpace.isDefaultDecode(e,this.numComps)}fillRgb(e,t,a,n,s,r,i,o,l){const f=t*a;let c=null;const h=1<h&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=i<=8?new Uint8Array(h):new Uint16Array(h);for(let e=0;e=.99554525?1:MathClamp(1.055*e**(1/2.4)-.055,0,1)}#j(e){return e<0?-this.#j(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#p}#k(e,t,a){if(0===e[0]&&0===e[1]&&0===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const n=this.#j(0),s=(1-n)/(1-this.#j(e[0])),r=1-s,i=(1-n)/(1-this.#j(e[1])),o=1-i,l=(1-n)/(1-this.#j(e[2])),f=1-l;a[0]=t[0]*s+r;a[1]=t[1]*i+o;a[2]=t[2]*l+f}#y(e,t,a){if(1===e[0]&&1===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const n=a;this.#d(CalRGBCS.#o,t,n);const s=CalRGBCS.#h;this.#g(e,n,s);this.#d(CalRGBCS.#l,s,a)}#q(e,t,a){const n=a;this.#d(CalRGBCS.#o,t,n);const s=CalRGBCS.#h;this.#b(e,n,s);this.#d(CalRGBCS.#l,s,a)}#i(e,t,a,n,s){const r=MathClamp(e[t]*s,0,1),i=MathClamp(e[t+1]*s,0,1),o=MathClamp(e[t+2]*s,0,1),l=1===r?1:r**this.GR,f=1===i?1:i**this.GG,c=1===o?1:o**this.GB,h=this.MXA*l+this.MXB*f+this.MXC*c,u=this.MYA*l+this.MYB*f+this.MYC*c,m=this.MZA*l+this.MZB*f+this.MZC*c,p=CalRGBCS.#u;p[0]=h;p[1]=u;p[2]=m;const d=CalRGBCS.#m;this.#y(this.whitePoint,p,d);const g=CalRGBCS.#u;this.#k(this.blackPoint,d,g);const b=CalRGBCS.#m;this.#q(CalRGBCS.#c,g,b);const w=CalRGBCS.#u;this.#d(CalRGBCS.#f,b,w);a[n]=255*this.#w(w[0]);a[n+1]=255*this.#w(w[1]);a[n+2]=255*this.#w(w[2])}getRgbItem(e,t,a,n){this.#i(e,t,a,n,1)}getRgbBuffer(e,t,a,n,s,r,i){const o=1/((1<this.amax||this.bmin>this.bmax){info("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#v(e){return e>=6/29?e**3:108/841*(e-4/29)}#S(e,t,a,n){return a+e*(n-a)/t}#i(e,t,a,n,s){let r=e[t],i=e[t+1],o=e[t+2];if(!1!==a){r=this.#S(r,a,0,100);i=this.#S(i,a,this.amin,this.amax);o=this.#S(o,a,this.bmin,this.bmax)}i>this.amax?i=this.amax:ithis.bmax?o=this.bmax:ofunction qcms_convert_one(e,t,a){xa.qcms_convert_one(e,t,a)}(this.#A,255*e[t],a);break;case 3:n=ja.RGB8;this.#x=(e,t,a)=>function qcms_convert_three(e,t,a,n,s){xa.qcms_convert_three(e,t,a,n,s)}(this.#A,255*e[t],255*e[t+1],255*e[t+2],a);break;case 4:n=ja.CMYK;this.#x=(e,t,a)=>function qcms_convert_four(e,t,a,n,s,r){xa.qcms_convert_four(e,t,a,n,s,r)}(this.#A,255*e[t],255*e[t+1],255*e[t+2],255*e[t+3],a);break;default:throw new Error(`Unsupported number of components: ${a}`)}this.#A=function qcms_transformer_from_memory(e,t,a){const n=passArray8ToWasm0(e,xa.__wbindgen_malloc),s=Ca;return xa.qcms_transformer_from_memory(n,s,t,a)>>>0}(e,n,ka.Perceptual);if(!this.#A)throw new Error("Failed to create ICC color space");IccColorSpace.#F||=new FinalizationRegistry(e=>{!function qcms_drop_transformer(e){xa.qcms_drop_transformer(e)}(e)});IccColorSpace.#F.register(this,this.#A)}getRgbHex(e,t){this.#x(e,t,!0);return QCMS._cssColor}getRgbItem(e,t,a,n){QCMS._destBuffer=a;QCMS._destOffset=n;QCMS._destLength=3;this.#x(e,t,!1);QCMS._destBuffer=null}getRgbBuffer(e,t,a,n,s,r,i){e=e.subarray(t,t+a*this.numComps);if(8!==r){const t=255/((1<=this.end?-1:this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,n=this.end;if(!e){this.pos=n;return t.subarray(a,n)}let s=a+e;s>n&&(s=n);this.pos=s;return t.subarray(a,s)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);return this.bytes.subarray(e,t)}reset(){this.pos=this.start}moveStart(){this.start=this.pos}makeSubStream(e,t,a=null){return new Stream(this.bytes.buffer,e,t,a)}clone(){return new Stream(this.bytes.buffer,this.start,this.end-this.start,this.dict.clone())}}class StringStream extends Stream{constructor(e){super(stringToBytes(e))}}class NullStream extends Stream{constructor(){super(new Uint8Array(0))}}class ChunkedStream extends Stream{progressiveDataLength=0;_lastSuccessfulEnsureByteChunk=-1;_loadedChunks=new Set;constructor(e,t,a){super(new Uint8Array(e),0,e,null);this.chunkSize=t;this.numChunks=Math.ceil(e/t);this.manager=a}getMissingChunks(){const e=[];for(let t=0,a=this.numChunks;t=this.end?this.numChunks:Math.floor(t/this.chunkSize);for(let e=a;ethis.numChunks)&&t!==this._lastSuccessfulEnsureByteChunk){if(!this._loadedChunks.has(t))throw new MissingDataException(e,e+1);this._lastSuccessfulEnsureByteChunk=t}}ensureRange(e,t){if(e>=t)return;if(t<=this.progressiveDataLength)return;const a=Math.floor(e/this.chunkSize);if(a>this.numChunks)return;const n=Math.min(Math.floor((t-1)/this.chunkSize)+1,this.numChunks);for(let s=a;s=this.end)return-1;e>=this.progressiveDataLength&&this.ensureByte(e);return this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,n=this.end;if(!e){n>this.progressiveDataLength&&this.ensureRange(a,n);this.pos=n;return t.subarray(a,n)}let s=a+e;s>n&&(s=n);s>this.progressiveDataLength&&this.ensureRange(a,s);this.pos=s;return t.subarray(a,s)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);t>this.progressiveDataLength&&this.ensureRange(e,t);return this.bytes.subarray(e,t)}makeSubStream(e,t,a=null){t?e+t>this.progressiveDataLength&&this.ensureRange(e,e+t):e>=this.progressiveDataLength&&this.ensureByte(e);function ChunkedStreamSubstream(){}ChunkedStreamSubstream.prototype=Object.create(this);ChunkedStreamSubstream.prototype.getMissingChunks=function(){const e=this.chunkSize,t=Math.floor(this.start/e),a=Math.floor((this.end-1)/e)+1,n=[];for(let e=t;e0){const e=this.groupChunks(s);for(const t of e){const e=t.beginChunk*this.chunkSize,a=Math.min(t.endChunk*this.chunkSize,this.length);this.sendRequest(e,a).catch(n.reject)}}return n.promise.catch(e=>{if(!this.aborted)throw e})}getStream(){return this.stream}requestRange(e,t){t=Math.min(t,this.length);const a=this.getBeginChunk(e),n=this.getEndChunk(t),s=[];for(let e=a;ee-t);return this._requestChunks(t)}groupChunks(e){const t=[];let a=-1,n=-1;for(let s=0,r=e.length;s=0&&n+1!==r){t.push({beginChunk:a,endChunk:n+1});a=r}s+1===e.length&&t.push({beginChunk:a,endChunk:r+1});n=r}return t}onReceiveData(e){const{chunkSize:t,length:a,stream:n}=this,s=e.chunk,r=void 0===e.begin,i=r?n.progressiveDataLength:e.begin,o=i+s.byteLength,l=Math.floor(i/t),f=o0||c.push(a)}}}if(!this.disableAutoFetch&&0===this._requestsByChunk.size){let e;if(1===n.numChunksLoaded){const t=n.numChunks-1;n.hasChunk(t)||(e=t)}else e=n.nextEmptyChunk(f);Number.isInteger(e)&&this._requestChunks([e])}for(const e of c){const t=this._promisesByRequest.get(e);this._promisesByRequest.delete(e);t.resolve()}this.msgHandler.send("DocProgress",{loaded:MathClamp(n.numChunksLoaded*t,n.progressiveDataLength,a),total:a})}onError(e){this._loadedStreamCapability.reject(e)}getBeginChunk(e){return Math.floor(e/this.chunkSize)}getEndChunk(e){return Math.floor((e-1)/this.chunkSize)+1}abort(e){this.aborted=!0;this.pdfStream?.cancelAllRequests(e);for(const t of this._promisesByRequest.values())t.reject(e)}}function convertToRGBA(e){switch(e.kind){case F:return convertBlackAndWhiteToRGBA(e);case T:return function convertRGBToRGBA({src:e,srcPos:t=0,dest:a,destPos:n=0,width:s,height:r}){let i=0;const o=s*r*3,l=o>>2,f=new Uint32Array(e.buffer,t,l),c=FeatureTest.isLittleEndian?4278190080:255;if(FeatureTest.isLittleEndian){for(;i>>24|t<<8|c;a[n+2]=t>>>16|s<<16|c;a[n+3]=s>>>8|c}for(let s=4*i,r=t+o;s>>8|c;a[n+2]=t<<16|s>>>16|c;a[n+3]=s<<8|c}for(let s=4*i,r=t+o;s>3,h=7&n,u=l^f,m=e.length;a=new Uint32Array(a.buffer);let p=0;for(let n=0;n>7&1)&u;a[p+1]=l^-(n>>6&1)&u;a[p+2]=l^-(n>>5&1)&u;a[p+3]=l^-(n>>4&1)&u;a[p+4]=l^-(n>>3&1)&u;a[p+5]=l^-(n>>2&1)&u;a[p+6]=l^-(n>>1&1)&u;a[p+7]=l^-(1&n)&u}if(0===h)continue;const n=t>7-e&1)&u}return{srcPos:t,destPos:p}}class ImageResizer{static#O=2048;static#R=FeatureTest.isImageDecoderSupported;constructor(e,t){this._imgData=e;this._isMask=t}static get canUseImageDecoder(){return shadow(this,"canUseImageDecoder",this.#R?ImageDecoder.isTypeSupported("image/bmp"):Promise.resolve(!1))}static needsToBeResized(e,t){if(e<=this.#O&&t<=this.#O)return!1;const{MAX_DIM:a}=this;if(e>a||t>a)return!0;const n=e*t;if(this._hasMaxArea)return n>this.MAX_AREA;if(n(this.MAX_AREA=this.#O**2)}static getReducePowerForJPX(e,t,a){const n=e*t,s=2**30/(4*a);if(!this.needsToBeResized(e,t))return n>s?Math.ceil(Math.log2(n/s)):0;const{MAX_DIM:r,MAX_AREA:i}=this,o=Math.max(e/r,t/r,Math.sqrt(n/Math.min(s,i)));return Math.ceil(Math.log2(o))}static get MAX_DIM(){return shadow(this,"MAX_DIM",this._guessMax(2048,32768,0,1))}static get MAX_AREA(){this._hasMaxArea=!0;return shadow(this,"MAX_AREA",this._guessMax(this.#O,this.MAX_DIM,128,0)**2)}static set MAX_AREA(e){if(e>=0){this._hasMaxArea=!0;shadow(this,"MAX_AREA",e)}}static setOptions({canvasMaxAreaInBytes:e=-1,isImageDecoderSupported:t=!1}){this._hasMaxArea||(this.MAX_AREA=e>>2);this.#R=t}static _areGoodDims(e,t){try{const a=new OffscreenCanvas(e,t),n=a.getContext("2d");n.fillRect(0,0,1,1);const s=n.getImageData(0,0,1,1).data[3];a.width=a.height=1;return 0!==s}catch{return!1}}static _guessMax(e,t,a,n){for(;e+a+1ma){const e=this.#H();if(e)return e}const n=this._encodeBMP();let s,r;if(await ImageResizer.canUseImageDecoder){s=new ImageDecoder({data:n,type:"image/bmp",preferAnimation:!1,transfer:[n.buffer]});r=s.decode().catch(e=>{warn(`BMP image decoding failed: ${e}`);return createImageBitmap(new Blob([this._encodeBMP().buffer],{type:"image/bmp"}))}).finally(()=>{s.close()})}else r=createImageBitmap(new Blob([n.buffer],{type:"image/bmp"}));const{MAX_AREA:i,MAX_DIM:o}=ImageResizer,l=Math.max(t/o,a/o,Math.sqrt(t*a/i)),f=Math.max(l,2),c=Math.round(10*(l+1.25))/10/f,h=Math.floor(Math.log2(c)),u=new Array(h+2).fill(2);u[0]=f;u.splice(-1,1,c/(1<>i,l=n>>i;let f,c=n;try{f=new Uint8Array(r)}catch{let e=Math.floor(Math.log2(r+1));for(;;)try{f=new Uint8Array(2**e-1);break}catch{e-=1}c=Math.floor((2**e-1)/(4*a));const t=a*c*4;t>i;e>3,i=a+3&-4;if(a!==i){const e=new Uint8Array(i*t);let n=0;for(let r=0,o=t*a;r{throw t},a=import.meta.url;try{new URL(".",a).href}catch{}0;console.log.bind(console);var n,s,r,i,o,l=console.error.bind(console),f=!1,c=!1;function updateMemoryViews(){var e=u.buffer;i=new Int8Array(e);new Int16Array(e);o=new Uint8Array(e);new Uint16Array(e);new Int32Array(e);new Uint32Array(e);new Float32Array(e);new Float64Array(e);new BigInt64Array(e);new BigUint64Array(e)}class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var h,u,callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(t)},m=[],addOnPostRun=e=>m.push(e),p=[],addOnPreRun=e=>p.push(e),d=!0,g=0,b={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return n;quit_(0,e)},keepRuntimeAlive=()=>d||g>0,_proc_exit=e=>{n=e;if(!keepRuntimeAlive()){t.onExit?.(e);f=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{n=e;_proc_exit(e)},callUserCallback=e=>{if(!f)try{return e()}catch(e){handleException(e)}finally{(()=>{if(!keepRuntimeAlive())try{_exit(n)}catch(e){handleException(e)}})()}},alignMemory=(e,t)=>Math.ceil(e/t)*t,growMemory=e=>{var t=(e-u.buffer.byteLength+65535)/65536|0;try{u.grow(t);updateMemoryViews();return 1}catch(e){}};t.noExitRuntime&&(d=t.noExitRuntime);t.print&&t.print;t.printErr&&(l=t.printErr);t.wasmBinary&&t.wasmBinary;t.arguments&&t.arguments;t.thisProgram&&t.thisProgram;if(t.preInit){"function"==typeof t.preInit&&(t.preInit=[t.preInit]);for(;t.preInit.length>0;)t.preInit.shift()()}t.writeArrayToMemory=(e,t)=>{i.set(e,t)};var w,j={e:()=>function abort(e){t.onAbort?.(e);l(e="Aborted("+e+")");f=!0;e+=". Build with -sASSERTIONS for more info.";var a=new WebAssembly.RuntimeError(e);r?.(a);throw a}(""),b:()=>{d=!1;g=0},c:(e,t)=>{if(b[e]){clearTimeout(b[e].id);delete b[e]}if(!t)return 0;var a=setTimeout(()=>{delete b[e];callUserCallback(()=>h(e,performance.now()))},t);b[e]={id:a,timeout_ms:t};return 0},g:function _createImageData(e){t.imageData=new Uint8Array(e)},d:e=>{var t=o.length,a=2147483648;if((e>>>=0)>a)return!1;for(var n=1;n<=4;n*=2){var s=t*(1+.2/n);s=Math.min(s,e+100663296);var r=Math.min(a,alignMemory(Math.max(e,s),65536));if(growMemory(r))return!0}return!1},a:_proc_exit,h:function _setImageData(e,a,n,s){if(n===a){t.imageData=new Uint8ClampedArray(o.subarray(e,e+n*s));return}const r=a*s,i=t.imageData=new Uint8ClampedArray(r);for(let t=e,s=0;s{t.instantiateWasm(e,(e,t)=>{a(receiveInstance(e))})})}();!function run(){!function preRun(){if(t.preRun){"function"==typeof t.preRun&&(t.preRun=[t.preRun]);for(;t.preRun.length;)addOnPreRun(t.preRun.shift())}callRuntimeCallbacks(p)}();function doRun(){t.calledRun=!0;if(!f){!function initRuntime(){c=!0;w.j()}();s?.(t);t.onRuntimeInitialized?.();!function postRun(){if(t.postRun){"function"==typeof t.postRun&&(t.postRun=[t.postRun]);for(;t.postRun.length;)addOnPostRun(t.postRun.shift())}callRuntimeCallbacks(m)}()}}if(t.setStatus){t.setStatus("Running...");setTimeout(()=>{setTimeout(()=>t.setStatus(""),1);doRun()},1)}else doRun()}();return c?t:new Promise((e,t)=>{s=e;r=t})};class Jbig2Error extends aa{constructor(e){super(e,"Jbig2Error")}}class JBig2CCITTFaxImage{static#B=null;static#D=null;static#M=null;static#C=!0;static#N=!0;static#I=null;static setOptions({handler:e,useWasm:t,useWorkerFetch:a,wasmUrl:n}){this.#C=t;this.#N=a;this.#I=n;a||(this.#D=e)}static async#P(e){const t=`${this.#I}jbig2_nowasm_fallback.js`;let a=null;try{a=(await import( +/*webpackIgnore: true*/ +/*@vite-ignore*/ +t)).default()}catch(e){warn(`JBig2CCITTFaxImage#getJsModule: ${e}`)}e(a)}static async#E(e,t,a){const n="jbig2.wasm";try{this.#B||(this.#N?this.#B=await fetchBinaryData(`${this.#I}${n}`):this.#B=await this.#D.sendWithPromise("FetchBinaryData",{kind:"wasmUrl",filename:n}));return a((await WebAssembly.instantiate(this.#B,t)).instance)}catch(t){warn(`JBig2CCITTFaxImage#instantiateWasm: ${t}`);this.#P(e);return null}finally{this.#D=null}}static async decode(e,t,a,n,s){if(!this.#M){const{promise:e,resolve:t}=Promise.withResolvers(),a=[e];this.#C?a.push(Ia({warn,instantiateWasm:this.#E.bind(this,t)})):this.#P(t);this.#M=Promise.race(a)}const r=await this.#M;if(!r)throw new Jbig2Error("JBig2 failed to initialize");let i,o;try{const l=e.length;i=r._malloc(l);r.writeArrayToMemory(e,i);if(s)r._ccitt_decode(i,l,t,a,s.K,s.EndOfLine?1:0,s.EncodedByteAlign?1:0,s.BlackIs1?1:0,s.Columns,s.Rows);else{const e=n?n.length:0;if(e>0){o=r._malloc(e);r.writeArrayToMemory(n,o)}r._jbig2_decode(i,l,t,a,o,e)}if(!r.imageData)throw new Jbig2Error("Unknown error");const{imageData:f}=r;r.imageData=null;return f}finally{i&&r._free(i);o&&r._free(o)}}static cleanup(){this.#M=null}}const Fa=new Uint8Array(0);class DecodeStream extends BaseStream{constructor(e){super();this._rawMinBufferLength=e||0;this.pos=0;this.bufferLength=0;this.eof=!1;this.buffer=Fa;this.minBufferLength=512;if(e)for(;this.minBufferLengths&&(n=s)}else{for(;!this.eof;)this.readBlock(t);n=this.bufferLength}this.pos=n;return this.buffer.subarray(a,n)}async getImageData(e,t){if(!this.canAsyncDecodeImageFromBuffer)return this.isAsyncDecoder?this.decodeImage(null,e,t):this.getBytes(e,t);const a=await this.stream.asyncGetBytes();return this.decodeImage(a,e,t)}async asyncGetBytesFromDecompressionStream(e){this.stream.reset();const t=this.stream.isAsync?await this.stream.asyncGetBytes():this.stream.getBytes();try{const{readable:a,writable:n}=new DecompressionStream(e),s=n.getWriter();await s.ready;s.write(t).then(async()=>{await s.ready;await s.close()}).catch(()=>{});const r=[];let i=0;for await(const e of a){r.push(e);i+=e.byteLength}const o=new Uint8Array(i);let l=0;for(const e of r){o.set(e,l);l+=e.byteLength}return{decompressed:o,compressed:t}}catch{return{decompressed:null,compressed:t}}}reset(){this.pos=0}makeSubStream(e,t,a=null){if(void 0===t)for(;!this.eof;)this.readBlock();else{const a=e+t;for(;this.bufferLength<=a&&!this.eof;)this.readBlock()}return new Stream(this.buffer,e,t,a)}getBaseStreams(){return this.stream?this.stream.getBaseStreams():null}clone(){for(;!this.eof;)this.readBlock();return new Stream(this.buffer,this.start,this.end-this.start,this.dict.clone())}}class StreamsSequenceStream extends DecodeStream{constructor(e,t=null){e=e.filter(e=>e instanceof BaseStream&&!e.isImageStream);let a=0;for(const t of e)a+=t instanceof DecodeStream?t._rawMinBufferLength:t.length;super(a);this.streams=e;this._onError=t}readBlock(){const e=this.streams;if(0===e.length){this.eof=!0;return}const t=e.shift();let a;try{a=t.getBytes()}catch(e){if(this._onError){this._onError(e,t.dict?.objId);return}throw e}const n=this.bufferLength,s=n+a.length;this.ensureBuffer(s).set(a,n);this.bufferLength=s}getBaseStreams(){const e=[];for(const t of this.streams){const a=t.getBaseStreams();a&&e.push(...a)}return e.length>0?e:null}}class ColorSpaceUtils{static parse({cs:e,xref:t,resources:a=null,pdfFunctionFactory:n,globalColorSpaceCache:s,localColorSpaceCache:r,asyncIfNotCached:i=!1}){const o={xref:t,resources:a,pdfFunctionFactory:n,globalColorSpaceCache:s,localColorSpaceCache:r};let l,f,c;if(e instanceof Ref){f=e;const a=s.getByRef(f)||r.getByRef(f);if(a)return a;e=t.fetch(e)}if(e instanceof Name){l=e.name;const t=r.getByName(l);if(t)return t}try{c=this.#_(e,o)}catch(e){if(i&&!(e instanceof MissingDataException))return Promise.reject(e);throw e}if(l||f){r.set(l,f,c);f&&s.set(null,f,c)}return i?Promise.resolve(c):c}static#z(e,t){const{globalColorSpaceCache:a}=t;let n;if(e instanceof Ref){n=e;const t=a.getByRef(n);if(t)return t}const s=this.#_(e,t);n&&a.set(null,n,s);return s}static#_(e,t){const{xref:a,resources:n,pdfFunctionFactory:s,globalColorSpaceCache:r}=t;if((e=a.fetchIfRef(e))instanceof Name)switch(e.name){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"DeviceRGBA":return this.rgba;case"CMYK":case"DeviceCMYK":return this.cmyk;case"Pattern":return new PatternCS(null);default:if(n instanceof Dict){const a=n.get("ColorSpace");if(a instanceof Dict){const n=a.get(e.name);if(n){if(n instanceof Name)return this.#_(n,t);e=n;break}}}warn(`Unrecognized ColorSpace: ${e.name}`);return this.gray}if(Array.isArray(e)){const n=a.fetchIfRef(e[0]).name;let i,o,l,f,c,h;switch(n){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"CMYK":case"DeviceCMYK":return this.cmyk;case"CalGray":i=a.fetchIfRef(e[1]);f=i.getArray("WhitePoint");c=i.getArray("BlackPoint");h=i.get("Gamma");return new CalGrayCS(f,c,h);case"CalRGB":i=a.fetchIfRef(e[1]);f=i.getArray("WhitePoint");c=i.getArray("BlackPoint");h=i.getArray("Gamma");const u=i.getArray("Matrix");return new CalRGBCS(f,c,h,u);case"ICCBased":const m=e[1]instanceof Ref;if(m){const t=r.getByRef(e[1]);if(t)return t}const p=a.fetchIfRef(e[1]),d=p.dict;o=d.get("N");if(IccColorSpace.isUsable)try{const t=new IccColorSpace(p.getBytes(),"ICCBased",o);m&&r.set(null,e[1],t);return t}catch(t){if(t instanceof MissingDataException)throw t;warn(`ICCBased color space (${e[1]}): "${t}".`)}const g=d.getRaw("Alternate");if(g){const e=this.#z(g,t);if(e.numComps===o)return e;warn("ICCBased color space: Ignoring incorrect /Alternate entry.")}if(1===o)return this.gray;if(3===o)return this.rgb;if(4===o)return this.cmyk;break;case"Pattern":l=e[1]||null;l&&(l=this.#z(l,t));return new PatternCS(l);case"I":case"Indexed":l=this.#z(e[1],t);const b=MathClamp(a.fetchIfRef(e[2]),0,255),w=a.fetchIfRef(e[3]);return new IndexedCS(l,b,w);case"Separation":case"DeviceN":const j=a.fetchIfRef(e[1]);o=Array.isArray(j)?j.length:1;l=this.#z(e[2],t);const k=s.create(e[3]);return new AlternateCS(o,l,k);case"Lab":i=a.fetchIfRef(e[1]);f=i.getArray("WhitePoint");c=i.getArray("BlackPoint");const y=i.getArray("Range");return new LabCS(f,c,y);default:warn(`Unimplemented ColorSpace object: ${n}`);return this.gray}}warn(`Unrecognized ColorSpace object: ${e}`);return this.gray}static get gray(){return shadow(this,"gray",new DeviceGrayCS)}static get rgb(){return shadow(this,"rgb",new DeviceRgbCS)}static get rgba(){return shadow(this,"rgba",new DeviceRgbaCS)}static get cmyk(){if(CmykICCBasedCS.isUsable)try{return shadow(this,"cmyk",new CmykICCBasedCS)}catch{warn("CMYK fallback: DeviceCMYK")}return shadow(this,"cmyk",new DeviceCmykCS)}}class JpegError extends aa{constructor(e){super(e,"JpegError")}}class DNLMarkerError extends aa{constructor(e,t){super(e,"DNLMarkerError");this.scanLines=t}}class EOIMarkerError extends aa{constructor(e){super(e,"EOIMarkerError")}}const Ta=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]),Oa=4017,Ra=799,Ha=3406,Ba=2276,Da=1567,Ma=3784,Na=5793,Pa=2896;function buildHuffmanTable(e,t){let a,n,s=0,r=16;for(;r>0&&!e[r-1];)r--;const i=[{children:[],index:0}];let o,l=i[0];for(a=0;a0;)l=i.pop();l.index++;i.push(l);for(;i.length<=a;){i.push(o={children:[],index:0});l.children[l.index]=o.children;l=o}s++}if(a+10){d--;return p>>d&1}p=e[a++];if(255===p){const s=e[a++];if(s){if(220===s&&c){a+=2;const e=t.getUint16(a);a+=2;if(e>0&&e!==n.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",e)}else if(217===s){if(c){const e=j*(8===n.precision?8:0);if(e>0&&Math.round(n.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(p<<8|s).toString(16)}`)}}d=7;return p>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<0){g--;return}let a=i;const n=o;for(;a<=n;){const n=decodeHuffman(e.huffmanTableAC),s=15&n,r=n>>4;if(0===s){if(r<15){g=receive(r)+(1<>4;if(0===s)if(l<15){g=receive(l)+(1<>4;if(0===n){if(r<15)break;s+=16;continue}s+=r;const i=Ta[s];e.blockData[t+i]=receiveAndExtend(n);s++}};let F,T=0;const O=1===k?s[0].blocksPerLine*s[0].blocksPerColumn:h*n.mcusPerColumn;let R,H;for(;T<=O;){const n=r?Math.min(O-T,r):O;if(n>0){for(q=0;q0?"unexpected":"excessive"} MCU data, current marker is: ${F.invalid}`);a=F.offset}if(!(F.marker>=65488&&F.marker<=65495))break;a+=2}return a-m}function quantizeAndInverse(e,t,a){const n=e.quantizationTable,s=e.blockData;let r,i,o,l,f,c,h,u,m,p,d,g,b,w,j,k,y;if(!n)throw new JpegError("missing required Quantization Table.");for(let e=0;e<64;e+=8){m=s[t+e];p=s[t+e+1];d=s[t+e+2];g=s[t+e+3];b=s[t+e+4];w=s[t+e+5];j=s[t+e+6];k=s[t+e+7];m*=n[e];if(0!==(p|d|g|b|w|j|k)){p*=n[e+1];d*=n[e+2];g*=n[e+3];b*=n[e+4];w*=n[e+5];j*=n[e+6];k*=n[e+7];r=Na*m+128>>8;i=Na*b+128>>8;o=d;l=j;f=Pa*(p-k)+128>>8;u=Pa*(p+k)+128>>8;c=g<<4;h=w<<4;r=r+i+1>>1;i=r-i;y=o*Ma+l*Da+128>>8;o=o*Da-l*Ma+128>>8;l=y;f=f+h+1>>1;h=f-h;u=u+c+1>>1;c=u-c;r=r+l+1>>1;l=r-l;i=i+o+1>>1;o=i-o;y=f*Ba+u*Ha+2048>>12;f=f*Ha-u*Ba+2048>>12;u=y;y=c*Ra+h*Oa+2048>>12;c=c*Oa-h*Ra+2048>>12;h=y;a[e]=r+u;a[e+7]=r-u;a[e+1]=i+h;a[e+6]=i-h;a[e+2]=o+c;a[e+5]=o-c;a[e+3]=l+f;a[e+4]=l-f}else{y=Na*m+512>>10;a[e]=y;a[e+1]=y;a[e+2]=y;a[e+3]=y;a[e+4]=y;a[e+5]=y;a[e+6]=y;a[e+7]=y}}for(let e=0;e<8;++e){m=a[e];p=a[e+8];d=a[e+16];g=a[e+24];b=a[e+32];w=a[e+40];j=a[e+48];k=a[e+56];if(0!==(p|d|g|b|w|j|k)){r=Na*m+2048>>12;i=Na*b+2048>>12;o=d;l=j;f=Pa*(p-k)+2048>>12;u=Pa*(p+k)+2048>>12;c=g;h=w;r=4112+(r+i+1>>1);i=r-i;y=o*Ma+l*Da+2048>>12;o=o*Da-l*Ma+2048>>12;l=y;f=f+h+1>>1;h=f-h;u=u+c+1>>1;c=u-c;r=r+l+1>>1;l=r-l;i=i+o+1>>1;o=i-o;y=f*Ba+u*Ha+2048>>12;f=f*Ha-u*Ba+2048>>12;u=y;y=c*Ra+h*Oa+2048>>12;c=c*Oa-h*Ra+2048>>12;h=y;m=r+u;k=r-u;p=i+h;j=i-h;d=o+c;w=o-c;g=l+f;b=l-f;m<16?m=0:m>=4080?m=255:m>>=4;p<16?p=0:p>=4080?p=255:p>>=4;d<16?d=0:d>=4080?d=255:d>>=4;g<16?g=0:g>=4080?g=255:g>>=4;b<16?b=0:b>=4080?b=255:b>>=4;w<16?w=0:w>=4080?w=255:w>>=4;j<16?j=0:j>=4080?j=255:j>>=4;k<16?k=0:k>=4080?k=255:k>>=4;s[t+e]=m;s[t+e+8]=p;s[t+e+16]=d;s[t+e+24]=g;s[t+e+32]=b;s[t+e+40]=w;s[t+e+48]=j;s[t+e+56]=k}else{y=Na*m+8192>>14;y=y<-2040?0:y>=2024?255:y+2056>>4;s[t+e]=y;s[t+e+8]=y;s[t+e+16]=y;s[t+e+24]=y;s[t+e+32]=y;s[t+e+40]=y;s[t+e+48]=y;s[t+e+56]=y}}}function buildComponentData(e,t){const a=t.blocksPerLine,n=t.blocksPerColumn,s=new Int16Array(64);for(let e=0;e=s)return null;const i=t.getUint16(a);if(i>=65472&&i<=65534)return{invalid:null,marker:i,offset:a};let o=t.getUint16(r);for(;!(o>=65472&&o<=65534);){if(++r>=s)return null;o=t.getUint16(r)}return{invalid:i.toString(16),marker:o,offset:r}}function prepareComponents(e){const t=Math.ceil(e.samplesPerLine/8/e.maxH),a=Math.ceil(e.scanLines/8/e.maxV);for(const n of e.components){const s=Math.ceil(Math.ceil(e.samplesPerLine/8)*n.h/e.maxH),r=Math.ceil(Math.ceil(e.scanLines/8)*n.v/e.maxV),i=t*n.h,o=64*(a*n.v)*(i+1);n.blockData=new Int16Array(o);n.blocksPerLine=s;n.blocksPerColumn=r}e.mcusPerLine=t;e.mcusPerColumn=a}function readDataBlock(e,t,a){const n=t.getUint16(a);let s=(a+=2)+n-2;const r=findNextFileMarker(e,t,s,a);if(r?.invalid){warn("readDataBlock - incorrect length, current marker is: "+r.invalid);s=r.offset}const i=e.subarray(a,s);return{appData:i,oldOffset:a,newOffset:a+i.length}}function skipData(e,t,a){const n=t.getUint16(a),s=(a+=2)+n-2,r=findNextFileMarker(e,t,s,a);return r?.invalid?r.offset:s}class JpegImage{constructor({decodeTransform:e=null,colorTransform:t=-1}={}){this._decodeTransform=e;this._colorTransform=t}static canUseImageDecoder(e,t=-1){const a=new DataView(e.buffer,e.byteOffset,e.byteLength);let n=null,s=0,r=null,i=a.getUint16(s);s+=2;if(65496!==i)throw new JpegError("SOI not found");i=a.getUint16(s);s+=2;e:for(;65497!==i;){switch(i){case 65505:const{appData:t,oldOffset:o,newOffset:l}=readDataBlock(e,a,s);s=l;if(69===t[0]&&120===t[1]&&105===t[2]&&102===t[3]&&0===t[4]&&0===t[5]){if(n)throw new JpegError("Duplicate EXIF-blocks found.");n={exifStart:o+6,exifEnd:l}}i=a.getUint16(s);s+=2;continue;case 65472:case 65473:case 65474:r=e[s+7];break e;case 65535:255!==e[s]&&s--}s=skipData(e,a,s);i=a.getUint16(s);s+=2}return 4===r||3===r&&0===t?null:n||{}}parse(e,{dnlScanLines:t=null}={}){const a=new DataView(e.buffer,e.byteOffset,e.byteLength),n=e.length-1;let s,r,i=0,o=null,l=null,f=0;const c=[],h=[],u=[];let m=a.getUint16(i);i+=2;if(65496!==m)throw new JpegError("SOI not found");m=a.getUint16(i);i+=2;e:for(;65497!==m;){let p,d,g;switch(m){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:const{appData:b,newOffset:w}=readDataBlock(e,a,i);i=w;65504===m&&74===b[0]&&70===b[1]&&73===b[2]&&70===b[3]&&0===b[4]&&(o={version:{major:b[5],minor:b[6]},densityUnits:b[7],xDensity:b[8]<<8|b[9],yDensity:b[10]<<8|b[11],thumbWidth:b[12],thumbHeight:b[13],thumbData:b.subarray(14,14+3*b[12]*b[13])});65518===m&&65===b[0]&&100===b[1]&&111===b[2]&&98===b[3]&&101===b[4]&&(l={version:b[5]<<8|b[6],flags0:b[7]<<8|b[8],flags1:b[9]<<8|b[10],transformCode:b[11]});break;case 65499:const j=a.getUint16(i);i+=2;const k=j+i-2;let y;for(;i>4){if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(d=0;d<64;d++){y=Ta[d];n[y]=a.getUint16(i);i+=2}}else for(d=0;d<64;d++){y=Ta[d];n[y]=e[i++]}c[15&t]=n}break;case 65472:case 65473:case 65474:if(s)throw new JpegError("Only single frame JPEGs supported");i+=2;s={};s.extended=65473===m;s.progressive=65474===m;s.precision=e[i++];const q=a.getUint16(i);i+=2;s.scanLines=t||q;s.samplesPerLine=a.getUint16(i);i+=2;s.components=[];s.componentIds={};const v=e[i++];let S=0,x=0;for(p=0;p>4,n=15&e[i+1];S>4?h:u)[15&t]=buildHuffmanTable(a,s)}break;case 65501:i+=2;r=a.getUint16(i);i+=2;break;case 65498:const F=1===++f&&!t;i+=2;const T=e[i++],O=[];for(p=0;p>4];n.huffmanTableAC=h[15&r];O.push(n)}const R=e[i++],H=e[i++],D=e[i++];try{i+=decodeScan(e,a,i,s,O,r,R,H,D>>4,15&D,F)}catch(t){if(t instanceof DNLMarkerError){warn(`${t.message} -- attempting to re-parse the JPEG image.`);return this.parse(e,{dnlScanLines:t.scanLines})}if(t instanceof EOIMarkerError){warn(`${t.message} -- ignoring the rest of the image data.`);break e}throw t}break;case 65500:i+=4;break;case 65535:255!==e[i]&&i--;break;default:const M=findNextFileMarker(e,a,i-2,i-3);if(M?.invalid){warn("JpegImage.parse - unexpected data, current marker is: "+M.invalid);i=M.offset;break}if(!M||i>=n){warn("JpegImage.parse - reached the end of the image data without finding an EOI marker (0xFFD9).");break e}throw new JpegError("JpegImage.parse - unknown marker: "+m.toString(16))}if(i>8)+v[m+1];return j}get _isColorConversionNeeded(){return this.adobe?!!this.adobe.transformCode:3===this.numComponents?0!==this._colorTransform&&(82!==this.components[0].index||71!==this.components[1].index||66!==this.components[2].index):1===this._colorTransform}_convertYccToRgb(e){let t,a,n;for(let s=0,r=e.length;s4)throw new JpegError("Unsupported color mode");const r=this._getLinearizedBlockData(e,t,s);if(1===this.numComponents&&(a||n)){const e=r.length*(a?4:3),t=new Uint8ClampedArray(e);let n=0;if(a)!function grayToRGBA(e,t){if(FeatureTest.isLittleEndian)for(let a=0,n=e.length;a0&&(e=e.subarray(t));break}return e}decodeImage(e){if(this.eof)return this.buffer;e=this.#L(e||this.bytes);const t=new JpegImage(this.jpegOptions);t.parse(e);const a=t.getData({width:this.drawWidth,height:this.drawHeight,forceRGBA:this.forceRGBA,forceRGB:this.forceRGB,isSourcePDF:!0});this.buffer=a;this.bufferLength=a.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}async getTransferableImage(){if(!await JpegStream.canUseImageDecoder)return null;const e=this.jpegOptions;if(e.decodeTransform)return null;let t;try{const a=this.canAsyncDecodeImageFromBuffer&&await this.stream.asyncGetBytes()||this.bytes;if(!a)return null;let n=this.#L(a);const s=JpegImage.canUseImageDecoder(n,e.colorTransform);if(!s)return null;if(s.exifStart){n=n.slice();n.fill(0,s.exifStart,s.exifEnd)}t=new ImageDecoder({data:n,type:"image/jpeg",preferAnimation:!1});return(await t.decode()).image}catch(e){warn(`getTransferableImage - failed: "${e}".`);return null}finally{t?.close()}}get isImageStream(){return!0}}const Ea=async function OpenJPEG(e={}){var t=e,a="./this.program",quit_=(e,t)=>{throw t},n=import.meta.url;try{new URL(".",n).href}catch{}0;var s,r,i,o,l,f,c,h=console.log.bind(console),u=console.error.bind(console),m=!1,p=!1;function updateMemoryViews(){var e=g.buffer;l=new Int8Array(e);new Int16Array(e);c=new Uint8Array(e);new Uint16Array(e);o=new Int32Array(e);f=new Uint32Array(e);new Float32Array(e);new Float64Array(e);new BigInt64Array(e);new BigUint64Array(e)}class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var d,g,callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(t)},b=[],addOnPostRun=e=>b.push(e),w=[],addOnPreRun=e=>w.push(e),j=!0,k=0,y={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return s;quit_(0,e)},keepRuntimeAlive=()=>j||k>0,_proc_exit=e=>{s=e;if(!keepRuntimeAlive()){t.onExit?.(e);m=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{s=e;_proc_exit(e)},callUserCallback=e=>{if(!m)try{return e()}catch(e){handleException(e)}finally{(()=>{if(!keepRuntimeAlive())try{_exit(s)}catch(e){handleException(e)}})()}},alignMemory=(e,t)=>Math.ceil(e/t)*t,growMemory=e=>{var t=(e-g.buffer.byteLength+65535)/65536|0;try{g.grow(t);updateMemoryViews();return 1}catch(e){}},q={},getEnvStrings=()=>{if(!getEnvStrings.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8",_:a||"./this.program"};for(var t in q)void 0===q[t]?delete e[t]:e[t]=q[t];var n=[];for(var t in e)n.push(`${t}=${e[t]}`);getEnvStrings.strings=n}return getEnvStrings.strings},stringToUTF8=(e,t,a)=>((e,t,a,n)=>{if(!(n>0))return 0;for(var s=a,r=a+n-1,i=0;i=r)break;t[a++]=o}else if(o<=2047){if(a+1>=r)break;t[a++]=192|o>>6;t[a++]=128|63&o}else if(o<=65535){if(a+2>=r)break;t[a++]=224|o>>12;t[a++]=128|o>>6&63;t[a++]=128|63&o}else{if(a+3>=r)break;t[a++]=240|o>>18;t[a++]=128|o>>12&63;t[a++]=128|o>>6&63;t[a++]=128|63&o;i++}}t[a]=0;return a-s})(e,c,t,a),lengthBytesUTF8=e=>{for(var t=0,a=0;a=55296&&n<=57343){t+=4;++a}else t+=3}return t},v=[null,[],[]],S=globalThis.TextDecoder&&new TextDecoder,UTF8ArrayToString=(e,t=0,a,n)=>{var s=((e,t,a,n)=>{var s=t+a;if(n)return s;for(;e[t]&&!(t>=s);)++t;return t})(e,t,a,n);if(s-t>16&&e.buffer&&S)return S.decode(e.subarray(t,s));for(var r="";t>10,56320|1023&f)}}else r+=String.fromCharCode((31&i)<<6|o)}else r+=String.fromCharCode(i)}return r},printChar=(e,t)=>{var a=v[e];if(0===t||10===t){(1===e?h:u)(UTF8ArrayToString(a));a.length=0}else a.push(t)},UTF8ToString=(e,t,a)=>e?UTF8ArrayToString(c,e,t,a):"";t.noExitRuntime&&(j=t.noExitRuntime);t.print&&(h=t.print);t.printErr&&(u=t.printErr);t.wasmBinary&&t.wasmBinary;t.arguments&&t.arguments;t.thisProgram&&(a=t.thisProgram);if(t.preInit){"function"==typeof t.preInit&&(t.preInit=[t.preInit]);for(;t.preInit.length>0;)t.preInit.shift()()}t.writeArrayToMemory=(e,t)=>{l.set(e,t)};var x,C={m:()=>function abort(e){t.onAbort?.(e);u(e=`Aborted(${e})`);m=!0;e+=". Build with -sASSERTIONS for more info.";var a=new WebAssembly.RuntimeError(e);i?.(a);throw a}(""),l:()=>{j=!1;k=0},i:(e,t)=>{if(y[e]){clearTimeout(y[e].id);delete y[e]}if(!t)return 0;var a=setTimeout(()=>{delete y[e];callUserCallback(()=>d(e,performance.now()))},t);y[e]={id:a,timeout_ms:t};return 0},f:function _copy_pixels_1(e,a){e>>=2;const n=t.imageData=new Uint8ClampedArray(a),s=o.subarray(e,e+a);n.set(s)},e:function _copy_pixels_3(e,a,n,s){e>>=2;a>>=2;n>>=2;const r=t.imageData=new Uint8ClampedArray(3*s),i=o.subarray(e,e+s),l=o.subarray(a,a+s),f=o.subarray(n,n+s);for(let e=0;e>=2;a>>=2;n>>=2;s>>=2;const i=t.imageData=new Uint8ClampedArray(4*r),l=o.subarray(e,e+r),f=o.subarray(a,a+r),c=o.subarray(n,n+r),h=o.subarray(s,s+r);for(let e=0;e{var t=c.length,a=2147483648;if((e>>>=0)>a)return!1;for(var n=1;n<=4;n*=2){var s=t*(1+.2/n);s=Math.min(s,e+100663296);var r=Math.min(a,alignMemory(Math.max(e,s),65536));if(growMemory(r))return!0}return!1},o:(e,t)=>{var a=0,n=0;for(var s of getEnvStrings()){var r=t+a;f[e+n>>2]=r;a+=stringToUTF8(s,r,1/0)+1;n+=4}return 0},p:(e,t)=>{var a=getEnvStrings();f[e>>2]=a.length;var n=0;for(var s of a)n+=lengthBytesUTF8(s)+1;f[t>>2]=n;return 0},n:function _fd_seek(e,t,a,n){t=(s=t)<-9007199254740992||s>9007199254740992?NaN:Number(s);var s;return 70},b:(e,t,a,n)=>{for(var s=0,r=0;r>2],o=f[t+4>>2];t+=8;for(var l=0;l>2]=s;return 0},q:function _gray_to_rgba(e,a){e>>=2;const n=t.imageData=new Uint8ClampedArray(4*a),s=o.subarray(e,e+a);for(let e=0;e>=2;a>>=2;const s=t.imageData=new Uint8ClampedArray(4*n),r=o.subarray(e,e+n),i=o.subarray(a,a+n);for(let e=0;e>=2;a>>=2;n>>=2;const r=t.imageData=new Uint8ClampedArray(4*s),i=o.subarray(e,e+s),l=o.subarray(a,a+s),f=o.subarray(n,n+s);for(let e=0;e{t.instantiateWasm(e,(e,t)=>{a(receiveInstance(e))})})}();!function run(){!function preRun(){if(t.preRun){"function"==typeof t.preRun&&(t.preRun=[t.preRun]);for(;t.preRun.length;)addOnPreRun(t.preRun.shift())}callRuntimeCallbacks(w)}();function doRun(){t.calledRun=!0;if(!m){!function initRuntime(){p=!0;x.s()}();r?.(t);t.onRuntimeInitialized?.();!function postRun(){if(t.postRun){"function"==typeof t.postRun&&(t.postRun=[t.postRun]);for(;t.postRun.length;)addOnPostRun(t.postRun.shift())}callRuntimeCallbacks(b)}()}}if(t.setStatus){t.setStatus("Running...");setTimeout(()=>{setTimeout(()=>t.setStatus(""),1);doRun()},1)}else doRun()}();return p?t:new Promise((e,t)=>{r=e;i=t})};class JpxError extends aa{constructor(e){super(e,"JpxError")}}class JpxImage{static#B=null;static#D=null;static#M=null;static#C=!0;static#N=!0;static#I=null;static setOptions({handler:e,useWasm:t,useWorkerFetch:a,wasmUrl:n}){this.#C=t;this.#N=a;this.#I=n;a||(this.#D=e)}static async#P(e){const t=`${this.#I}openjpeg_nowasm_fallback.js`;let a=null;try{a=(await import( +/*webpackIgnore: true*/ +/*@vite-ignore*/ +t)).default()}catch(e){warn(`JpxImage#getJsModule: ${e}`)}e(a)}static async#E(e,t,a){const n="openjpeg.wasm";try{this.#B||(this.#N?this.#B=await fetchBinaryData(`${this.#I}${n}`):this.#B=await this.#D.sendWithPromise("FetchBinaryData",{kind:"wasmUrl",filename:n}));return a((await WebAssembly.instantiate(this.#B,t)).instance)}catch(t){warn(`JpxImage#instantiateWasm: ${t}`);this.#P(e);return null}finally{this.#D=null}}static async decode(e,{numComponents:t=4,isIndexedColormap:a=!1,smaskInData:n=!1,reducePower:s=0}={}){if(!this.#M){const{promise:e,resolve:t}=Promise.withResolvers(),a=[e];this.#C?a.push(Ea({warn,instantiateWasm:this.#E.bind(this,t)})):this.#P(t);this.#M=Promise.race(a)}const r=await this.#M;if(!r)throw new JpxError("OpenJPEG failed to initialize");let i;try{const o=e.length;i=r._malloc(o);r.writeArrayToMemory(e,i);if(r._jp2_decode(i,o,t>0?t:0,!!a,!!n,s)){const{errorMessages:e}=r;if(e){delete r.errorMessages;throw new JpxError(e)}throw new JpxError("Unknown error")}const{imageData:l}=r;r.imageData=null;return l}finally{i&&r._free(i)}}static cleanup(){this.#M=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const a=t;t=e.getByte();if(65361===(a<<8|t)){e.skip(4);const t=e.getInt32()>>>0,a=e.getInt32()>>>0,n=e.getInt32()>>>0,s=e.getInt32()>>>0;e.skip(16);return{width:t-n,height:a-s,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}function addState(e,t,a,n,s){let r=e;for(let e=0,a=t.length-1;e1e3){f=Math.max(f,u);m+=h+2;u=0;h=0}c.push({transform:t,x:u,y:m,w:a.width,h:a.height});u+=a.width+2;h=Math.max(h,a.height)}const p=Math.max(f,u)+1,d=m+h+1,g=new Uint8Array(p*d*4),b=p<<2;for(let e=0;e=0;){t[r-4]=t[r];t[r-3]=t[r+1];t[r-2]=t[r+2];t[r-1]=t[r+3];t[r+a]=t[r+a-4];t[r+a+1]=t[r+a-3];t[r+a+2]=t[r+a-2];t[r+a+3]=t[r+a-1];r-=b}}const w={width:p,height:d};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(p,d);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(g.buffer),p,d),0,0);w.bitmap=e.transferToImageBitmap();w.data=null}else{w.kind=O;w.data=g}a.splice(r,4*l,_t);n.splice(r,4*l,[w,c]);return r+1});addState(_a,[ke,qe,Mt,ye],null,function iterateImageMaskGroup(e,t){const a=e.fnArray,n=(t-(e.iCurr-3))%4;switch(n){case 0:return a[t]===ke;case 1:return a[t]===qe;case 2:return a[t]===Mt;case 3:return a[t]===ye}throw new Error(`iterateImageMaskGroup - invalid pos: ${n}`)},function foundImageMaskGroup(e,t){const a=e.fnArray,n=e.argsArray,s=e.iCurr,r=s-3,i=s-2,o=s-1;let l=Math.floor((t-r)/4);if(l<10)return t-(t-r)%4;let f,c,h=!1;const u=n[o][0],m=n[i][0],p=n[i][1],d=n[i][2],g=n[i][3];if(p===d){h=!0;f=i+4;let e=o+4;for(let t=1;t=4&&a[r-4]===a[i]&&a[r-3]===a[o]&&a[r-2]===a[l]&&a[r-1]===a[f]&&n[r-4][0]===c&&n[r-4][1]===h){u++;m-=5}let p=m+4;for(let e=1;e{const t=e.argsArray,a=t[e.iCurr-1][0];if(a!==Te&&a!==Oe&&a!==Be&&a!==De&&a!==Me&&a!==Ne)return!0;const n=t[e.iCurr-2];return 1===n[0]&&0===n[1]&&0===n[2]&&1===n[3]},()=>!1,(e,t)=>{const{fnArray:n,argsArray:s}=e,r=e.iCurr,i=r-3,o=r-2,l=s[r-1],f=s[o],[,[c],h]=l;if(h){const e=a.slice();Util.axialAlignedBoundingBox(h,f,e);h.set(e);for(let e=0,t=c.length;e=a)break}n=(n||_a)[e[t]];if(n&&!Array.isArray(n)){r.iCurr=t;t++;if(!n.checkFn||(0,n.checkFn)(r)){s=n;n=null}else n=null}else t++}this.state=n;this.match=s;this.lastProcessed=t}flush(){for(;this.match;){const e=this.queue.fnArray.length;this.lastProcessed=(0,this.match.processFn)(this.context,e);this.match=null;this.state=null;this._optimize()}}reset(){this.state=null;this.match=null;this.lastProcessed=0}}class OperatorList{static CHUNK_SIZE=1e3;static CHUNK_SIZE_ABOUT=this.CHUNK_SIZE-5;static isOffscreenCanvasSupported=!1;constructor(e=0,t){this._streamSink=t;this.fnArray=[];this.argsArray=[];this.optimizer=!t||e&p?new NullOptimizer(this):new QueueOptimizer(this);this.dependencies=new Set;this._totalLength=0;this.weight=0;this._resolved=t?null:Promise.resolve()}static setOptions({isOffscreenCanvasSupported:e}){this.isOffscreenCanvasSupported=e}get length(){return this.argsArray.length}get ready(){return this._resolved||this._streamSink.ready}get totalLength(){return this._totalLength+this.length}addOp(e,t){this.optimizer.push(e,t);this.weight++;this._streamSink&&(this.weight>=OperatorList.CHUNK_SIZE||this.weight>=OperatorList.CHUNK_SIZE_ABOUT&&(e===ye||e===Le))&&this.flush()}addImageOps(e,t,a,n=!1){if(n){this.addOp(ke);this.addOp(je,[[["SMask",!1]]])}void 0!==a&&this.addOp(xt,["OC",a]);this.addOp(e,t);void 0!==a&&this.addOp(Ct,[]);n&&this.addOp(ye)}addDependency(e){if(!this.dependencies.has(e)){this.dependencies.add(e);this.addOp(he,[e])}}addDependencies(e){for(const t of e)this.addDependency(t)}addOpList(e){if(e instanceof OperatorList){for(const t of e.dependencies)this.dependencies.add(t);for(let t=0,a=e.length;t"boolean"==typeof e)})(m,2)&&([h,u]=m);this.extendStart=h;this.extendEnd=u;const p=e.getRaw("Function"),d=n.create(p,!0),g=(f-l)/840,b=this.colorStops=[];if(l>=f||g<=0){info("Bad shading domain.");return}const w=new Float32Array(o.numComps),j=new Float32Array(1);let k=0;j[0]=l;d(j,0,w,0);const y=new Uint8ClampedArray(3);o.getRgb(w,0,y);let[q,v,S]=y;b.push([0,Util.makeHexColor(q,v,S)]);let x=1;j[0]=l+g;d(j,0,w,0);o.getRgb(w,0,y);let[C,F,T]=y,O=C-q+1,R=F-v+1,H=T-S+1,D=C-q-1,M=F-v-1,N=T-S-1;for(let e=2;e<840;e++){j[0]=l+e*g;d(j,0,w,0);o.getRgb(w,0,y);const[t,a,n]=y,s=e-k;O=Math.min(O,(t-q+1)/s);R=Math.min(R,(a-v+1)/s);H=Math.min(H,(n-S+1)/s);D=Math.max(D,(t-q-1)/s);M=Math.max(M,(a-v-1)/s);N=Math.max(N,(n-S-1)/s);if(!(D<=O&&M<=R&&N<=H)){const e=Util.makeHexColor(C,F,T);b.push([x/840,e]);O=t-C+1;R=a-F+1;H=n-T+1;D=t-C-1;M=a-F-1;N=n-T-1;k=x;q=C;v=F;S=T}x=e;C=t;F=a;T=n}b.push([1,Util.makeHexColor(C,F,T)]);let E="transparent";e.has("Background")&&(E=o.getRgbHex(e.get("Background"),0));if(!h){b.unshift([0,E]);b[1][0]+=BaseShading.SMALL_NUMBER}if(!u){b.at(-1)[0]-=BaseShading.SMALL_NUMBER;b.push([1,E])}this.colorStops=b}getIR(){const{coordsArr:e,shadingType:t}=this;let a,n,s,r,i;if(t===La){n=[e[0],e[1]];s=[e[2],e[3]];r=null;i=null;a="axial"}else if(t===Ua){n=[e[0],e[1]];s=[e[3],e[4]];r=e[2];i=e[5];a="radial"}else unreachable(`getPattern type unknown: ${t}`);return["RadialAxial",a,this.bbox,this.colorStops,n,s,r,i]}}function buildMeshVertexData(e,t,a){let n=0;for(const e of a)if(e.type===q)n+=e.coords.length;else if(e.type===v){const t=e.verticesPerRow;n+=(Math.floor(e.coords.length/t)-1)*(t-1)*6}const s=new Float32Array(2*n),r=new Uint8Array(4*n);let i=0,o=0;const addVertex=(a,n)=>{s[i++]=e[2*a];s[i++]=e[2*a+1];r[o++]=t[4*n];r[o++]=t[4*n+1];r[o++]=t[4*n+2];o++};for(const e of a){const t=e.coords,a=e.colors;if(e.type===q)for(let e=0,n=t.length;e0)return!0;const e=this.stream.getByte();if(e<0)return!1;this.buffer=e;this.bufferLength=8;return!0}readBits(e){const{stream:t}=this;let{buffer:a,bufferLength:n}=this;if(32===e){if(0===n)return t.getInt32()>>>0;a=a<<24|t.getByte()<<16|t.getByte()<<8|t.getByte();const e=t.getByte();this.buffer=e&(1<>n)>>>0}if(8===e&&0===n)return t.getByte();for(;n>n}align(){this.buffer=0;this.bufferLength=0}readFlag(){return this.readBits(this.context.bitsPerFlag)}readCoordinate(){const{bitsPerCoordinate:e,decode:t}=this.context,a=this.readBits(e),n=this.readBits(e),s=e<32?1/((1<i?i:t;a=a>o?o:a;n=n>>0}function hexToStr(e,t){return 1===t?String.fromCharCode(e[0],e[1]):3===t?String.fromCharCode(e[0],e[1],e[2],e[3]):String.fromCharCode(...e.subarray(0,t+1))}function addHex(e,t,a){let n=0;for(let s=a;s>=0;s--){n+=e[s]+t[s];e[s]=255&n;n>>=8}}function incHex(e,t){let a=1;for(let n=t;n>=0&&a>0;n--){a+=e[n];e[n]=255&a;a>>=8}}const $a=16;class BinaryCMapStream extends Stream{tmpBuf=new Uint8Array(19);constructor(e){super(e,0,e.length,null)}readNumber(){let e,t=0;do{const a=this.getByte();if(a<0)throw new FormatError("unexpected EOF in bcmap");e=!(128&a);t=t<<7|127&a}while(!e);return t}readSigned(){const e=this.readNumber();return 1&e?~(e>>>1):e>>>1}readHex(e,t){e.set(this.getBytes(t+1))}readHexNumber(e,t){let a;const n=this.tmpBuf;let s=0;do{const e=this.getByte();if(e<0)throw new FormatError("unexpected EOF in bcmap");a=!(128&e);n[s++]=127&e}while(!a);let r=t,i=0,o=0;for(;r>=0;){for(;o<8&&n.length>0;){i|=n[--s]<>=8;o-=8}}readHexSigned(e,t){this.readHexNumber(e,t);const a=1&e[t]?255:0;let n=0;for(let s=0;s<=t;s++){n=(1&n)<<8|e[s];e[s]=n>>1^a}}readString(){const e=this.readNumber(),t=new Array(e);for(let a=0;a=0;){const e=u>>5;if(7===e){switch(31&u){case 0:n.readString();break;case 1:r=n.readString()}continue}const a=!!(16&u),s=15&u;if(s+1>$a)throw new Error("BinaryCMapReader.process: Invalid dataSize.");const m=1,p=n.readNumber();switch(e){case 0:n.readHex(i,s);n.readHexNumber(o,s);addHex(o,i,s);t.addCodespaceRange(s+1,hexToInt(i,s),hexToInt(o,s));for(let e=1;e=0;--s){n[a+s]=255&i;i>>=8}}}}class AsciiHexStream extends DecodeStream{constructor(e,t){t&&(t*=.5);super(t);this.stream=e;this.dict=e.dict;this.firstDigit=-1}readBlock(){const e=this.stream.getBytes(8e3);if(!e.length){this.eof=!0;return}const t=e.length+1>>1,a=this.ensureBuffer(this.bufferLength+t);let n=this.bufferLength,s=this.firstDigit;for(const t of e){let e;if(t>=48&&t<=57)e=15&t;else{if(!(t>=65&&t<=70||t>=97&&t<=102)){if(62===t){this.eof=!0;break}continue}e=9+(15&t)}if(s<0)s=e;else{a[n++]=s<<4|e;s=-1}}if(s>=0&&this.eof){a[n++]=s<<4;s=-1}this.firstDigit=s;this.bufferLength=n}}let Ya=(()=>{const e=Int32Array.from([256,402,436,468,500,534,566,598,630,662,694,726,758,790,822,854,886,920,952,984,1016,1048,1080]),t=Int32Array.from([1,2,3,4,0,5,17,6,16,7,8,9,10,11,12,13,14,15]),a=Int32Array.from([0,3,2,1,0,0,0,0,0,0,3,3,3,3,3,3]),n=Int32Array.from([0,0,0,0,-1,1,-2,2,-3,3,-1,1,-2,2,-3,3]),s=Int32Array.from([131072,131076,131075,196610,131072,131076,131075,262145,131072,131076,131075,196610,131072,131076,131075,262149]),r=Int32Array.from([1,5,9,13,17,25,33,41,49,65,81,97,113,145,177,209,241,305,369,497,753,1265,2289,4337,8433,16625]),i=Int32Array.from([2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,7,8,9,10,11,12,13,24]),o=Int16Array.from([0,0,0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,7,8,9,10,12,14,24]),l=Int16Array.from([0,0,0,0,0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,7,8,9,10,24]),f=new Int16Array(2816);!function unpackCommandLookupTable(e){const t=new Int32Array(24),a=new Int32Array(24);a[0]=2;for(let e=0;e<23;++e){t[e+1]=t[e]+(1<>6,r=-4;if(s>=2){s-=2;r=0}const i=(170064>>2*s&3)<<3|n>>3&7,f=(156228>>2*s&3)<<3|7&n,c=a[f],h=r+Math.min(c,5)-2,u=4*n;e[u]=o[i]|l[f]<<8;e[u+1]=t[i];e[u+2]=a[f];e[u+3]=h}}(f);function log2floor(e){let t=-1,a=16,n=e;for(;a>0;){let e=n>>a;if(0!==e){t+=a;n=e}a>>=1}return t+n}function calculateDistanceAlphabetSize(e,t,a){return 16+t+2*(a<>a),r=log2floor(s)-1;return((r-1<<1|s>>r&1)-1<=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}if(0!==readFewBits(e,1)){const t=readFewBits(e,3);return 0===t?1:readFewBits(e,t)+(1<>>a.bitOffset;n+=255&s;const r=e[n]>>16,i=65535&e[n];if(r<=8){a.bitOffset+=r;return i}n+=i;n+=(s&(1<>>8;a.bitOffset+=8+(e[n]>>16);return 65535&e[n]}function readBlockLength(e,t,a){if(a.bitOffset>=16){a.accumulator32=a.shortBuffer[a.halfOffset++]<<16|a.accumulator32>>>16;a.bitOffset-=16}const n=readSymbol(e,t,a),s=i[n];if(a.bitOffset>=16){a.accumulator32=a.shortBuffer[a.halfOffset++]<<16|a.accumulator32>>>16;a.bitOffset-=16}return r[n]+(s<=16?readFewBits(a,s):readManyBits(a,s))}function moveToFront(e,t){let a=t;const n=e[a];for(;a>0;){e[a]=e[a-1];a--}e[0]=n}function readSimpleHuffmanCode(e,t,a,n,s){const r=new Int32Array(t),i=new Int32Array(4),o=1+log2floor(e-1),l=readFewBits(s,2)+1;for(let e=0;e=16){s.accumulator32=s.shortBuffer[s.halfOffset++]<<16|s.accumulator32>>>16;s.bitOffset-=16}const a=readFewBits(s,o);if(a>=t)return makeError(s,-15);i[e]=a}const f=function checkDupes(e,t,a){for(let n=0;n=16){i.accumulator32=i.shortBuffer[i.halfOffset++]<<16|i.accumulator32>>>16;i.bitOffset-=16}const n=i.accumulator32>>>i.bitOffset&15;i.bitOffset+=s[n]>>16;const r=65535&s[n];l[a]=r;if(0!==r){f-=32>>r;c++;if(f<=0)break}}if(0!==f&&1!==c)return makeError(i,-4);const h=function readHuffmanCodeLengths(e,t,a,n){let s=0,r=8,i=0,o=0,l=32768;const f=new Int32Array(33);buildHuffmanTable(f,f.length-1,5,e,18);for(;s0;){if(n.halfOffset>2030){const e=readMoreInput(n);if(e<0)return e}if(n.bitOffset>=16){n.accumulator32=n.shortBuffer[n.halfOffset++]<<16|n.accumulator32>>>16;n.bitOffset-=16}const e=n.accumulator32>>>n.bitOffset&31;n.bitOffset+=f[e]>>16;const c=65535&f[e];if(c<16){i=0;a[s++]=c;if(0!==c){r=c;l-=32768>>c}}else{const e=c-14;let f=0;16===c&&(f=r);if(o!==f){i=0;o=f}const h=i;if(i>0){i-=2;i<<=e}if(n.bitOffset>=16){n.accumulator32=n.shortBuffer[n.halfOffset++]<<16|n.accumulator32>>>16;n.bitOffset-=16}i+=readFewBits(n,e)+3;const u=i-h;if(s+u>t)return makeError(n,-2);for(let e=0;e2030){const e=readMoreInput(s);if(e<0)return e}if(s.bitOffset>=16){s.accumulator32=s.shortBuffer[s.halfOffset++]<<16|s.accumulator32>>>16;s.bitOffset-=16}const r=readFewBits(s,2);return 1===r?readSimpleHuffmanCode(e,t,a,n,s):readComplexHuffmanCode(t,r,a,n,s)}function decodeContextMap(t,a,n){let s;if(n.halfOffset>2030){s=readMoreInput(n);if(s<0)return s}const r=decodeVarLenUnsignedByte(n)+1;if(1===r){a.fill(0,0,t);return r}if(n.bitOffset>=16){n.accumulator32=n.shortBuffer[n.halfOffset++]<<16|n.accumulator32>>>16;n.bitOffset-=16}let i=0;0!==readFewBits(n,1)&&(i=readFewBits(n,4)+1);const o=r+i,l=e[o+31>>5],f=new Int32Array(l+1),c=f.length-1;s=readHuffmanCode(o,o,f,c,n);if(s<0)return s;let h=0;for(;h2030){s=readMoreInput(n);if(s<0)return s}if(n.bitOffset>=16){n.accumulator32=n.shortBuffer[n.halfOffset++]<<16|n.accumulator32>>>16;n.bitOffset-=16}const e=readSymbol(f,c,n);if(0===e){a[h]=0;h++}else if(e<=i){if(n.bitOffset>=16){n.accumulator32=n.shortBuffer[n.halfOffset++]<<16|n.accumulator32>>>16;n.bitOffset-=16}let s=(1<=t)return makeError(n,-3);a[h]=0;h++;s--}}else{a[h]=e-i;h++}}if(n.bitOffset>=16){n.accumulator32=n.shortBuffer[n.halfOffset++]<<16|n.accumulator32>>>16;n.bitOffset-=16}1===readFewBits(n,1)&&function inverseMoveToFrontTransform(e,t){const a=new Int32Array(256);for(let e=0;e<256;++e)a[e]=e;for(let n=0;n=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}let r=readSymbol(e.blockTrees,2*t,e);const i=readBlockLength(e.blockTrees,2*t+1,e);1===r?r=n[s+1]+1:0===r?r=n[s]:r-=2;r>=a&&(r-=a);n[s]=n[s+1];n[s+1]=r;return i}function decodeLiteralBlockSwitch(e){e.literalBlockLength=decodeBlockTypeAndLength(e,0,e.numLiteralBlockTypes);const t=e.rings[5];e.contextMapSlice=t<<6;e.literalTreeIdx=255&e.contextMap[e.contextMapSlice];const a=e.contextModes[t];e.contextLookupOffset1=a<<9;e.contextLookupOffset2=e.contextLookupOffset1+256}function decodeCommandBlockSwitch(e){e.commandBlockLength=decodeBlockTypeAndLength(e,1,e.numCommandBlockTypes);e.commandTreeIdx=e.rings[7]}function decodeDistanceBlockSwitch(e){e.distanceBlockLength=decodeBlockTypeAndLength(e,2,e.numDistanceBlockTypes);e.distContextMapSlice=e.rings[9]<<2}function readNextMetablockHeader(e){if(0!==e.inputEnd){e.nextRunningState=10;e.runningState=12;return 0}e.literalTreeGroup=new Int32Array(0);e.commandTreeGroup=new Int32Array(0);e.distanceTreeGroup=new Int32Array(0);let t;if(e.halfOffset>2030){t=readMoreInput(e);if(t<0)return t}t=function decodeMetaBlockLength(e){if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}e.inputEnd=readFewBits(e,1);e.metaBlockLength=0;e.isUncompressed=0;e.isMetadata=0;if(0!==e.inputEnd&&0!==readFewBits(e,1))return 0;const t=readFewBits(e,2)+4;if(7===t){e.isMetadata=1;if(0!==readFewBits(e,1))return makeError(e,-6);const t=readFewBits(e,2);if(0===t)return 0;for(let a=0;a=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}const n=readFewBits(e,8);if(0===n&&a+1===t&&t>1)return makeError(e,-8);e.metaBlockLength+=n<<8*a}}else for(let a=0;a=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}const n=readFewBits(e,4);if(0===n&&a+1===t&&t>4)return makeError(e,-8);e.metaBlockLength+=n<<4*a}e.metaBlockLength++;0===e.inputEnd&&(e.isUncompressed=readFewBits(e,1));return 0}(e);if(t<0)return t;if(0===e.metaBlockLength&&0===e.isMetadata)return 0;if(0!==e.isUncompressed||0!==e.isMetadata){t=jumpToByteBoundary(e);if(t<0)return t;0===e.isMetadata?e.runningState=6:e.runningState=5}else e.runningState=3;if(0!==e.isMetadata)return 0;e.expectedTotalSize+=e.metaBlockLength;e.expectedTotalSize>1<<30&&(e.expectedTotalSize=1<<30);e.ringBufferSizee.expectedTotalSize){const a=e.expectedTotalSize;for(;t>>1>a;)t>>=1;0===e.inputEnd&&t<16384&&e.maxRingBufferSize>=16384&&(t=16384)}if(t<=e.ringBufferSize)return;const a=new Int8Array(t+37),n=e.ringBuffer;0!==n.length&&a.set(n.subarray(0,e.ringBufferSize),0);e.ringBuffer=a;e.ringBufferSize=t}(e);return 0}function readMetablockPartition(e,t,a){let n=e.blockTrees[2*t];if(a<=1){e.blockTrees[2*t+1]=n;e.blockTrees[2*t+2]=n;return 1<<28}const s=a+2;let r=readHuffmanCode(s,s,e.blockTrees,2*t,e);if(r<0)return r;n+=r;e.blockTrees[2*t+1]=n;r=readHuffmanCode(26,26,e.blockTrees,2*t+1,e);if(r<0)return r;n+=r;e.blockTrees[2*t+2]=n;return readBlockLength(e.blockTrees,2*t+1,e)}function readMetablockHuffmanCodesAndContextMaps(e){e.numLiteralBlockTypes=decodeVarLenUnsignedByte(e)+1;let t=readMetablockPartition(e,0,e.numLiteralBlockTypes);if(t<0)return t;e.literalBlockLength=t;e.numCommandBlockTypes=decodeVarLenUnsignedByte(e)+1;t=readMetablockPartition(e,1,e.numCommandBlockTypes);if(t<0)return t;e.commandBlockLength=t;e.numDistanceBlockTypes=decodeVarLenUnsignedByte(e)+1;t=readMetablockPartition(e,2,e.numDistanceBlockTypes);if(t<0)return t;e.distanceBlockLength=t;if(e.halfOffset>2030){t=readMoreInput(e);if(t<0)return t}if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}e.distancePostfixBits=readFewBits(e,2);e.numDirectDistanceCodes=readFewBits(e,4)<=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}e.contextModes[a]=readFewBits(e,2);a++}if(e.halfOffset>2030){t=readMoreInput(e);if(t<0)return t}}const n=e.numLiteralBlockTypes<<6;e.contextMap=new Int8Array(n);t=decodeContextMap(n,e.contextMap,e);if(t<0)return t;const s=t;e.trivialLiteralContext=1;for(let t=0;t>6){e.trivialLiteralContext=0;break}e.distContextMap=new Int8Array(e.numDistanceBlockTypes<<2);t=decodeContextMap(e.numDistanceBlockTypes<<2,e.distContextMap,e);if(t<0)return t;const r=t;e.literalTreeGroup=new Int32Array(huffmanTreeGroupAllocSize(256,s));t=decodeHuffmanTreeGroup(256,256,s,e,e.literalTreeGroup);if(t<0)return t;e.commandTreeGroup=new Int32Array(huffmanTreeGroupAllocSize(704,e.numCommandBlockTypes));t=decodeHuffmanTreeGroup(704,704,e.numCommandBlockTypes,e,e.commandTreeGroup);if(t<0)return t;let i=calculateDistanceAlphabetSize(e.distancePostfixBits,e.numDirectDistanceCodes,24),o=i;if(1===e.isLargeWindow){i=calculateDistanceAlphabetSize(e.distancePostfixBits,e.numDirectDistanceCodes,62);t=calculateDistanceAlphabetLimit(e,2147483644,e.distancePostfixBits,e.numDirectDistanceCodes);if(t<0)return t;o=t}e.distanceTreeGroup=new Int32Array(huffmanTreeGroupAllocSize(o,r));t=decodeHuffmanTreeGroup(i,o,r,e,e.distanceTreeGroup);if(t<0)return t;!function calculateDistanceLut(e,t){const a=e.distExtraBits,n=e.distOffset,s=e.distancePostfixBits,r=e.numDirectDistanceCodes,i=1<>>e.bitOffset;e.bitOffset+=8;r--}if(0===r)return 0;const i=Math.min(halfAvailable(e),r>>1);if(i>0){const a=e.halfOffset<<1,n=i<<1;t.set(e.byteBuffer.subarray(a,a+n),s);s+=n;r-=n;e.halfOffset+=i}if(0===r)return 0;if(halfAvailable(e)>0){if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}for(;0!==r;){t[s++]=e.accumulator32>>>e.bitOffset;e.bitOffset+=8;r--}return checkHealth(e,0)}for(;r>0;){const a=readInput(e,t,s,r);if(a<-1)return a;if(a<=0)return makeError(e,-16);s+=a;r-=a}return 0}(e,t,e.pos,n);if(a<0)return a;e.metaBlockLength-=n;e.pos+=n;if(e.pos===e.ringBufferSize){e.nextRunningState=6;e.runningState=12;return 0}a=reload(e);if(a<0)return a;e.runningState=2;return 0}function writeRingBuffer(e){const t=Math.min(e.outputLength-e.outputUsed,e.ringBufferBytesReady-e.ringBufferBytesWritten);if(0!==t){e.output.set(e.ringBuffer.subarray(e.ringBufferBytesWritten,e.ringBufferBytesWritten+t),e.outputOffset+e.outputUsed);e.outputUsed+=t;e.ringBufferBytesWritten+=t}return e.outputUsed>5]}function decodeHuffmanTreeGroup(e,t,a,n,s){let r=a;for(let i=0;i2147483644)return makeError(e,-9);const a=e.distance-e.maxDistance-1-e.cdTotalSize;if(a<0){const t=function initializeCompoundDictionaryCopy(e,t,a){-1===e.cdBlockBits&&function initializeCompoundDictionary(e){e.cdBlockMap=new Int8Array(256);let t=8;for(;e.cdTotalSize-1>>t;)t++;t-=8;e.cdBlockBits=t;let a=0,n=0;for(;a>t]=n;a+=1<>e.cdBlockBits];for(;t>=e.cdChunkOffsets[n+1];)n++;if(e.cdTotalSize>t+a)return makeError(e,-9);e.distRbIdx=e.distRbIdx+1&3;e.rings[e.distRbIdx]=e.distance;e.metaBlockLength-=a;e.cdBrIndex=n;e.cdBrOffset=t-e.cdChunkOffsets[n];e.cdBrLength=a;e.cdBrCopied=0;return 0}(e,-a-1,e.copyLength);if(t<0)return t;e.runningState=14}else{const n=u,s=e.copyLength;if(s>31)return makeError(e,-9);const r=p[s];if(0===r)return makeError(e,-9);let i=m[s];const o=a>>r;i+=(a&(1<=l.numTransforms)return makeError(e,-9);const f=function transformDictionaryWord(e,t,a,n,s,r,i){let o=t;const l=r.triplets,f=r.prefixSuffixStorage,c=r.prefixSuffixHeads,h=3*i,u=l[h],m=l[h+1],p=l[h+2];let d=c[u];const g=c[u+1];let b=c[p];const w=c[p+1];let j=m-11,k=m;(j<1||j>9)&&(j=0);(k<1||k>9)&&(k=0);for(;d!==g;)e[o++]=f[d++];let y=s;j>y&&(j=y);let q=n+j;y-=j;y-=k;let v=y;for(;v>0;){e[o++]=a[q++];v--}if(10===m||11===m){let t=o-y;10===m&&(y=1);for(;y>0;){const a=255&e[t];if(a<192){a>=97&&a<=122&&(e[t]=32^e[t]);t+=1;y-=1}else if(a<224){e[t+1]=32^e[t+1];t+=2;y-=2}else{e[t+2]=5^e[t+2];t+=3;y-=3}}}else if(21===m||22===m){let t=o-y;const a=r.params[i];let n=16777216-(32768&a)+(32767&a);for(;y>0;){let a=1;const s=255&e[t];if(s<128){n+=s;e[t]=127&n}else if(s<192);else if(s<224)if(y>=2){const r=e[t+1];n+=63&r|(31&s)<<6;e[t]=192|n>>6&31;e[t+1]=192&r|63&n;a=2}else a=y;else if(s<240)if(y>=3){const r=e[t+1],i=e[t+2];n+=63&i|(63&r)<<6|(15&s)<<12;e[t]=224|n>>12&15;e[t+1]=192&r|n>>6&63;e[t+2]=192&i|63&n;a=3}else a=y;else if(s<248)if(y>=4){const r=e[t+1],i=e[t+2],o=e[t+3];n+=63&o|(63&i)<<6|(63&r)<<12|(7&s)<<18;e[t]=240|n>>18&7;e[t+1]=192&r|n>>12&63;e[t+2]=192&i|n>>6&63;e[t+3]=192&o|63&n;a=4}else a=y;t+=a;y-=a;21===m&&(y=0)}}for(;b!==w;)e[o++]=f[b++];return o-t}(e.ringBuffer,e.pos,n,i,s,l,o);e.pos+=f;e.metaBlockLength-=f;if(e.pos>=t){e.nextRunningState=4;e.runningState=12;return 0}e.runningState=4}return 0}function copyFromCompoundDictionary(e,t){let a=e.pos;const n=a;for(;e.cdBrLength!==e.cdBrCopied;){const n=t-a,s=e.cdChunkOffsets[e.cdBrIndex+1]-e.cdChunkOffsets[e.cdBrIndex]-e.cdBrOffset;let r=e.cdBrLength-e.cdBrCopied;r>s&&(r=s);r>n&&(r=n);e.ringBuffer.set(e.cdChunks[e.cdBrIndex].subarray(e.cdBrOffset,e.cdBrOffset+r),a);a+=r;e.cdBrOffset+=r;e.cdBrCopied+=r;if(r===s){e.cdBrIndex++;e.cdBrOffset=0}if(a>=t)break}return a-n}function decompress(e){let t;if(0===e.runningState)return makeError(e,-25);if(e.runningState<0)return makeError(e,-28);if(11===e.runningState)return makeError(e,-22);if(1===e.runningState){const t=function decodeWindowBits(e){const t=e.isLargeWindow;e.isLargeWindow=0;if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}if(0===readFewBits(e,1))return 16;let a=readFewBits(e,3);if(0!==a)return 17+a;a=readFewBits(e,3);if(0!==a){if(1===a){if(0===t)return-1;e.isLargeWindow=1;if(1===readFewBits(e,1))return-1;a=readFewBits(e,6);return a<10||a>30?-1:a}return 8+a}return 17}(e);if(-1===t)return makeError(e,-11);e.maxRingBufferSize=1<2030){t=readMoreInput(e);if(t<0)return t}0===e.commandBlockLength&&decodeCommandBlockSwitch(e);e.commandBlockLength--;if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}const o=readSymbol(e.commandTreeGroup,e.commandTreeIdx,e)<<2,l=f[o],c=f[o+1],u=f[o+2];e.distanceCode=f[o+3];if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}const m=255&l;e.insertLength=c+(m<=16?readFewBits(e,m):readManyBits(e,m));if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}const p=l>>8;e.copyLength=u+(p<=16?readFewBits(e,p):readManyBits(e,p));e.j=0;e.runningState=7;continue;case 7:if(0!==e.trivialLiteralContext)for(;e.j2030){t=readMoreInput(e);if(t<0)return t}0===e.literalBlockLength&&decodeLiteralBlockSwitch(e);e.literalBlockLength--;if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}i[e.pos]=readSymbol(e.literalTreeGroup,e.literalTreeIdx,e);e.pos++;e.j++;if(e.pos>=s){e.nextRunningState=7;e.runningState=12;break}}else{let a=255&i[e.pos-1&r],n=255&i[e.pos-2&r];for(;e.j2030){t=readMoreInput(e);if(t<0)return t}0===e.literalBlockLength&&decodeLiteralBlockSwitch(e);const r=h[e.contextLookupOffset1+a]|h[e.contextLookupOffset2+n],o=255&e.contextMap[e.contextMapSlice+r];e.literalBlockLength--;n=a;if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}a=readSymbol(e.literalTreeGroup,o,e);i[e.pos]=a;e.pos++;e.j++;if(e.pos>=s){e.nextRunningState=7;e.runningState=12;break}}}if(7!==e.runningState)continue;e.metaBlockLength-=e.insertLength;if(e.metaBlockLength<=0){e.runningState=4;continue}let d=e.distanceCode;if(d<0)e.distance=e.rings[e.distRbIdx];else{if(e.halfOffset>2030){t=readMoreInput(e);if(t<0)return t}0===e.distanceBlockLength&&decodeDistanceBlockSwitch(e);e.distanceBlockLength--;if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}const s=255&e.distContextMap[e.distContextMapSlice+d];d=readSymbol(e.distanceTreeGroup,s,e);if(d<16){const t=e.distRbIdx+a[d]&3;e.distance=e.rings[t]+n[d];if(e.distance<0)return makeError(e,-12)}else{const t=e.distExtraBits[d];let a;if(e.bitOffset+t<=32)a=readFewBits(e,t);else{if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}a=t<=16?readFewBits(e,t):readManyBits(e,t)}e.distance=e.distOffset[d]+(a<e.maxDistance){e.runningState=9;continue}if(d>0){e.distRbIdx=e.distRbIdx+1&3;e.rings[e.distRbIdx]=e.distance}if(e.copyLength>e.metaBlockLength)return makeError(e,-9);e.j=0;e.runningState=8;continue;case 8:let g=e.pos-e.distance&r,b=e.pos;const w=e.copyLength-e.j,j=g+w,k=b+w;if(jb&&k>g){const e=w+3>>2;for(let t=0;t=s){e.nextRunningState=8;e.runningState=12;break}}8===e.runningState&&(e.runningState=4);continue;case 9:t=doUseDictionary(e,s);if(t<0)return t;continue;case 14:e.pos+=copyFromCompoundDictionary(e,s);if(e.pos>=s){e.nextRunningState=14;e.runningState=12;return 2}e.runningState=4;continue;case 5:for(;e.metaBlockLength>0;){if(e.halfOffset>2030){t=readMoreInput(e);if(t<0)return t}if(e.bitOffset>=16){e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16}readFewBits(e,8);e.metaBlockLength--}e.runningState=2;continue;case 6:t=copyUncompressedData(e);if(t<0)return t;continue;case 12:e.ringBufferBytesReady=Math.min(e.pos,e.ringBufferSize);e.runningState=13;continue;case 13:t=writeRingBuffer(e);if(0!==t)return t;e.pos>=e.maxBackwardDistance&&(e.maxDistance=e.maxBackwardDistance);if(e.pos>=e.ringBufferSize){e.pos>e.ringBufferSize&&i.copyWithin(0,e.ringBufferSize,e.pos);e.pos=e.pos&r;e.ringBufferBytesWritten=0}e.runningState=e.nextRunningState;continue;default:return makeError(e,-28)}if(10!==e.runningState)return makeError(e,-29);if(e.metaBlockLength<0)return makeError(e,-10);t=jumpToByteBoundary(e);if(0!==t)return t;t=checkHealth(e,1);return 0!==t?t:1}const c=new function Transforms(e,t,a){this.numTransforms=0;this.triplets=new Int32Array(0);this.prefixSuffixStorage=new Int8Array(0);this.prefixSuffixHeads=new Int32Array(0);this.params=new Int16Array(0);this.numTransforms=e;this.triplets=new Int32Array(3*e);this.params=new Int16Array(e);this.prefixSuffixStorage=new Int8Array(t);this.prefixSuffixHeads=new Int32Array(a+1)}(121,167,50);!function unpackTransforms(e,t,a,n,s){const r=toUtf8Runes(n),i=r.length;let o=1,l=0;for(let a=0;a#\n#]# for # a # that #. # with #\'# from # by #. The # on # as # is #ing #\n\t#:#ed #(# at #ly #="# of the #. This #,# not #er #al #=\'#ful #ive #less #est #ize #ous #'," !! ! , *! &! \" ! ) * * - ! # ! #!*! + ,$ ! - % . / # 0 1 . \" 2 3!* 4% ! # / 5 6 7 8 0 1 & $ 9 + : ; < ' != > ?! 4 @ 4 2 & A *# ( B C& ) % ) !*# *-% A +! *. D! %' & E *6 F G% ! *A *% H! D I!+! J!+ K +- *4! A L!*4 M N +6 O!*% +.! K *G P +%( ! G *D +D Q +# *K!*G!+D!+# +G +A +4!+% +K!+4!*D!+K!*K");function getNextKey(e,t){let a=1<>=1;return(e&a-1)+a}function replicateValue(e,t,a,n,s){let r=n;for(;r>0;){r-=a;e[t+r]=s}}function nextTableBitSize(e,t,a){let n=t,s=1<0;){replicateValue(e,r+u,p,c,t<<16|i[m++]);u=getNextKey(u,t);o[t]--}}const d=h-1;let g=-1,b=r;p=1;for(let t=a+1;t<=15;++t){p<<=1;for(;o[t]>0;){if((u&d)!==g){b+=c;f=nextTableBitSize(o,t,a);c=1<>a),p,c,t-a<<16|i[m++]);u=getNextKey(u,t);o[t]--}}return h}function readMoreInput(e){if(0!==e.endOfStreamReached)return halfAvailable(e)>=-2?0:makeError(e,-16);const t=e.halfOffset<<1;let a=4096-t;e.byteBuffer.copyWithin(0,t,4096);e.halfOffset=0;for(;a<4096;){const t=4096-a,n=readInput(e,e.byteBuffer,a,t);if(n<-1)return n;if(n<=0){e.endOfStreamReached=1;e.tailBytes=a;a+=1;break}a+=n}!function bytesToNibbles(e,t){const a=e.byteBuffer,n=t>>1,s=e.shortBuffer;for(let e=0;e>3)-4;return a>e.tailBytes?makeError(e,-13):0!==t&&a!==e.tailBytes?makeError(e,-17):0}function readFewBits(e,t){const a=e.accumulator32>>>e.bitOffset&(1<>>16;e.bitOffset-=16;return a|readFewBits(e,t-16)<<16}function prepare(e){if(e.halfOffset>2030){const t=readMoreInput(e);if(0!==t)return t}let t=checkHealth(e,0);if(0!==t)return t;e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16;e.accumulator32=e.shortBuffer[e.halfOffset++]<<16|e.accumulator32>>>16;e.bitOffset-=16;return 0}function reload(e){return 32===e.bitOffset?prepare(e):0}function jumpToByteBoundary(e){const t=32-e.bitOffset&7;if(0!==t){if(0!==readFewBits(e,t))return makeError(e,-5)}return 0}function halfAvailable(e){let t=2048;0!==e.endOfStreamReached&&(t=e.tailBytes+1>>1);return t-e.halfOffset}const h=new Int32Array(2048);!function unpackLookupTable(e,t,a){for(let t=0;t<256;++t){e[t]=63&t;e[512+t]=t>>2;e[1792+t]=2+(t>>6)}for(let a=0;a<128;++a)e[1024+a]=4*(t.charCodeAt(a)-32);for(let t=0;t<64;++t){e[1152+t]=1&t;e[1216+t]=2+(1&t)}let n=1280;for(let t=0;t<19;++t){const s=3&t,r=a.charCodeAt(t)-32;for(let t=0;t>1;for(let e=0;e!pfwp6s{8-ip<73s{je#+pllmpfbwmlmfwvafyfqlpfmwqffgeb`wjmwldjewkbqn2;s{`bnfkjooalogyllnuljgfbpzqjmdejoosfbhjmjw`lpw0s{8ib`hwbdpajwpqloofgjwhmftmfbq?"..dqltIPLMgvwzMbnfpbofzlv#olwpsbjmibyy`logfzfpejpkttt-qjphwbapsqfu23s{qjpf16s{Aovfgjmd033/abooelqgfbqmtjogal{-ebjqob`hufqpsbjqivmfwf`kje+"sj`hfujo\'+! tbqnolqgglfpsvoo/333jgfbgqbtkvdfpslwevmgavqmkqfe`foohfzpwj`hklvqolppevfo21s{pvjwgfboQPP!bdfgdqfzDFW!fbpfbjnpdjqobjgp;s{8mbuzdqjgwjsp :::tbqpobgz`bqp*8#~sksolpfmvooubpwtjmgQPP#tfbqqfozaffmpbnfgvhfmbpb`bsftjpkdvoeW109kjwppolwdbwfhj`haovqwkfz26s{$$*8*8!=npjftjmpajqgplqwafwbpffhW2;9lqgpwqffnboo53s{ebqnlupalzpX3^-$*8!SLPWafbqhjgp*8~~nbqzwfmg+VH*rvbgyk9\n.pjy....sqls$*8ojewW2:9uj`fbmgzgfaw=QPPsllomf`haoltW259gllqfuboW249ofwpebjolqbosloomlub`lopdfmf#lxplewqlnfwjooqlpp?k0=slvqebgfsjmh?wq=njmj*"+njmfyk9abqpkfbq33*8njoh#..=jqlmeqfggjphtfmwpljosvwp,ip,klozW119JPAMW139bgbnpffp?k1=iplm$/#$`lmwW129#QPPollsbpjbnllm?,s=plvoOJMFelqw`bqwW279?k2=;3s{"..?:s{8W379njhf975Ymj`fjm`kZlqhqj`fyk9\b$**8svqfnbdfsbqbwlmfalmg904Y\\le\\$^*8333/yk9\vwbmhzbqgaltoavpk965YIbub03s{\t~\t&@0&907YifeeF[SJ`bpkujpbdloepmltyk9rvfq-`pppj`hnfbwnjm-ajmggfookjqfsj`pqfmw905YKWWS.132elwltloeFMG#{al{967YALGZgj`h8\t~\tf{jw906Yubqpafbw$~*8gjfw:::8bmmf~~?,Xj^-Obmdhn.^tjqfwlzpbggppfbobof{8\t\n~f`klmjmf-lqd336*wlmziftppbmgofdpqlle333*#133tjmfdfbqgldpallwdbqz`vwpwzofwfnswjlm-{no`l`hdbmd\'+$-63s{Sk-Gnjp`bobmolbmgfphnjofqzbmvmj{gjp`*8~\tgvpw`ojs*-\t\t43s{.133GUGp4^=?wbsfgfnlj((*tbdffvqlskjolswpklofEBRpbpjm.15WobapsfwpVQO#avoh`llh8~\tKFBGX3^*baaqivbm+2:;ofpkwtjm?,j=plmzdvzpev`hsjsf.\t"331*mgltX2^8X^8\tOld#pbow\t\n\nabmdwqjnabwk*x\t33s{\t~*8hl9\0effpbg=p9,,#X^8wloosovd+*x\tx\t#-ip$133sgvboalbw-ISD*8\t~rvlw*8\t\t$*8\t\t~1327132613251324132;132:13131312131113101317131613151314131;131:130313021301130013071306130513041320132113221323133:133;133413351336133713301331133213332:::2::;2::42::52::62::72::02::12::22::32:;:2:;;2:;42:;52:;62:;72:;02:;12:;22:;32:4:2:4;2:442:452:462:472:402:412:422:432:5:2:5;2:542:552:562:572:502:512:522:532:6:2:6;2:642:652:662:672:602:612:622:632333231720:73333::::`lnln/Mpfpwffpwbsfqlwlglkb`f`bgbb/]lajfmg/Abbp/Aujgb`bpllwqlelqlplollwqb`vbogjilpjgldqbmwjslwfnbgfafbodlrv/Efpwlmbgbwqfpsl`l`bpbabilwlgbpjmlbdvbsvfpvmlpbmwfgj`fovjpfoobnbzlylmbbnlqsjpllaqb`oj`foolgjlpklqb`bpj<[<\\!sbqhpnlvpfNlpw#---?,bnlmdaqbjmalgz#mlmf8abpfg`bqqzgqbewqfefqsbdf\\klnf-nfwfqgfobzgqfbnsqlufiljmw?,wq=gqvdp?"..#bsqjojgfboboofmf{b`welqwk`lgfpoldj`Ujft#pffnpaobmhslqwp#+133pbufg\\ojmhdlbopdqbmwdqffhklnfpqjmdpqbwfg03s{8tklpfsbqpf+*8!#Aol`hojmv{ilmfpsj{fo$*8!=*8je+.ofewgbujgklqpfEl`vpqbjpfal{fpWqb`hfnfmw?,fn=abq!=-pq`>wltfqbow>!`baofkfmqz17s{8pfwvsjwbozpkbqsnjmlqwbpwftbmwpwkjp-qfpfwtkffodjqop,`pp,233&8`ovappwveeajaofulwfp#2333hlqfb~*8\tabmgprvfvf>#x~8;3s{8`hjmdx\t\n\nbkfbg`ol`hjqjpkojhf#qbwjlpwbwpElqn!zbkll*X3^8Balvwejmgp?,k2=gfavdwbphpVQO#>`foop~*+*821s{8sqjnfwfoopwvqmp3{533-isd!psbjmafb`kwb{fpnj`qlbmdfo..=?,djewppwfuf.ojmhalgz-~*8\t\nnlvmw#+2::EBR?,qldfqeqbmh@obpp1;s{8effgp?k2=?p`lwwwfpwp11s{8gqjmh*##oftjppkboo 30:8#elq#olufgtbpwf33s{8ib9npjnlm?elmwqfsoznffwpvmwfq`kfbswjdkwAqbmg*#">#gqfpp`ojspqllnplmhfznlajonbjm-Mbnf#sobwfevmmzwqffp`ln,!2-isdtnlgfsbqbnPWBQWofew#jggfm/#132*8\t~\telqn-ujqvp`kbjqwqbmptlqpwSbdfpjwjlmsbw`k?"..\tl.`b`ejqnpwlvqp/333#bpjbmj((*xbglaf$*X3^jg>23alwk8nfmv#-1-nj-smd!hfujm`lb`k@kjogaqv`f1-isdVQO*(-isdpvjwfpoj`fkbqqz213!#ptffwwq=\tmbnf>gjfdlsbdf#ptjpp..=\t\t eee8!=Old-`ln!wqfbwpkffw*#%%#27s{8poffsmwfmwejofgib9ojg>!`Mbnf!tlqpfpklwp.al{.gfowb\t%ow8afbqp97;Y?gbwb.qvqbo?,b=#psfmgabhfqpklsp>#!!8sks!=`wjlm20s{8aqjbmkfoolpjyf>l>&1E#iljmnbzaf?jnd#jnd!=/#eipjnd!#!*X3^NWlsAWzsf!mftozGbmph`yf`kwqbjohmltp?,k6=ebr!=yk.`m23*8\t.2!*8wzsf>aovfpwqvozgbujp-ip$8=\t?"pwffo#zlv#k1=\telqn#ifpvp233&#nfmv-\t\n\ttbofpqjphpvnfmwggjmda.ojhwfb`kdje!#ufdbpgbmphffpwjpkrjspvlnjplaqfgfpgffmwqfwlglpsvfgfb/]lpfpw/Mwjfmfkbpwblwqlpsbqwfglmgfmvfulkb`fqelqnbnjpnlnfilqnvmglbrv/Ag/Abpp/_olbzvgbef`kbwlgbpwbmwlnfmlpgbwlplwqbppjwjlnv`klbklqbovdbqnbzlqfpwlpklqbpwfmfqbmwfpelwlpfpwbpsb/Apmvfubpbovgelqlpnfgjlrvjfmnfpfpslgfq`kjofpfq/Muf`fpgf`jqilp/Efpwbqufmwbdqvslkf`klfoolpwfmdlbnjdl`lpbpmjufodfmwfnjpnbbjqfpivojlwfnbpkb`jbebulqivmjlojaqfsvmwlavfmlbvwlqbaqjoavfmbwf{wlnbqylpbafqojpwbovfdl`/_nlfmfqlivfdlsfq/Vkbafqfpwlzmvm`bnvifqubolqevfqbojaqldvpwbjdvboulwlp`bplpdv/Absvfglplnlpbujplvpwfggfafmml`kfavp`bebowbfvqlppfqjfgj`kl`vqpl`obuf`bpbpof/_msobylobqdllaqbpujpwbbslzlivmwlwqbwbujpwl`qfbq`bnslkfnlp`jm`l`bqdlsjplplqgfmkb`fm/Mqfbgjp`lsfgql`fq`bsvfgbsbsfonfmlq/Vwjo`obqlilqdf`boofslmfqwbqgfmbgjfnbq`bpjdvffoobppjdol`l`kfnlwlpnbgqf`obpfqfpwlmj/]lrvfgbsbpbqabm`lkjilpujbifsbaol/Epwfujfmfqfjmlgfibqelmgl`bmbomlqwfofwqb`bvpbwlnbqnbmlpovmfpbvwlpujoobufmglsfpbqwjslpwfmdbnbq`loofubsbgqfvmjglubnlpylmbpbnalpabmgbnbqjbbavplnv`kbpvajqqjlibujujqdqbgl`kj`bboo/Ailufmgj`kbfpwbmwbofppbojqpvfolsfplpejmfpoobnbavp`l/Epwboofdbmfdqlsobybkvnlqsbdbqivmwbglaofjpobpalopbab/]lkbaobov`kb/mqfbgj`fmivdbqmlwbpuboofboo/M`bqdbglolqbabilfpw/Edvpwlnfmwfnbqjlejqnb`lpwlej`kbsobwbkldbqbqwfpofzfpbrvfonvpflabpfpsl`lpnjwbg`jfol`kj`lnjfgldbmbqpbmwlfwbsbgfafpsobzbqfgfppjfwf`lqwf`lqfbgvgbpgfpflujfilgfpfbbdvbp%rvlw8glnbjm`lnnlmpwbwvpfufmwpnbpwfqpzpwfnb`wjlmabmmfqqfnlufp`qloovsgbwfdolabonfgjvnejowfqmvnafq`kbmdfqfpvowsvaoj`p`qffm`kllpfmlqnbowqbufojppvfpplvq`fwbqdfwpsqjmdnlgvofnlajofptjw`ksklwlpalqgfqqfdjlmjwpfoepl`jbob`wjuf`lovnmqf`lqgelooltwjwof=fjwkfqofmdwkebnjozeqjfmgobzlvwbvwklq`qfbwfqfujftpvnnfqpfqufqsobzfgsobzfqf{sbmgsloj`zelqnbwglvaofsljmwppfqjfpsfqplmojujmdgfpjdmnlmwkpelq`fpvmjrvftfjdkwsflsoffmfqdzmbwvqfpfbq`kejdvqfkbujmd`vpwlnleepfwofwwfqtjmgltpvanjwqfmgfqdqlvspvsolbgkfbowknfwklgujgflpp`klloevwvqfpkbgltgfabwfubovfpLaif`wlwkfqpqjdkwpofbdvf`kqlnfpjnsofmlwj`fpkbqfgfmgjmdpfbplmqfslqwlmojmfprvbqfavwwlmjnbdfpfmbaofnlujmdobwfpwtjmwfqEqbm`fsfqjlgpwqlmdqfsfbwOlmglmgfwbjoelqnfggfnbmgpf`vqfsbppfgwlddofsob`fpgfuj`fpwbwj``jwjfppwqfbnzfooltbwwb`hpwqffweojdkwkjggfmjmel!=lsfmfgvpfevouboofz`bvpfpofbgfqpf`qfwpf`lmggbnbdfpslqwpf{`fswqbwjmdpjdmfgwkjmdpfeef`wejfogppwbwfpleej`fujpvbofgjwlqulovnfQfslqwnvpfvnnlujfpsbqfmwb``fppnlpwoznlwkfq!#jg>!nbqhfwdqlvmg`kbm`fpvqufzafelqfpznalonlnfmwpsff`knlwjlmjmpjgfnbwwfq@fmwfqlaif`wf{jpwpnjggofFvqlsfdqltwkofdb`znbmmfqfmlvdk`bqffqbmptfqlqjdjmslqwbo`ojfmwpfof`wqbmgln`olpfgwlsj`p`lnjmdebwkfqlswjlmpjnsozqbjpfgfp`bsf`klpfm`kvq`kgfejmfqfbplm`lqmfqlvwsvwnfnlqzjeqbnfsloj`fnlgfopMvnafqgvqjmdleefqppwzofphjoofgojpwfg`boofgpjoufqnbqdjmgfofwfafwwfqaqltpfojnjwpDolabopjmdoftjgdfw`fmwfqavgdfwmltqbs`qfgjw`objnpfmdjmfpbefwz`klj`fpsjqjw.pwzofpsqfbgnbhjmdmffgfgqvppjbsofbpff{wfmwP`qjswaqlhfmbooltp`kbqdfgjujgfeb`wlqnfnafq.abpfgwkflqz`lmejdbqlvmgtlqhfgkfosfg@kvq`kjnsb`wpklvogbotbzpoldl!#alwwlnojpw!=*xubq#sqfej{lqbmdfKfbgfq-svpk+`lvsofdbqgfmaqjgdfobvm`kQfujftwbhjmdujpjlmojwwofgbwjmdAvwwlmafbvwzwkfnfpelqdlwPfbq`kbm`klqbonlpwolbgfg@kbmdfqfwvqmpwqjmdqfolbgNlajofjm`lnfpvssozPlvq`flqgfqpujftfg%maps8`lvqpfBalvw#jpobmg?kwno#`llhjfmbnf>!bnbylmnlgfqmbguj`fjm?,b=9#Wkf#gjboldklvpfpAFDJM#Nf{j`lpwbqwp`fmwqfkfjdkwbggjmdJpobmgbppfwpFnsjqfP`kllofeelqwgjqf`wmfbqoznbmvboPfof`w-\t\tLmfiljmfgnfmv!=SkjojsbtbqgpkbmgofjnslqwLeej`fqfdbqgphjoopmbwjlmPslqwpgfdqfftffhoz#+f-d-afkjmggl`wlqolddfgvmjwfg?,a=?,afdjmpsobmwpbppjpwbqwjpwjppvfg033s{`bmbgbbdfm`zp`kfnfqfnbjmAqbyjopbnsofoldl!=afzlmg.p`bofb``fswpfqufgnbqjmfEllwfq`bnfqb?,k2=\t\\elqn!ofbufppwqfpp!#,=\t-dje!#lmolbgolbgfqL{elqgpjpwfqpvqujuojpwfmefnbofGfpjdmpjyf>!bssfbowf{w!=ofufopwkbmhpkjdkfqelq`fgbmjnbobmzlmfBeqj`bbdqffgqf`fmwSflsof?aq#,=tlmgfqsqj`fpwvqmfg#x~8nbjm!=jmojmfpvmgbztqbs!=ebjofg`fmpvpnjmvwfafb`lmrvlwfp263s{fpwbwfqfnlwffnbjo!ojmhfgqjdkw8pjdmboelqnbo2-kwnopjdmvssqjm`feolbw9-smd!#elqvn-B``fppsbsfqpplvmgpf{wfmgKfjdkwpojgfqVWE.;!%bns8#Afelqf-#TjwkpwvgjlltmfqpnbmbdfsqlejwiRvfqzbmmvbosbqbnpalvdkwebnlvpdlldofolmdfqj((*#xjpqbfopbzjmdgf`jgfklnf!=kfbgfqfmpvqfaqbm`ksjf`fpaol`h8pwbwfgwls!=?qb`jmdqfpjyf..%dw8sb`jwzpf{vboavqfbv-isd!#23/333lawbjmwjwofpbnlvmw/#Jm`-`lnfgznfmv!#ozqj`pwlgbz-jmgffg`lvmwz\\oldl-EbnjozollhfgNbqhfwopf#jeSobzfqwvqhfz*8ubq#elqfpwdjujmdfqqlqpGlnbjm~fopfxjmpfqwAold?,ellwfqoldjm-ebpwfqbdfmwp?algz#23s{#3sqbdnbeqjgbzivmjlqgloobqsob`fg`lufqpsovdjm6/333#sbdf!=alpwlm-wfpw+bubwbqwfpwfg\\`lvmwelqvnpp`kfnbjmgf{/ejoofgpkbqfpqfbgfqbofqw+bssfbqPvanjwojmf!=algz!=\t)#WkfWklvdkpffjmdifqpfzMftp?,ufqjezf{sfqwjmivqztjgwk>@llhjfPWBQW#b`qlpp\\jnbdfwkqfbgmbwjufsl`hfwal{!=\tPzpwfn#Gbujg`bm`fqwbaofpsqlufgBsqjo#qfboozgqjufqjwfn!=nlqf!=albqgp`lolqp`bnsvpejqpw##X^8nfgjb-dvjwbqejmjpktjgwk9pkltfgLwkfq#-sks!#bppvnfobzfqptjoplmpwlqfpqfojfeptfgfm@vpwlnfbpjoz#zlvq#Pwqjmd\t\tTkjowbzolq`ofbq9qfplqweqfm`kwklvdk!*#(#!?algz=avzjmdaqbmgpNfnafqmbnf!=lssjmdpf`wlq6s{8!=upsb`fslpwfqnbilq#`leeffnbqwjmnbwvqfkbssfm?,mbu=hbmpbpojmh!=Jnbdfp>ebopftkjof#kpsb`f3%bns8#\t\tJm##sltfqSlophj.`lolqilqgbmAlwwlnPwbqw#.`lvmw1-kwnomftp!=32-isdLmojmf.qjdkwnjoofqpfmjlqJPAM#33/333#dvjgfpubovf*f`wjlmqfsbjq-{no!##qjdkwp-kwno.aol`hqfdF{s9klufqtjwkjmujqdjmsklmfp?,wq=vpjmd#\t\nubq#=$*8\t\n?,wg=\t?,wq=\tabkbpbaqbpjodbofdlnbdzbqslophjpqsphj4]4C5d\bTA\nzk\vBl\bQ\vUmGx\bSM\nmC\bTA\twQ\nd}\bW@\bTl\bTF\ti@\tcT\vBM\v|jBV\tqw\tcC\bWI\npa\tfM\n{Z{X\bTF\bVV\bVK\tmkF\t[]\bPm\bTv\nsI\vpg\t[I\bQpmx\v_W\n^M\npe\vQ}\vGu\nel\npeChBV\bTA\tSo\nzk\vGL\vxD\nd[JzMY\bQpli\nfl\npC{BNt\vwT\ti_\bTgQQ\n|p\vXN\bQS\vxDQC\bWZ\tpD\vVS\bTWNtYh\nzuKjN}\twr\tHa\n_D\tj`\vQ}\vWp\nxZ{c\tji\tBU\nbDa|\tTn\tpV\nZd\nmC\vEV{X\tc}\tTo\bWl\bUd\tIQ\tcg\vxs\nXW\twR\vek\tc}\t]y\tJn\nrp\neg\npV\nz\\{W\npl\nz\\\nzU\tPc\t`{\bV@\nc|\bRw\ti_\bVb\nwX\tHvSu\bTF\v_W\vWs\vsIm\nTT\ndc\tUS\t}f\tiZ\bWz\tc}MD\tBe\tiD\v@@\bTl\bPv\t}tSwM`\vnU\tkW\ved\nqo\vxY\tA|\bTz\vy`BRBM\tiaXU\nyun^\tfL\tiI\nXW\tfD\bWz\bW@\tyj\tm\tav\tBN\vb\\\tpD\bTf\nY[\tJn\bQy\t[^\vWc\vyuDlCJ\vWj\vHR\t`V\vuW\tQy\np@\vGuplJm\bW[\nLP\nxC\n`m\twQuiR\nbI\twQ\tBZ\tWVBR\npg\tcgtiCW\n_y\tRg\bQa\vQB\vWc\nYble\ngESu\nL[\tQ\tea\tdj\v]W\nb~M`\twL\bTV\bVH\nt\npl\t|bs_\bU|\bTaoQlvSkM`\bTv\vK}\nfl\tcCoQBR\tHk\t|d\bQp\tHK\tBZ\vHR\bPv\vLx\vEZ\bT\bTv\tiDoDMU\vwBSuk`St\ntC\tPl\tKg\noi\tjY\vxYh}\nzk\bWZ\tm\ve`\tTB\tfE\nzk\t`zYh\nV|\tHK\tAJ\tAJ\bUL\tp\\\tql\nYcKd\nfyYh\t[I\vDgJm\n]n\nlb\bUd\n{Z\tlu\tfsoQ\bTWJm\vwB\teaYhBC\tsb\tTn\nzU\n_y\vxY\tQ]\ngwmt\tO\\\ntb\bWW\bQy\tmI\tV[\ny\\\naB\vRb\twQ\n]QQJ\bWg\vWa\bQj\ntC\bVH\nYm\vxs\bVK\nel\bWI\vxYCq\ntR\vHV\bTl\bVw\tay\bQa\bVV\t}t\tdj\nr|\tp\\\twR\n{i\nTT\t[I\ti[\tAJ\vxs\v_W\td{\vQ}\tcg\tTz\tA|\tCj\vLmN}m\nbK\tdZ\tp\\\t`V\tsV\np@\tiD\twQ\vQ}\bTfkaJm\v@@\bV`\tzp\n@NSw\tiI\tcg\noiSu\bVwloCy\tc}\vb\\\tsUBA\bWI\bTf\nxS\tVp\nd|\bTV\vbC\tNoJu\nTC\t|`\n{Z\tD]\bU|\tc}lm\bTl\tBv\tPl\tc}\bQp\tm\nLk\tkj\n@NSbKO\tj_\tp\\\nzU\bTl\bTg\bWI\tcfXO\bWW\ndzli\tBN\nd[\bWOMD\vKC\tdj\tI_\bVV\ny\\\vLmxl\txB\tkV\vb\\\vJW\vVS\tVx\vxD\td{MD\bTa\t|`\vPzR}\vWsBM\nsICN\bTaJm\npe\ti_\npV\nrh\tRd\tHv\n~A\nxR\vWh\vWk\nxS\vAz\vwX\nbIoQ\tfw\nqI\nV|\nunz\vpg\td\\\voA{D\ti_xB\bT\t`Vqr\tTTg]CA\vuR\tVJ\tT`\npw\vRb\tI_\nCxRo\vsICjKh\tBv\tWVBBoD{D\nhcKm\v^R\tQE\n{I\np@\nc|Gt\tc}Dl\nzUqN\tsVk}\tHh\v|j\nqou|\tQ]\vekZM`St\npe\tdj\bVG\veE\tm\vWc|I\n[W\tfL\bT\tBZSu\vKaCqNtY[\nqI\bTv\tfM\ti@\t}fB\\\tQy\vBl\bWgXDkc\vx[\bVV\tQ]\ta\tPy\vxD\nfI\t}foD\tdj\tSGls\t~DCN\n{Z\t\\v\n_D\nhc\vx_C[\tAJ\nLM\tVxCI\tbj\tc^\tcF\ntCSx\twrXA\bU\\\t|a\vK\\\bTV\bVj\nd|\tfsCX\ntb\bRw\tVx\tAE\tA|\bTNt\vDg\tVc\bTld@\npo\tM\tcF\npe\tiZ\tBo\bSq\nfHl`\bTx\bWf\tHE\vF{\tcO\tfD\nlm\vfZ\nlm\veU\tdGBH\bTV\tSiMW\nwX\nz\\\t\\cCX\nd}\tl}\bQp\bTV\tF~\bQ\t`i\ng@nO\bUd\bTl\nL[\twQ\tji\ntC\t|J\nLU\naB\vxYKj\tAJuN\ti[\npeSk\vDg\vx]\bVb\bVV\nea\tkV\nqI\bTaSk\nAO\tpD\ntb\nts\nyi\bVg\ti_\v_W\nLkNt\tyj\tfMR\tiI\bTl\vwX\tsV\vMl\nyu\tAJ\bVjKO\tWV\vA}\vW\nrp\tiD\v|olv\vsIBM\td~\tCU\bVbeV\npC\vwT\tj`\tc}\vxs\vps\vvh\tWV\vGg\vAe\vVK\v]W\trg\vWcF`\tBr\vb\\\tdZ\bQp\nqIkF\nLk\vAR\bWI\bTg\tbs\tdw\n{L\n_y\tiZ\bTA\tlg\bVV\bTl\tdk\n`k\ta{\ti_{Awj\twN\v@@\bTe\ti_\n_D\twL\nAH\viK\vek\n[]\tp_\tyj\bTv\tUS\t[r\n{I\npsGt\vVK\nplS}\vWP\t|dMD\vHV\bTR}M`\bTV\bVHlvCh\bW[Ke\tR{\v^R\tab\tBZ\tVA\tB`\nd|\nhsKe\tBeOi\tR{\td\\nB\bWZ\tdZ\tVJOs\tmuQ\vhZQ@QQ\nfI\bW[B\\li\nzU\nMdM`\nxS\bVV\n\\}\vxD\tm\bTpIS\nc|\tkVi~\tV{\vhZ\t|b\bWt\n@R\voA\vnU\bWI\tea\tB`\tiD\tc}\tTzBR\vQBNj\tCP\t[I\bTv\t`WuN\vpg\vpg\vWc\tiT\tbs\twL\tU_\tc\\\t|h\vKa\tNr\tfL\nq|\nzu\nz\\\tNr\bUg\t|bm`\bTv\nyd\nrp\bWf\tUXBV\nzk\nd}\twQ\t}fCe\ved\bTW\bSB\nxU\tcn\bTb\ne\ta\\\tSG\bU|\npV\nN\\Kn\vnU\tAt\tpD\v^R\vIrb[\tR{\tdE\vxD\vWK\vWA\bQL\bW@Su\bUd\nDM\tPcCADloQ\tHswiub\na\bQpOb\nLP\bTlY[\vK}\tAJ\bQn^\vsA\bSM\nqM\bWZ\n^W\vz{S|\tfD\bVK\bTv\bPvBB\tCPdF\tid\vxsmx\vws\tcC\ntC\tycM`\vW\nrh\bQp\vxD\\o\nsI_k\nzukF\tfDXsXO\tjp\bTvBS{B\tBr\nzQ\nbI\tc{BDBVnO\bTF\tcaJd\tfL\tPV\tI_\nlK`o\twX\npa\tgu\bP}{^\bWf\n{I\tBN\npaKl\vpg\tcn\tfL\vvhCq\bTl\vnU\bSqCm\twR\bUJ\npe\nyd\nYgCy\vKW\tfD\neaoQ\tj_\tBvnM\vID\bTa\nzApl\n]n\bTa\tR{\tfr\n_y\bUg{Xkk\vxD|Ixl\nfyCe\vwB\nLk\vd]\noi\n}h\tQ]\npe\bVwHkOQ\nzk\tAJ\npV\bPv\ny\\\tA{Oi\bSBXA\veE\tjp\nq}\tiDqN\v^R\tm\tiZ\tBr\bVg\noi\n\\X\tU_\nc|\vHV\bTf\tTn\\N\\N\nuBlv\nyu\tTd\bTf\bPL\v]W\tdG\nA`\nw^\ngI\npe\tdw\nz\\ia\bWZ\tcFJm\n{Z\bWO_kDfRR\td\\\bVV\vxsBNtilm\tTd\t]y\vHV\tSo\v|jXX\tA|\vZ^\vGu\bTWM`kF\vhZ\vVK\tdG\vBl\tay\nxUqEnO\bVw\nqICX\ne\tPl\bWO\vLm\tdLuHCm\tdTfn\vwBka\vnU\n@M\nyT\tHv\t\\}Kh\td~Yhk}\neR\td\\\bWI\t|b\tHK\tiD\bTWMY\npl\bQ_\twr\vAx\tHE\bTg\bSqvp\vb\\\bWO\nOl\nsI\nfy\vID\t\\c\n{Z\n^~\npe\nAO\tTT\vxvk_\bWO\v|j\vwB\tQy\ti@\tPl\tHa\tdZk}ra\tUT\vJc\ved\np@\tQN\nd|\tkj\tHkM`\noi\twr\td\\\nlq\no_\nlb\nL[\tacBBBHCm\npl\tIQ\bVK\vxs\n`e\viK\npaOi\tUS\bTp\tfD\nPGkkXA\nz\\\neg\vWh\twRqN\nqS\tcnlo\nxS\n^W\tBU\nt\tHE\tp\\\tfF\tfw\bVV\bW@\tak\vVKls\tVJ\bVV\veE\\o\nyX\nYmM`lL\nd|\nzk\tA{sE\twQXT\nt\tPl\t]y\vwT{pMD\vb\\\tQ]Kj\tJn\nAH\vRb\tBU\tHK\t\\c\nfIm\nqM\n@R\tSo\noiBT\tHv\n_yKh\tBZ\t]i\bUJ\tV{Sr\nbI\vGg\ta_\bTR\nfI\nfl\t[K\tIIS|\vuW\tiI\bWI\nqI\v|jBV\bVg\bWZkF\vx]\bTA\tab\tfr\ti@\tJd\tJd\vps\nAO\bTaxu\tiD\nzk\t|d\t|`\bW[\tlP\tdG\bVV\vw}\vqO\ti[\bQ\bTz\vVF\twNts\tdw\bTv\neS\ngi\tNryS\npe\bVV\bSq\n`m\tyj\tBZ\vWX\bSB\tc\\\nUR\t[J\tc_nM\bWQ\vAx\nMd\tBrui\vxY\bSM\vWc\v|j\vxs\t}Q\tBO\bPL\bWW\tfM\nAO\tPc\veUe^\bTg\nqI\tac\bPv\tcFoQ\tQ\vhZka\nz\\\tiK\tBU\n`k\tCPS|M`\n{I\tS{_O\tBZZiSk\tps\tp\\\nYu\n]s\nxC\bWt\nbD\tkV\vGuyS\nqA\t[r\neKM`\tdZlL\bUg\bTl\nbD\tUS\vb\\\tpV\nccS\\\tct\t`z\bPL\vWs\nA`\neg\bSquECR\vDg\t`W\vz{\vWcSkSk\tbW\bUg\tea\nxZ\tiI\tUX\tVJ\nqn\tS{\vRb\bTQ\nplGt\vuWuj\npF\nqI\tfL\t[I\tiaXO\nyu\vDg\ved\tq{VG\bQka\tVj\tkV\txB\nd|\np@\tQN\tPc\tps]j\tkV\toU\bTp\nzUnB\vB]\ta{\bV@\n]nm`\tcz\tR{m`\bQa\vwT\bSMMYqN\tdj~s\vQ}MY\vMB\tBv\twR\bRg\vQ}\tql\vKC\nrmxuCC\vwB\vvh\tBqXq\npV\ti_ObuE\nbd\nqo\v{i\nC~\tBL\veEuH\bVjEyGz\vzR\v{i\tcf\n{Z\n]nXA\vGu\vnU\thS\vGI\nCc\tHE\bTA\tHBBHCj\nCc\bTF\tHE\nXI\tA{\bQ\tc\\\vmO\vWX\nfH\np@MY\bTF\nlK\tBt\nzU\tTTKm\vwT\npV\ndt\vyI\tVx\tQ\tRg\tTd\nzU\bRS\nLM\twAnM\tTn\ndS\t]g\nLc\vwB\t}t\t[I\tCPkX\vFm\vhZm\ti[\np@\vQ}\vW\t|d\nMO\nMd\tf_\tfD\tcJ\tHz\vRb\tio\tPyY[\nxU\tct\v@@\tww\bPvBMFF\ntbv|\vKm\tBq\tBqKh`o\nZdXU\ti]\t|`\tStB\\\bQ\v_W\tTJ\nqI\t|a\tA{\vuPMD\tPl\nxR\tfL\vws\tc{\td\\\bV`\neg\tHKkc\nd|\bVV\ny\\kc\ti]\bVG\t`V\tss\tI_\tAE\tbs\tdu\nel\tpD\vW\nqslv\bSMZi\vVKia\vQB\tQ\n{Z\bPt\vKl\nlK\nhs\ndS\bVKmf\nd^\tkV\tcO\nc|\bVH\t\\]\bTv\bSq\tmI\vDg\tVJ\tcn\ny\\\bVg\bTv\nyX\bTF\t]]\bTp\noi\nhs\veU\nBf\tdjMr\n|p\t\\g\t]r\bVb{D\nd[XN\tfM\tO\\s_\tcf\tiZXN\vWc\tqv\n`m\tU^oD\nd|\vGg\tdE\vwflou}\nd|oQ\t`iOi\vxD\ndZ\nCxYw\nzk\ntb\ngw\tyj\tB`\nyX\vps\ntC\vpP\vqw\bPu\bPX\tDm\npwNj\tss\taG\vxs\bPt\noLGz\tOk\ti@\ti]eC\tIQ\tii\tdj\v@J\t|duh\bWZ\veU\vnU\bTa\tcCg]\nzkYh\bVK\nLU\np@\ntb\ntR\tCj\vNP\ti@\bP{\n\\}\n{c\nwX\tfL\bVG\tc{\t|`\tAJ\t|C\tfDln\t|d\tbs\nqI{B\vAx\np@\nzk\vRbOs\vWSe^\vD_\tBv\vWd\bVb\vxs\veE\bRw\n]n\n|p\vg|\tfwkc\bTIka\n\\TSp\tju\vps\npeu|\vGr\bVe\tCU]MXU\vxD\bTa\tIQ\vWq\tCU\tam\tdj\bSoSw\vnUCh\tQ]s_\bPt\tfS\bTa\t\\}\n@OYc\tUZ\bTx\npe\vnU\nzU\t|}\tiD\nz\\\bSM\vxDBR\nzQ\tQN]MYh\nLP\vFm\vLXvc\vqlka\tHK\bVb\ntC\nCy\bTv\nuVoQ\t`z\t[I\tB`\vRb\tyj\tsb\vWs\bTl\tkV\ved\nelL\vxN\tm\nJn\tjY\vxD\bVb\bSq\vyu\twL\vXL\bTA\tpg\tAt\tnDXX\twR\npl\nhwyS\nps\tcO\bW[\v|jXN\tsV\tp\\\tBe\nb~\nAJ\n]ek`qN\tdw\tWV\tHE\vEVJz\tid\tB`\tzhE]\tfD\bTgqN\bTa\tjaCv\bSM\nhc\bUet_\tieg]\twQ\nPn\bVB\tjw\bVg\vbE\tBZ\vRH\bP{\tjp\n\\}\ta_\tcC\t|a\vD]\tBZ\ti[\tfD\vxW\no_\td\\\n_D\ntb\t\\c\tAJ\nlKoQlo\vLx\vM@\bWZKn\vpg\nTi\nIv\n|r\v@}JzLmWhk}ln\vxD\n]sgc\vps\tBr\bTW\vBMtZ\nBYDW\tjf\vSWC}\nqo\tdE\tmv\tIQ\bPP\bUblvBC\nzQ\t[I\vgl\nig\bUsBT\vbC\bSq\tsU\tiW\nJn\tSY\tHK\trg\npV\vID\v|jKO\t`S\t|a`vbmglfmujbqnbgqjgavp`bqjmj`jlwjfnslslqrvf`vfmwbfpwbglsvfgfmivfdlp`lmwqbfpw/Mmmlnaqfwjfmfmsfqejonbmfqbbnjdlp`jvgbg`fmwqlbvmrvfsvfgfpgfmwqlsqjnfqsqf`jlpfd/Vmavfmlpuloufqsvmwlppfnbmbkba/Abbdlpwlmvfulpvmjglp`bqolpfrvjslmj/]lpnv`klpbodvmb`lqqfljnbdfmsbqwjqbqqjabnbq/Abklnaqffnsoflufqgbg`bnajlnv`kbpevfqlmsbpbglo/Amfbsbqf`fmvfubp`vqplpfpwbabrvjfqlojaqlp`vbmwlb``fplnjdvfoubqjlp`vbwqlwjfmfpdqvslppfq/Mmfvqlsbnfgjlpeqfmwfb`fq`bgfn/Mplefqwb`l`kfpnlgfoljwbojbofwqbpbod/Vm`lnsqb`vbofpf{jpwf`vfqslpjfmglsqfmpboofdbqujbifpgjmfqlnvq`jbslgq/Msvfpwlgjbqjlsvfaolrvjfqfnbmvfosqlsjl`qjpjp`jfqwlpfdvqlnvfqwfevfmwf`fqqbqdqbmgffef`wlsbqwfpnfgjgbsqlsjbleqf`fwjfqqbf.nbjoubqjbpelqnbpevwvqllaifwlpfdvjqqjfpdlmlqnbpnjpnlp/Vmj`l`bnjmlpjwjlpqby/_mgfajglsqvfabwlofglwfm/Abifp/Vpfpsfql`l`jmblqjdfmwjfmgb`jfmwl`/Mgjykbaobqpfq/Abobwjmbevfqybfpwjoldvfqqbfmwqbq/E{jwlo/_sfybdfmgbu/Agflfujwbqsbdjmbnfwqlpibujfqsbgqfpe/M`jo`bafyb/Mqfbppbojgbfmu/Alibs/_mbavplpajfmfpwf{wlpoofubqsvfgbmevfqwf`ln/Vm`obpfpkvnbmlwfmjglajoablvmjgbgfpw/Mpfgjwbq`qfbgl2%bns8Kjpwlqz#>#mft#@fmwqbovsgbwfgPsf`jboMfwtlqhqfrvjqf`lnnfmwtbqmjmd@loofdfwlloabqqfnbjmpaf`bvpffof`wfgGfvwp`kejmbm`ftlqhfqprvj`hozafwtffmf{b`wozpfwwjmdgjpfbpfPl`jfwztfbslmpf{kjajw%ow8"..@lmwqlo`obppfp`lufqfglvwojmfbwwb`hpgfuj`fp+tjmgltsvqslpfwjwof>!Nlajof#hjoojmdpkltjmdJwbojbmgqlssfgkfbujozfeef`wp.2$^*8\t`lmejqn@vqqfmwbgubm`fpkbqjmdlsfmjmdgqbtjmdajoojlmlqgfqfgDfqnbmzqfobwfg?,elqn=jm`ovgftkfwkfqgfejmfgP`jfm`f`bwboldBqwj`ofavwwlmpobqdfpwvmjelqnilvqmfzpjgfabq@kj`bdlklojgbzDfmfqbosbppbdf/%rvlw8bmjnbwfeffojmdbqqjufgsbppjmdmbwvqboqlvdkoz-\t\tWkf#avw#mlwgfmpjwzAqjwbjm@kjmfpfob`h#lewqjavwfJqfobmg!#gbwb.eb`wlqpqf`fjufwkbw#jpOjaqbqzkvpabmgjm#eb`wbeebjqp@kbqofpqbgj`boaqlvdkwejmgjmdobmgjmd9obmd>!qfwvqm#ofbgfqpsobmmfgsqfnjvnsb`hbdfBnfqj`bFgjwjlm^%rvlw8Nfppbdfmffg#wlubovf>!`lnsof{ollhjmdpwbwjlmafojfufpnboofq.nlajofqf`lqgptbmw#wlhjmg#leEjqfel{zlv#bqfpjnjobqpwvgjfgnb{jnvnkfbgjmdqbsjgoz`ojnbwfhjmdglnfnfqdfgbnlvmwpelvmgfgsjlmffqelqnvobgzmbpwzklt#wl#Pvsslqwqfufmvff`lmlnzQfpvowpaqlwkfqplogjfqobqdfoz`boojmd-%rvlw8B``lvmwFgtbqg#pfdnfmwQlafqw#feelqwpSb`jej`ofbqmfgvs#tjwkkfjdkw9tf#kbufBmdfofpmbwjlmp\\pfbq`kbssojfgb`rvjqfnbppjufdqbmwfg9#ebopfwqfbwfgajddfpwafmfejwgqjujmdPwvgjfpnjmjnvnsfqkbspnlqmjmdpfoojmdjp#vpfgqfufqpfubqjbmw#qlof>!njppjmdb`kjfufsqlnlwfpwvgfmwplnflmff{wqfnfqfpwlqfalwwln9fuloufgboo#wkfpjwfnbsfmdojpktbz#wl##Bvdvpwpznalop@lnsbmznbwwfqpnvpj`bobdbjmpwpfqujmd~*+*8\tsbznfmwwqlvaof`lm`fsw`lnsbqfsbqfmwpsobzfqpqfdjlmpnlmjwlq#$$Wkf#tjmmjmdf{solqfbgbswfgDboofqzsqlgv`fbajojwzfmkbm`f`bqffqp*-#Wkf#`loof`wPfbq`k#bm`jfmwf{jpwfgellwfq#kbmgofqsqjmwfg`lmplofFbpwfqmf{slqwptjmgltp@kbmmfojoofdbomfvwqbopvddfpw\\kfbgfqpjdmjmd-kwno!=pfwwofgtfpwfqm`bvpjmd.tfahjw`objnfgIvpwj`f`kbswfquj`wjnpWklnbp#nlyjoobsqlnjpfsbqwjfpfgjwjlmlvwpjgf9ebopf/kvmgqfgLoznsj`\\avwwlmbvwklqpqfb`kfg`kqlmj`gfnbmgppf`lmgpsqlwf`wbglswfgsqfsbqfmfjwkfqdqfbwozdqfbwfqlufqboojnsqluf`lnnbmgpsf`jbopfbq`k-tlqpkjsevmgjmdwklvdkwkjdkfpwjmpwfbgvwjojwzrvbqwfq@vowvqfwfpwjmd`ofbqozf{slpfgAqltpfqojafqbo~#`bw`kSqlif`wf{bnsofkjgf+*8EolqjgbbmptfqpbooltfgFnsfqlqgfefmpfpfqjlvpeqffglnPfufqbo.avwwlmEvqwkfqlvw#le#">#mvoowqbjmfgGfmnbqhuljg+3*,boo-ipsqfufmwQfrvfpwPwfskfm\t\tTkfm#lapfquf?,k1=\tNlgfqm#sqlujgf!#bow>!alqgfqp-\t\tElq#\t\tNbmz#bqwjpwpsltfqfgsfqelqnej`wjlmwzsf#lenfgj`bowj`hfwplsslpfg@lvm`jotjwmfppivpwj`fDflqdf#Afodjvn---?,b=wtjwwfqmlwbaoztbjwjmdtbqebqf#Lwkfq#qbmhjmdskqbpfpnfmwjlmpvqujufp`klobq?,s=\t#@lvmwqzjdmlqfgolpp#leivpw#bpDflqdjbpwqbmdf?kfbg=?pwlssfg2$^*8\tjpobmgpmlwbaofalqgfq9ojpw#le`bqqjfg233/333?,k0=\t#pfufqboaf`lnfppfof`w#tfggjmd33-kwnonlmbq`klee#wkfwfb`kfqkjdkoz#ajloldzojef#lelq#fufmqjpf#le%qbrvl8sovplmfkvmwjmd+wklvdkGlvdobpiljmjmd`jq`ofpElq#wkfBm`jfmwUjfwmbnufkj`ofpv`k#bp`qzpwboubovf#>Tjmgltpfmilzfgb#pnboobppvnfg?b#jg>!elqfjdm#Boo#qjklt#wkfGjpsobzqfwjqfgkltfufqkjggfm8abwwofppffhjmd`bajmfwtbp#mlwollh#bw`lmgv`wdfw#wkfIbmvbqzkbssfmpwvqmjmdb9klufqLmojmf#Eqfm`k#ob`hjmdwzsj`bof{wqb`wfmfnjfpfufm#jedfmfqbwgf`jgfgbqf#mlw,pfbq`kafojfep.jnbdf9ol`bwfgpwbwj`-oldjm!=`lmufqwujlofmwfmwfqfgejqpw!=`jq`vjwEjmobmg`kfnjpwpkf#tbp23s{8!=bp#pv`kgjujgfg?,psbm=tjoo#afojmf#leb#dqfbwnzpwfqz,jmgf{-eboojmdgvf#wl#qbjotbz`loofdfnlmpwfqgfp`fmwjw#tjwkmv`ofbqIftjpk#sqlwfpwAqjwjpkeoltfqpsqfgj`wqfelqnpavwwlm#tkl#tbpof`wvqfjmpwbmwpvj`jgfdfmfqj`sfqjlgpnbqhfwpPl`jbo#ejpkjmd`lnajmfdqbskj`tjmmfqp?aq#,=?az#wkf#MbwvqboSqjub`z`llhjfplvw`lnfqfploufPtfgjpkaqjfeozSfqpjbmpl#nv`k@fmwvqzgfsj`wp`lovnmpklvpjmdp`qjswpmf{w#wlafbqjmdnbssjmdqfujpfgiRvfqz+.tjgwk9wjwof!=wllowjsPf`wjlmgfpjdmpWvqhjpkzlvmdfq-nbw`k+~*+*8\t\tavqmjmdlsfqbwfgfdqffpplvq`f>Qj`kbqg`olpfozsobpwj`fmwqjfp?,wq=\t`lolq9 vo#jg>!slppfppqloojmdskzpj`pebjojmdf{f`vwf`lmwfpwojmh#wlGfebvow?aq#,=\t9#wqvf/`kbqwfqwlvqjpn`obppj`sql`ffgf{sobjm?,k2=\tlmojmf-<{no#ufkfosjmdgjbnlmgvpf#wkfbjqojmffmg#..=*-bwwq+qfbgfqpklpwjmd eeeeeeqfbojyfUjm`fmwpjdmbop#pq`>!,Sqlgv`wgfpsjwfgjufqpfwfoojmdSvaoj`#kfog#jmIlpfsk#wkfbwqfbeef`wp?pwzof=b#obqdfglfpm$wobwfq/#Fofnfmwebuj`lm`qfbwlqKvmdbqzBjqslqwpff#wkfpl#wkbwNj`kbfoPzpwfnpSqldqbnp/#bmg##tjgwk>f%rvlw8wqbgjmdofew!=\tsfqplmpDlogfm#Beebjqpdqbnnbqelqnjmdgfpwqlzjgfb#le`bpf#lelogfpw#wkjp#jp-pq`#>#`bqwllmqfdjpwq@lnnlmpNvpojnpTkbw#jpjm#nbmznbqhjmdqfufbopJmgffg/frvbooz,pklt\\blvwgllqfp`bsf+Bvpwqjbdfmfwj`pzpwfn/Jm#wkf#pjwwjmdKf#boplJpobmgpB`bgfnz\t\n\n?"..Gbmjfo#ajmgjmdaol`h!=jnslpfgvwjojyfBaqbkbn+f{`fswxtjgwk9svwwjmd*-kwno+#X^8\tGBWBX#)hjw`kfmnlvmwfgb`wvbo#gjbof`wnbjmoz#\\aobmh$jmpwboof{sfqwpje+wzsfJw#bopl%`lsz8#!=Wfqnpalqm#jmLswjlmpfbpwfqmwbohjmd`lm`fqmdbjmfg#lmdljmdivpwjez`qjwj`peb`wlqzjwp#ltmbppbvowjmujwfgobpwjmdkjp#ltmkqfe>!,!#qfo>!gfufols`lm`fqwgjbdqbngloobqp`ovpwfqsksbo`lklo*8~*+*8vpjmd#b=?psbm=ufppfopqfujuboBggqfppbnbwfvqbmgqljgboofdfgjoomfpptbohjmd`fmwfqprvbojeznbw`kfpvmjejfgf{wjm`wGfefmpfgjfg#jm\t\n?"..#`vpwlnpojmhjmdOjwwof#Allh#lefufmjmdnjm-iptfbqjmdBoo#Qjd8\t~*+*8qbjpjmd#Bopl/#`qv`jbobalvw!=gf`obqf..=\t?p`ejqfel{bp#nv`kbssojfpjmgf{/#p/#avw#wzsf#>#\t\t?"..wltbqgpQf`lqgpSqjubwfElqfjdmSqfnjfq`klj`fpUjqwvboqfwvqmp@lnnfmwSltfqfgjmojmf8slufqwz`kbnafqOjujmd#ulovnfpBmwklmzoldjm!#QfobwfgF`lmlnzqfb`kfp`vwwjmddqbujwzojef#jm@kbswfq.pkbgltMlwbaof?,wg=\t#qfwvqmpwbgjvntjgdfwpubqzjmdwqbufopkfog#aztkl#bqftlqh#jmeb`vowzbmdvobqtkl#kbgbjqslqwwltm#le\t\tPlnf#$`oj`h$`kbqdfphfztlqgjw#tjoo`jwz#le+wkjp*8Bmgqft#vmjrvf#`kf`hfglq#nlqf033s{8#qfwvqm8qpjlm>!sovdjmptjwkjm#kfqpfoePwbwjlmEfgfqboufmwvqfsvaojpkpfmw#wlwfmpjlmb`wqfpp`lnf#wlejmdfqpGvhf#lesflsof/f{soljwtkbw#jpkbqnlmzb#nbilq!9!kwwsjm#kjp#nfmv!=\tnlmwkozleej`fq`lvm`jodbjmjmdfufm#jmPvnnbqzgbwf#leolzbowzejwmfppbmg#tbpfnsfqlqpvsqfnfPf`lmg#kfbqjmdQvppjbmolmdfpwBoafqwbobwfqbopfw#le#pnboo!=-bssfmggl#tjwkefgfqboabmh#leafmfbwkGfpsjwf@bsjwbodqlvmgp*/#bmg#sfq`fmwjw#eqln`olpjmd`lmwbjmJmpwfbgejewffmbp#tfoo-zbkll-qfpslmgejdkwfqlap`vqfqfeof`wlqdbmj`>#Nbwk-fgjwjmdlmojmf#sbggjmdb#tkloflmfqqlqzfbq#lefmg#le#abqqjfqtkfm#jwkfbgfq#klnf#leqfpvnfgqfmbnfgpwqlmd=kfbwjmdqfwbjmp`olvgeqtbz#le#Nbq`k#2hmltjmdjm#sbqwAfwtffmofpplmp`olpfpwujqwvboojmhp!=`qlppfgFMG#..=ebnlvp#btbqgfgOj`fmpfKfbowk#ebjqoz#tfbowkznjmjnboBeqj`bm`lnsfwfobafo!=pjmdjmdebqnfqpAqbpjo*gjp`vppqfsob`fDqfdlqzelmw#`lsvqpvfgbssfbqpnbhf#vsqlvmgfgalwk#leaol`hfgpbt#wkfleej`fp`lolvqpje+gl`vtkfm#kffmelq`fsvpk+evBvdvpw#VWE.;!=Ebmwbpzjm#nlpwjmivqfgVpvboozebqnjmd`olpvqflaif`w#gfefm`fvpf#le#Nfgj`bo?algz=\tfujgfmwaf#vpfghfz@lgfpj{wffmJpobnj` 333333fmwjqf#tjgfoz#b`wjuf#+wzsflelmf#`bm`lolq#>psfbhfqf{wfmgpSkzpj`pwfqqbjm?walgz=evmfqboujftjmdnjggof#`qj`hfwsqlskfwpkjewfggl`wlqpQvppfoo#wbqdfw`lnsb`wbodfaqbpl`jbo.avoh#lenbm#bmg?,wg=\t#kf#ofew*-ubo+*ebopf*8oldj`boabmhjmdklnf#wlmbnjmd#Bqjylmb`qfgjwp*8\t~*8\telvmgfqjm#wvqm@loojmpafelqf#Avw#wkf`kbqdfgWjwof!=@bswbjmpsfoofgdlggfppWbd#..=Bggjmd9avw#tbpQf`fmw#sbwjfmwab`h#jm>ebopf%Ojm`lomtf#hmlt@lvmwfqIvgbjpnp`qjsw#bowfqfg$^*8\t##kbp#wkfvm`ofbqFufmw$/alwk#jmmlw#boo\t\t?"..#sob`jmdkbqg#wl#`fmwfqplqw#le`ojfmwppwqffwpAfqmbqgbppfqwpwfmg#wlebmwbpzgltm#jmkbqalvqEqffglniftfoqz,balvw--pfbq`kofdfmgpjp#nbgfnlgfqm#lmoz#lmlmoz#wljnbdf!#ojmfbq#sbjmwfqbmg#mlwqbqfoz#b`qlmzngfojufqpklqwfq33%bns8bp#nbmztjgwk>!,)#?"X@wjwof#>le#wkf#oltfpw#sj`hfg#fp`bsfgvpfp#lesflsofp#Svaoj`Nbwwkftwb`wj`pgbnbdfgtbz#elqobtp#lefbpz#wl#tjmgltpwqlmd##pjnsof~`bw`k+pfufmwkjmelal{tfmw#wlsbjmwfg`jwjyfmJ#glm$wqfwqfbw-#Plnf#tt-!*8\talnajmdnbjowl9nbgf#jm-#Nbmz#`bqqjfpx~8tjtlqh#lepzmlmzngfefbwpebulqfglswj`bosbdfWqbvmofpp#pfmgjmdofew!=?`lnP`lqBoo#wkfiRvfqz-wlvqjpw@obppj`ebopf!#Tjokfonpvavqapdfmvjmfajpklsp-psojw+dolabo#elooltpalgz#lemlnjmbo@lmwb`wpf`vobqofew#wl`kjfeoz.kjggfm.abmmfq?,oj=\t\t-#Tkfm#jm#alwkgjpnjppF{solqfbotbzp#ujb#wkfpsb/]lotfoebqfqvojmd#bqqbmdf`bswbjmkjp#plmqvof#lekf#wllhjwpfoe/>3%bns8+`boofgpbnsofpwl#nbhf`ln,sbdNbqwjm#Hfmmfgzb``fswpevoo#lekbmgofgAfpjgfp,,..=?,baof#wlwbqdfwpfppfm`fkjn#wl#jwp#az#`lnnlm-njmfqbowl#wbhftbzp#wlp-lqd,obgujpfgsfmbowzpjnsof9je#wkfzOfwwfqpb#pklqwKfqafqwpwqjhfp#dqlvsp-ofmdwkeojdkwplufqobspoltoz#ofppfq#pl`jbo#?,s=\t\n\njw#jmwlqbmhfg#qbwf#levo=\t##bwwfnswsbjq#lenbhf#jwHlmwbhwBmwlmjlkbujmd#qbwjmdp#b`wjufpwqfbnpwqbssfg!*-`pp+klpwjofofbg#wlojwwof#dqlvsp/Sj`wvqf..=\t\t#qltp>!#laif`wjmufqpf?ellwfq@vpwlnU=?_,p`qploujmd@kbnafqpobufqztlvmgfgtkfqfbp">#$vmgelq#boosbqwoz#.qjdkw9Bqbajbmab`hfg#`fmwvqzvmjw#lenlajof.Fvqlsf/jp#klnfqjph#legfpjqfg@ojmwlm`lpw#lebdf#le#af`lnf#mlmf#les%rvlw8Njggof#fbg$*X3@qjwj`ppwvgjlp=%`lsz8dqlvs!=bppfnaonbhjmd#sqfppfgtjgdfw-sp9!#<#qfavjowaz#plnfElqnfq#fgjwlqpgfobzfg@bmlmj`kbg#wkfsvpkjmd`obpp>!avw#bqfsbqwjboAbazolmalwwln#`bqqjfq@lnnbmgjwp#vpfBp#tjwk`lvqpfpb#wkjqggfmlwfpbopl#jmKlvpwlm13s{8!=b``vpfgglvaof#dlbo#leEbnlvp#*-ajmg+sqjfpwp#Lmojmfjm#Ivozpw#(#!d`lmpvowgf`jnbokfosevoqfujufgjp#ufqzq$($jswolpjmd#efnbofpjp#boplpwqjmdpgbzp#lebqqjuboevwvqf#?laif`welq`jmdPwqjmd+!#,=\t\n\nkfqf#jpfm`lgfg-##Wkf#aboollmglmf#az,`lnnlmad`lolqobt#le#Jmgjbmbbuljgfgavw#wkf1s{#0s{irvfqz-bewfq#bsloj`z-nfm#bmgellwfq.>#wqvf8elq#vpfp`qffm-Jmgjbm#jnbdf#>ebnjoz/kwws9,,#%maps8gqjufqpfwfqmbopbnf#bpmlwj`fgujftfqp~*+*8\t#jp#nlqfpfbplmpelqnfq#wkf#mftjp#ivpw`lmpfmw#Pfbq`ktbp#wkftkz#wkfpkjssfgaq=?aq=tjgwk9#kfjdkw>nbgf#le`vjpjmfjp#wkbwb#ufqz#Bgnjqbo#ej{fg8mlqnbo#NjppjlmSqfpp/#lmwbqjl`kbqpfwwqz#wl#jmubgfg>!wqvf!psb`jmdjp#nlpwb#nlqf#wlwboozeboo#le~*8\t##jnnfmpfwjnf#jmpfw#lvwpbwjpezwl#ejmggltm#wlolw#le#Sobzfqpjm#Ivmfrvbmwvnmlw#wkfwjnf#wlgjpwbmwEjmmjpkpq`#>#+pjmdof#kfos#leDfqnbm#obt#bmgobafofgelqfpwp`llhjmdpsb`f!=kfbgfq.tfoo#bpPwbmofzaqjgdfp,dolabo@qlbwjb#Balvw#X3^8\t##jw/#bmgdqlvsfgafjmd#b*xwkqltkf#nbgfojdkwfqfwkj`boEEEEEE!alwwln!ojhf#b#fnsolzpojuf#jmbp#pffmsqjmwfqnlpw#leva.ojmhqfif`wpbmg#vpfjnbdf!=pv``ffgeffgjmdMv`ofbqjmelqnbwl#kfosTlnfm$pMfjwkfqNf{j`bmsqlwfjm?wbaof#az#nbmzkfbowkzobtpvjwgfujpfg-svpk+xpfoofqppjnsoz#Wkqlvdk-`llhjf#Jnbdf+logfq!=vp-ip!=#Pjm`f#vmjufqpobqdfq#lsfm#wl"..#fmgojfp#jm$^*8\t##nbqhfwtkl#jp#+!GLN@lnbmbdfglmf#elqwzsfle#Hjmdglnsqlejwpsqlslpfwl#pklt`fmwfq8nbgf#jwgqfppfgtfqf#jmnj{wvqfsqf`jpfbqjpjmdpq`#>#$nbhf#b#pf`vqfgAbswjpwulwjmd#\t\n\nubq#Nbq`k#1dqft#vs@ojnbwf-qfnlufphjoofgtbz#wkf?,kfbg=eb`f#leb`wjmd#qjdkw!=wl#tlqhqfgv`fpkbp#kbgfqf`wfgpklt+*8b`wjlm>allh#lebm#bqfb>>#!kww?kfbgfq\t?kwno=`lmelqneb`jmd#`llhjf-qfoz#lmklpwfg#-`vpwlnkf#tfmwavw#elqpsqfbg#Ebnjoz#b#nfbmplvw#wkfelqvnp-ellwbdf!=Nlajo@ofnfmwp!#jg>!bp#kjdkjmwfmpf..=?"..efnbof#jp#pffmjnsojfgpfw#wkfb#pwbwfbmg#kjpebpwfpwafpjgfpavwwlm\\alvmgfg!=?jnd#Jmelal{fufmwp/b#zlvmdbmg#bqfMbwjuf#`kfbsfqWjnflvwbmg#kbpfmdjmfptlm#wkf+nlpwozqjdkw9#ejmg#b#.alwwlnSqjm`f#bqfb#lenlqf#lepfbq`k\\mbwvqf/ofdboozsfqjlg/obmg#lelq#tjwkjmgv`fgsqlujmdnjppjofol`boozBdbjmpwwkf#tbzh%rvlw8s{8!=\tsvpkfg#babmglmmvnfqbo@fqwbjmJm#wkjpnlqf#jmlq#plnfmbnf#jpbmg/#jm`qltmfgJPAM#3.`qfbwfpL`wlafqnbz#mlw`fmwfq#obwf#jmGfefm`ffmb`wfgtjpk#wlaqlbgoz`llojmdlmolbg>jw-#Wkfqf`lufqNfnafqpkfjdkw#bppvnfp?kwno=\tsflsof-jm#lmf#>tjmgltellwfq\\b#dllg#qfhobnblwkfqp/wl#wkjp\\`llhjfsbmfo!=Olmglm/gfejmfp`qvpkfgabswjpn`lbpwbopwbwvp#wjwof!#nluf#wlolpw#jmafwwfq#jnsojfpqjuboqzpfqufqp#PzpwfnSfqkbspfp#bmg#`lmwfmgeoltjmdobpwfg#qjpf#jmDfmfpjpujft#leqjpjmd#pffn#wlavw#jm#ab`hjmdkf#tjoodjufm#bdjujmd#`jwjfp-eolt#le#Obwfq#boo#avwKjdktbzlmoz#azpjdm#lekf#glfpgjeefqpabwwfqz%bns8obpjmdofpwkqfbwpjmwfdfqwbhf#lmqfevpfg`boofg#>VP%bnsPff#wkfmbwjufpaz#wkjppzpwfn-kfbg#le9klufq/ofpajbmpvqmbnfbmg#boo`lnnlm,kfbgfq\\\\sbqbnpKbqubqg,sj{fo-qfnlubopl#olmdqlof#leiljmwozphzp`qbVmj`lgfaq#,=\tBwobmwbmv`ofvp@lvmwz/svqfoz#`lvmw!=fbpjoz#avjog#blm`oj`hb#djufmsljmwfqk%rvlw8fufmwp#fopf#x\tgjwjlmpmlt#wkf/#tjwk#nbm#tkllqd,Tfalmf#bmg`buboqzKf#gjfgpfbwwof33/333#xtjmgltkbuf#wlje+tjmgbmg#jwpplofoz#n%rvlw8qfmftfgGfwqljwbnlmdpwfjwkfq#wkfn#jmPfmbwlqVp?,b=?Hjmd#leEqbm`jp.sqlgv`kf#vpfgbqw#bmgkjn#bmgvpfg#azp`lqjmdbw#klnfwl#kbufqfobwfpjajojwzeb`wjlmAveebolojmh!=?tkbw#kfeqff#wl@jwz#le`lnf#jmpf`wlqp`lvmwfglmf#gbzmfqulvpprvbqf#~8je+dljm#tkbwjnd!#bojp#lmozpfbq`k,wvfpgbzollpfozPlolnlmpf{vbo#.#?b#kqnfgjvn!GL#MLW#Eqbm`f/tjwk#b#tbq#bmgpf`lmg#wbhf#b#=\t\t\tnbqhfw-kjdktbzglmf#jm`wjujwz!obpw!=laojdfgqjpf#wl!vmgfejnbgf#wl#Fbqoz#sqbjpfgjm#jwp#elq#kjpbwkofwfIvsjwfqZbkll"#wfqnfg#pl#nbmzqfbooz#p-#Wkf#b#tlnbmgjqf`w#qjdkw!#aj`z`ofb`jmd>!gbz#bmgpwbwjmdQbwkfq/kjdkfq#Leej`f#bqf#mltwjnfp/#tkfm#b#sbz#elqlm#wkjp.ojmh!=8alqgfqbqlvmg#bmmvbo#wkf#Mftsvw#wkf-`ln!#wbhjm#wlb#aqjfe+jm#wkfdqlvsp-8#tjgwkfmyznfppjnsof#jm#obwfxqfwvqmwkfqbszb#sljmwabmmjmdjmhp!=\t+*8!#qfb#sob`f_v330@bbalvw#bwq=\t\n\n``lvmw#djufp#b?P@QJSWQbjotbzwkfnfp,wlloal{AzJg+!{kvnbmp/tbw`kfpjm#plnf#je#+tj`lnjmd#elqnbwp#Vmgfq#avw#kbpkbmgfg#nbgf#azwkbm#jmefbq#legfmlwfg,jeqbnfofew#jmulowbdfjm#fb`kb%rvlw8abpf#leJm#nbmzvmgfqdlqfdjnfpb`wjlm#?,s=\t?vpwlnUb8%dw8?,jnslqwplq#wkbwnlpwoz#%bns8qf#pjyf>!?,b=?,kb#`obppsbppjufKlpw#>#TkfwkfqefqwjofUbqjlvp>X^8+ev`bnfqbp,=?,wg=b`wp#bpJm#plnf=\t\t?"lqdbmjp#?aq#,=Afjijmd`bwbo/Lgfvwp`kfvqlsfvfvphbqbdbfjodfpufmphbfpsb/]bnfmpbifvpvbqjlwqbabiln/E{j`ls/Mdjmbpjfnsqfpjpwfnbl`wvaqfgvqbmwfb/]bgjqfnsqfpbnlnfmwlmvfpwqlsqjnfqbwqbu/Epdqb`jbpmvfpwqbsql`fplfpwbglp`bojgbgsfqplmbm/Vnfqlb`vfqgln/Vpj`bnjfnaqllefqwbpbodvmlpsb/Apfpfifnsolgfqf`klbgfn/Mpsqjubglbdqfdbqfmob`fpslpjaofklwfofppfujoobsqjnfql/Vowjnlfufmwlpbq`kjul`vowvqbnvifqfpfmwqbgbbmvm`jlfnabqdlnfq`bgldqbmgfpfpwvgjlnfilqfpefaqfqlgjpf/]lwvqjpnl`/_gjdlslqwbgbfpsb`jlebnjojbbmwlmjlsfqnjwfdvbqgbqbodvmbpsqf`jlpbodvjfmpfmwjglujpjwbpw/Awvol`lml`fqpfdvmgl`lmpfileqbm`jbnjmvwlppfdvmgbwfmfnlpfef`wlpn/Mobdbpfpj/_mqfujpwbdqbmbgb`lnsqbqjmdqfpldbq`/Abb``j/_mf`vbglqrvjfmfpjm`ovplgfafq/Mnbwfqjbklnaqfpnvfpwqbslgq/Abnb/]bmb/Vowjnbfpwbnlplej`jbowbnajfmmjmd/Vmpbovglpslgfnlpnfilqbqslpjwjlmavpjmfppklnfsbdfpf`vqjwzobmdvbdfpwbmgbqg`bnsbjdmefbwvqfp`bwfdlqzf{wfqmbo`kjogqfmqfpfqufgqfpfbq`kf{`kbmdfebulqjwfwfnsobwfnjojwbqzjmgvpwqzpfquj`fpnbwfqjbosqlgv`wpy.jmgf{9`lnnfmwpplewtbqf`lnsofwf`bofmgbqsobwelqnbqwj`ofpqfrvjqfgnlufnfmwrvfpwjlmavjogjmdslojwj`pslppjaofqfojdjlmskzpj`boeffgab`hqfdjpwfqsj`wvqfpgjpbaofgsqlwl`lobvgjfm`fpfwwjmdpb`wjujwzfofnfmwpofbqmjmdbmzwkjmdbapwqb`wsqldqfpplufqujftnbdbyjmff`lmlnj`wqbjmjmdsqfppvqfubqjlvp#?pwqlmd=sqlsfqwzpklssjmdwldfwkfqbgubm`fgafkbujlqgltmolbgefbwvqfgellwaboopfof`wfgObmdvbdfgjpwbm`fqfnfnafqwqb`hjmdsbpptlqgnlgjejfgpwvgfmwpgjqf`wozejdkwjmdmlqwkfqmgbwbabpfefpwjuboaqfbhjmdol`bwjlmjmwfqmfwgqlsgltmsqb`wj`ffujgfm`fevm`wjlmnbqqjbdfqfpslmpfsqlaofnpmfdbwjufsqldqbnpbmbozpjpqfofbpfgabmmfq!=svq`kbpfsloj`jfpqfdjlmbo`qfbwjufbqdvnfmwallhnbqhqfefqqfq`kfnj`bogjujpjlm`booab`hpfsbqbwfsqlif`wp`lmeoj`wkbqgtbqfjmwfqfpwgfojufqznlvmwbjmlawbjmfg>#ebopf8elq+ubq#b``fswfg`bsb`jwz`lnsvwfqjgfmwjwzbjq`qbewfnsolzfgsqlslpfgglnfpwj`jm`ovgfpsqlujgfgklpsjwboufqwj`bo`loobspfbssqlb`ksbqwmfqpoldl!=?bgbvdkwfqbvwklq!#`vowvqboebnjojfp,jnbdfp,bppfnaozsltfqevowfb`kjmdejmjpkfggjpwqj`w`qjwj`bo`dj.ajm,svqslpfpqfrvjqfpfof`wjlmaf`lnjmdsqlujgfpb`bgfnj`f{fq`jpfb`wvbooznfgj`jmf`lmpwbmwb``jgfmwNbdbyjmfgl`vnfmwpwbqwjmdalwwln!=lapfqufg9#%rvlw8f{wfmgfgsqfujlvpPlewtbqf`vpwlnfqgf`jpjlmpwqfmdwkgfwbjofgpojdkwozsobmmjmdwf{wbqfb`vqqfm`zfufqzlmfpwqbjdkwwqbmpefqslpjwjufsqlgv`fgkfqjwbdfpkjssjmdbaplovwfqf`fjufgqfofubmwavwwlm!#ujlofm`fbmztkfqfafmfejwpobvm`kfgqf`fmwozboojbm`felooltfgnvowjsofavoofwjmjm`ovgfgl``vqqfgjmwfqmbo\'+wkjp*-qfsvaoj`=?wq=?wg`lmdqfppqf`lqgfgvowjnbwfplovwjlm?vo#jg>!gjp`lufqKlnf?,b=tfapjwfpmfwtlqhpbowklvdkfmwjqfoznfnlqjbonfppbdfp`lmwjmvfb`wjuf!=plnftkbwuj`wlqjbTfpwfqm##wjwof>!Ol`bwjlm`lmwqb`wujpjwlqpGltmolbgtjwklvw#qjdkw!=\tnfbpvqfptjgwk#>#ubqjbaofjmuloufgujqdjmjbmlqnboozkbssfmfgb``lvmwppwbmgjmdmbwjlmboQfdjpwfqsqfsbqfg`lmwqlopb``vqbwfajqwkgbzpwqbwfdzleej`jbodqbskj`p`qjnjmboslppjaoz`lmpvnfqSfqplmbopsfbhjmdubojgbwfb`kjfufg-isd!#,=nb`kjmfp?,k1=\t##hfztlqgpeqjfmgozaqlwkfqp`lnajmfglqjdjmbo`lnslpfgf{sf`wfgbgfrvbwfsbhjpwbmeloolt!#ubovbaof?,obafo=qfobwjufaqjmdjmdjm`qfbpfdlufqmlqsovdjmp,Ojpw#le#Kfbgfq!=!#mbnf>!#+%rvlw8dqbgvbwf?,kfbg=\t`lnnfq`fnbobzpjbgjqf`wlqnbjmwbjm8kfjdkw9p`kfgvof`kbmdjmdab`h#wl#`bwkloj`sbwwfqmp`lolq9# dqfbwfpwpvssojfpqfojbaof?,vo=\t\n\n?pfof`w#`jwjyfmp`olwkjmdtbw`kjmd?oj#jg>!psf`jej``bqqzjmdpfmwfm`f?`fmwfq=`lmwqbpwwkjmhjmd`bw`k+f*plvwkfqmNj`kbfo#nfq`kbmw`bqlvpfosbggjmd9jmwfqjlq-psojw+!ojybwjlmL`wlafq#*xqfwvqmjnsqlufg..%dw8\t\t`lufqbdf`kbjqnbm-smd!#,=pvaif`wpQj`kbqg#tkbwfufqsqlabaozqf`lufqzabpfabooivgdnfmw`lmmf`w--`pp!#,=#tfapjwfqfslqwfggfebvow!,=?,b=\tfof`wqj`p`lwobmg`qfbwjlmrvbmwjwz-#JPAM#3gjg#mlw#jmpwbm`f.pfbq`k.!#obmd>!psfbhfqp@lnsvwfq`lmwbjmpbq`kjufpnjmjpwfqqfb`wjlmgjp`lvmwJwbojbml`qjwfqjbpwqlmdoz9#$kwws9$p`qjsw$`lufqjmdleefqjmdbssfbqfgAqjwjpk#jgfmwjezEb`fallhmvnfqlvpufkj`ofp`lm`fqmpBnfqj`bmkbmgojmdgju#jg>!Tjoojbn#sqlujgfq\\`lmwfmwb``vqb`zpf`wjlm#bmgfqplmeof{jaof@bwfdlqzobtqfm`f?p`qjsw=obzlvw>!bssqlufg#nb{jnvnkfbgfq!=?,wbaof=Pfquj`fpkbnjowlm`vqqfmw#`bmbgjbm`kbmmfop,wkfnfp,,bqwj`oflswjlmboslqwvdboubovf>!!jmwfqubotjqfofppfmwjwofgbdfm`jfpPfbq`k!#nfbpvqfgwklvpbmgpsfmgjmd%kfoojs8mft#Gbwf!#pjyf>!sbdfMbnfnjggof!#!#,=?,b=kjggfm!=pfrvfm`fsfqplmbolufqeoltlsjmjlmpjoojmljpojmhp!=\t\n?wjwof=ufqpjlmppbwvqgbzwfqnjmbojwfnsqlsfmdjmffqpf`wjlmpgfpjdmfqsqlslpbo>!ebopf!Fpsb/]loqfofbpfppvanjw!#fq%rvlw8bggjwjlmpznswlnplqjfmwfgqfplvq`fqjdkw!=?sofbpvqfpwbwjlmpkjpwlqz-ofbujmd##alqgfq>`lmwfmwp`fmwfq!=-\t\tPlnf#gjqf`wfgpvjwbaofavodbqjb-pklt+*8gfpjdmfgDfmfqbo#`lm`fswpF{bnsofptjoojbnpLqjdjmbo!=?psbm=pfbq`k!=lsfqbwlqqfrvfpwpb#%rvlw8booltjmdGl`vnfmwqfujpjlm-#\t\tWkf#zlvqpfoe@lmwb`w#nj`kjdbmFmdojpk#`lovnajbsqjlqjwzsqjmwjmdgqjmhjmdeb`jojwzqfwvqmfg@lmwfmw#leej`fqpQvppjbm#dfmfqbwf.;;6:.2!jmgj`bwfebnjojbq#rvbojwznbqdjm93#`lmwfmwujftslqw`lmwb`wp.wjwof!=slqwbaof-ofmdwk#fojdjaofjmuloufpbwobmwj`lmolbg>!gfebvow-pvssojfgsbznfmwpdolppbqz\t\tBewfq#dvjgbm`f?,wg=?wgfm`lgjmdnjggof!=`bnf#wl#gjpsobzpp`lwwjpkilmbwkbmnbilqjwztjgdfwp-`ojmj`bowkbjobmgwfb`kfqp?kfbg=\t\nbeef`wfgpvsslqwpsljmwfq8wlPwqjmd?,pnboo=lhobklnbtjoo#af#jmufpwlq3!#bow>!klojgbzpQfplvq`foj`fmpfg#+tkj`k#-#Bewfq#`lmpjgfqujpjwjmdf{solqfqsqjnbqz#pfbq`k!#bmgqljg!rvj`hoz#nffwjmdpfpwjnbwf8qfwvqm#8`lolq9 #kfjdkw>bssqlubo/#%rvlw8#`kf`hfg-njm-ip!nbdmfwj`=?,b=?,kelqf`bpw-#Tkjof#wkvqpgbzgufqwjpf%fb`vwf8kbp@obppfubovbwflqgfqjmdf{jpwjmdsbwjfmwp#Lmojmf#`lolqbglLswjlmp!`bnsafoo?"..#fmg?,psbm=??aq#,=\t\\slsvspp`jfm`fp/%rvlw8#rvbojwz#Tjmgltp#bppjdmfgkfjdkw9#?a#`obppof%rvlw8#ubovf>!#@lnsbmzf{bnsofp?jeqbnf#afojfufpsqfpfmwpnbqpkboosbqw#le#sqlsfqoz*-\t\tWkf#wb{lmlnznv`k#le#?,psbm=\t!#gbwb.pqwvdv/Fpp`qlooWl#sqlif`w?kfbg=\tbwwlqmfzfnskbpjppslmplqpebm`zal{tlqog$p#tjogojef`kf`hfg>pfppjlmpsqldqbnns{8elmw.#Sqlif`wilvqmbopafojfufgub`bwjlmwklnsplmojdkwjmdbmg#wkf#psf`jbo#alqgfq>3`kf`hjmd?,walgz=?avwwlm#@lnsofwf`ofbqej{\t?kfbg=\tbqwj`of#?pf`wjlmejmgjmdpqlof#jm#slsvobq##L`wlafqtfapjwf#f{slpvqfvpfg#wl##`kbmdfplsfqbwfg`oj`hjmdfmwfqjmd`lnnbmgpjmelqnfg#mvnafqp##?,gju=`qfbwjmdlmPvanjwnbqzobmg`loofdfpbmbozwj`ojpwjmdp`lmwb`w-olddfgJmbgujplqzpjaojmdp`lmwfmw!p%rvlw8*p-#Wkjp#sb`hbdfp`kf`hal{pvddfpwpsqfdmbmwwlnlqqltpsb`jmd>j`lm-smdibsbmfpf`lgfabpfavwwlm!=dbnaojmdpv`k#bp#/#tkjof#?,psbm=#njpplvqjpslqwjmdwls92s{#-?,psbm=wfmpjlmptjgwk>!1obyzolbgmlufnafqvpfg#jm#kfjdkw>!`qjsw!=\t%maps8?,?wq=?wg#kfjdkw91,sqlgv`w`lvmwqz#jm`ovgf#ellwfq!#%ow8"..#wjwof!=?,irvfqz-?,elqn=\t+\vBl\bQ*+\vUmGx*kqubwphjjwbojbmlqln/Nm(ow/Pqh/Kf4K4]4C5dwbnaj/Emmlwj`jbpnfmpbifpsfqplmbpgfqf`klpmb`jlmbopfquj`jl`lmwb`wlvpvbqjlpsqldqbnbdlajfqmlfnsqfpbpbmvm`jlpubofm`jb`lolnajbgfpsv/Epgfslqwfpsqlzf`wlsqlgv`wls/Vaoj`lmlplwqlpkjpwlqjbsqfpfmwfnjoolmfpnfgjbmwfsqfdvmwbbmwfqjlqqf`vqplpsqlaofnbpbmwjbdlmvfpwqlplsjmj/_mjnsqjnjqnjfmwqbpbn/Eqj`bufmgfglqpl`jfgbgqfpsf`wlqfbojybqqfdjpwqlsbobaqbpjmwfq/Epfmwlm`fpfpsf`jbonjfnaqlpqfbojgbg`/_qglabybqbdlybs/Mdjmbppl`jbofpaolrvfbqdfpwj/_mborvjofqpjpwfnbp`jfm`jbp`lnsofwlufqpj/_m`lnsofwbfpwvgjlps/Vaoj`blaifwjulboj`bmwfavp`bglq`bmwjgbgfmwqbgbpb``jlmfpbq`kjulppvsfqjlqnbzlq/Abbofnbmjbevm`j/_m/Vowjnlpkb`jfmglbrvfoolpfgj`j/_mefqmbmglbnajfmwfeb`fallhmvfpwqbp`ojfmwfpsql`fplpabpwbmwfsqfpfmwbqfslqwbq`lmdqfplsvaoj`bq`lnfq`jl`lmwqbwli/_ufmfpgjpwqjwlw/E`mj`b`lmivmwlfmfqd/Abwqbabibqbpwvqjbpqf`jfmwfvwjojybqalofw/Ampboubglq`lqqf`wbwqbabilpsqjnfqlpmfdl`jlpojafqwbggfwboofpsbmwboobsq/_{jnlbonfq/Abbmjnbofprvj/Emfp`lqby/_mpf``j/_mavp`bmglls`jlmfpf{wfqjlq`lm`fswlwlgbu/Abdbofq/Abfp`qjajqnfgj`jmboj`fm`jb`lmpvowbbpsf`wlp`q/Awj`bg/_obqfpivpwj`jbgfafq/Mmsfq/Alglmf`fpjwbnbmwfmfqsfrvf/]lqf`jajgbwqjavmbowfmfqjef`bm`j/_m`bmbqjbpgfp`bqdbgjufqplpnboolq`bqfrvjfqfw/E`mj`lgfafq/Abujujfmgbejmbmybpbgfobmwfevm`jlmb`lmpfilpgje/A`jo`jvgbgfpbmwjdvbpbubmybgbw/Eqnjmlvmjgbgfpp/Mm`kfy`bnsb/]bplewlmj`qfujpwbp`lmwjfmfpf`wlqfpnlnfmwlpeb`vowbg`q/Egjwlgjufqpbppvsvfpwleb`wlqfppfdvmglpsfrvf/]b<_!?,pfof`w=Bvpwqbojb!#`obpp>!pjwvbwjlmbvwklqjwzelooltjmdsqjnbqjozlsfqbwjlm`kboofmdfgfufolsfgbmlmznlvpevm`wjlm#evm`wjlmp`lnsbmjfppwqv`wvqfbdqffnfmw!#wjwof>!slwfmwjbofgv`bwjlmbqdvnfmwppf`lmgbqz`lszqjdkwobmdvbdfpf{`ovpjuf`lmgjwjlm?,elqn=\tpwbwfnfmwbwwfmwjlmAjldqbskz~#fopf#x\tplovwjlmptkfm#wkf#Bmbozwj`pwfnsobwfpgbmdfqlvppbwfoojwfgl`vnfmwpsvaojpkfqjnslqwbmwsqlwlwzsfjmeovfm`f%qbrvl8?,feef`wjufdfmfqboozwqbmpelqnafbvwjevowqbmpslqwlqdbmjyfgsvaojpkfgsqlnjmfmwvmwjo#wkfwkvnambjoMbwjlmbo#-el`vp+*8lufq#wkf#njdqbwjlmbmmlvm`fgellwfq!=\tf{`fswjlmofpp#wkbmf{sfmpjufelqnbwjlmeqbnftlqhwfqqjwlqzmgj`bwjlm`vqqfmwoz`obppMbnf`qjwj`jpnwqbgjwjlmfopftkfqfBof{bmgfqbssljmwfgnbwfqjbopaqlbg`bpwnfmwjlmfgbeejojbwf?,lswjlm=wqfbwnfmwgjeefqfmw,gfebvow-Sqfpjgfmwlm`oj`h>!ajldqbskzlwkfqtjpfsfqnbmfmwEqbm/KbjpKlooztllgf{sbmpjlmpwbmgbqgp?,pwzof=\tqfgv`wjlmGf`fnafq#sqfefqqfg@bnaqjgdflsslmfmwpAvpjmfpp#`lmevpjlm=\t?wjwof=sqfpfmwfgf{sobjmfgglfp#mlw#tlqogtjgfjmwfqeb`fslpjwjlmpmftpsbsfq?,wbaof=\tnlvmwbjmpojhf#wkf#fppfmwjboejmbm`jbopfof`wjlmb`wjlm>!,babmglmfgFgv`bwjlmsbqpfJmw+pwbajojwzvmbaof#wl?,wjwof=\tqfobwjlmpMlwf#wkbwfeej`jfmwsfqelqnfgwtl#zfbqpPjm`f#wkfwkfqfelqftqbssfq!=bowfqmbwfjm`qfbpfgAbwwof#lesfq`fjufgwqzjmd#wlmf`fppbqzslqwqbzfgfof`wjlmpFojybafwk?,jeqbnf=gjp`lufqzjmpvqbm`fp-ofmdwk8ofdfmgbqzDfldqbskz`bmgjgbwf`lqslqbwfplnfwjnfppfquj`fp-jmkfqjwfg?,pwqlmd=@lnnvmjwzqfojdjlvpol`bwjlmp@lnnjwwffavjogjmdpwkf#tlqogml#olmdfqafdjmmjmdqfefqfm`f`bmmlw#afeqfrvfm`zwzsj`boozjmwl#wkf#qfobwjuf8qf`lqgjmdsqfpjgfmwjmjwjboozwf`kmjrvfwkf#lwkfqjw#`bm#aff{jpwfm`fvmgfqojmfwkjp#wjnfwfofsklmfjwfnp`lsfsqb`wj`fpbgubmwbdf*8qfwvqm#Elq#lwkfqsqlujgjmdgfnl`qb`zalwk#wkf#f{wfmpjufpveefqjmdpvsslqwfg`lnsvwfqp#evm`wjlmsqb`wj`bopbjg#wkbwjw#nbz#afFmdojpk?,eqln#wkf#p`kfgvofggltmolbgp?,obafo=\tpvpsf`wfgnbqdjm9#3psjqjwvbo?,kfbg=\t\tnj`qlplewdqbgvboozgjp`vppfgkf#af`bnff{f`vwjufirvfqz-ipklvpfklog`lmejqnfgsvq`kbpfgojwfqboozgfpwqlzfgvs#wl#wkfubqjbwjlmqfnbjmjmdjw#jp#mlw`fmwvqjfpIbsbmfpf#bnlmd#wkf`lnsofwfgbodlqjwknjmwfqfpwpqfafoojlmvmgfejmfgfm`lvqbdfqfpjybaofjmuloujmdpfmpjwjufvmjufqpbosqlujpjlm+bowklvdkefbwvqjmd`lmgv`wfg*/#tkj`k#`lmwjmvfg.kfbgfq!=Efaqvbqz#mvnfqlvp#lufqeolt9`lnslmfmweqbdnfmwpf{`foofmw`lopsbm>!wf`kmj`bomfbq#wkf#Bgubm`fg#plvq`f#lef{sqfppfgKlmd#Hlmd#Eb`fallhnvowjsof#nf`kbmjpnfofubwjlmleefmpjuf?,elqn=\t\npslmplqfggl`vnfmw-lq#%rvlw8wkfqf#bqfwklpf#tklnlufnfmwpsql`fppfpgjeej`vowpvanjwwfgqf`lnnfmg`lmujm`fgsqlnlwjmd!#tjgwk>!-qfsob`f+`obppj`bo`lbojwjlmkjp#ejqpwgf`jpjlmpbppjpwbmwjmgj`bwfgfulovwjlm.tqbssfq!fmlvdk#wlbolmd#wkfgfojufqfg..=\t?"..Bnfqj`bm#sqlwf`wfgMlufnafq#?,pwzof=?evqmjwvqfJmwfqmfw##lmaovq>!pvpsfmgfgqf`jsjfmwabpfg#lm#Nlqflufq/balojpkfg`loof`wfgtfqf#nbgffnlwjlmbofnfqdfm`zmbqqbwjufbgul`bwfps{8alqgfq`lnnjwwfggjq>!owq!fnsolzffpqfpfbq`k-#pfof`wfgpv``fpplq`vpwlnfqpgjpsobzfgPfswfnafqbgg@obpp+Eb`fallh#pvddfpwfgbmg#obwfqlsfqbwjmdfobalqbwfPlnfwjnfpJmpwjwvwf`fqwbjmozjmpwboofgelooltfqpIfqvpbofnwkfz#kbuf`lnsvwjmddfmfqbwfgsqlujm`fpdvbqbmwffbqajwqbqzqf`ldmjyftbmwfg#wls{8tjgwk9wkflqz#leafkbujlvqTkjof#wkffpwjnbwfgafdbm#wl#jw#af`bnfnbdmjwvgfnvpw#kbufnlqf#wkbmGjqf`wlqzf{wfmpjlmpf`qfwbqzmbwvqboozl``vqqjmdubqjbaofpdjufm#wkfsobwelqn-?,obafo=?ebjofg#wl`lnslvmgphjmgp#le#pl`jfwjfpbolmdpjgf#..%dw8\t\tplvwktfpwwkf#qjdkwqbgjbwjlmnbz#kbuf#vmfp`bsf+pslhfm#jm!#kqfe>!,sqldqbnnflmoz#wkf#`lnf#eqlngjqf`wlqzavqjfg#jmb#pjnjobqwkfz#tfqf?,elmw=?,Mlqtfdjbmpsf`jejfgsqlgv`jmdsbppfmdfq+mft#Gbwfwfnslqbqzej`wjlmboBewfq#wkffrvbwjlmpgltmolbg-qfdvobqozgfufolsfqbaluf#wkfojmhfg#wlskfmlnfmbsfqjlg#lewllowjs!=pvapwbm`fbvwlnbwj`bpsf`w#leBnlmd#wkf`lmmf`wfgfpwjnbwfpBjq#Elq`fpzpwfn#lelaif`wjufjnnfgjbwfnbhjmd#jwsbjmwjmdp`lmrvfqfgbqf#pwjoosql`fgvqfdqltwk#lekfbgfg#azFvqlsfbm#gjujpjlmpnlof`vofpeqbm`kjpfjmwfmwjlmbwwqb`wfg`kjogkllgbopl#vpfggfgj`bwfgpjmdbslqfgfdqff#leebwkfq#le`lmeoj`wp?,b=?,s=\t`bnf#eqlntfqf#vpfgmlwf#wkbwqf`fjujmdF{f`vwjuffufm#nlqfb``fpp#wl`lnnbmgfqSlojwj`bonvpj`jbmpgfoj`jlvpsqjplmfqpbgufmw#leVWE.;!#,=?"X@GBWBX!=@lmwb`wPlvwkfqm#ad`lolq>!pfqjfp#le-#Jw#tbp#jm#Fvqlsfsfqnjwwfgubojgbwf-bssfbqjmdleej`jboppfqjlvpoz.obmdvbdfjmjwjbwfgf{wfmgjmdolmd.wfqnjmeobwjlmpv`k#wkbwdfw@llhjfnbqhfg#az?,avwwlm=jnsofnfmwavw#jw#jpjm`qfbpfpgltm#wkf#qfrvjqjmdgfsfmgfmw..=\t?"..#jmwfqujftTjwk#wkf#`lsjfp#le`lmpfmpvptbp#avjowUfmfyvfob+elqnfqozwkf#pwbwfsfqplmmfopwqbwfdj`ebulvq#lejmufmwjlmTjhjsfgjb`lmwjmfmwujqwvbooztkj`k#tbpsqjm`jsof@lnsofwf#jgfmwj`bopklt#wkbwsqjnjwjufbtbz#eqlnnlof`vobqsqf`jpfozgjpploufgVmgfq#wkfufqpjlm>!=%maps8?,Jw#jp#wkf#Wkjp#jp#tjoo#kbuflqdbmjpnpplnf#wjnfEqjfgqj`ktbp#ejqpwwkf#lmoz#eb`w#wkbwelqn#jg>!sqf`fgjmdWf`kmj`boskzpj`jpwl``vqp#jmmbujdbwlqpf`wjlm!=psbm#jg>!plvdkw#wlafolt#wkfpvqujujmd~?,pwzof=kjp#gfbwkbp#jm#wkf`bvpfg#azsbqwjboozf{jpwjmd#vpjmd#wkftbp#djufmb#ojpw#leofufop#lemlwjlm#leLeej`jbo#gjpnjppfgp`jfmwjpwqfpfnaofpgvsoj`bwff{solpjufqf`lufqfgboo#lwkfqdboofqjfpxsbggjmd9sflsof#leqfdjlm#lebggqfppfpbppl`jbwfjnd#bow>!jm#nlgfqmpklvog#afnfwklg#leqfslqwjmdwjnfpwbnsmffgfg#wlwkf#Dqfbwqfdbqgjmdpffnfg#wlujftfg#bpjnsb`w#lmjgfb#wkbwwkf#Tlqogkfjdkw#lef{sbmgjmdWkfpf#bqf`vqqfmw!=`bqfevooznbjmwbjmp`kbqdf#le@obppj`bobggqfppfgsqfgj`wfgltmfqpkjs?gju#jg>!qjdkw!=\tqfpjgfm`fofbuf#wkf`lmwfmw!=bqf#lewfm##~*+*8\tsqlabaoz#Sqlefpplq.avwwlm!#qfpslmgfgpbzp#wkbwkbg#wl#afsob`fg#jmKvmdbqjbmpwbwvp#lepfqufp#bpVmjufqpbof{f`vwjlmbddqfdbwfelq#tkj`kjmef`wjlmbdqffg#wlkltfufq/#slsvobq!=sob`fg#lm`lmpwqv`wfof`wlqbopznalo#lejm`ovgjmdqfwvqm#wlbq`kjwf`w@kqjpwjbmsqfujlvp#ojujmd#jmfbpjfq#wlsqlefpplq\t%ow8"..#feef`w#lebmbozwj`ptbp#wbhfmtkfqf#wkfwllh#lufqafojfe#jmBeqjhbbmpbp#ebq#bpsqfufmwfgtlqh#tjwkb#psf`jbo?ejfogpfw@kqjpwnbpQfwqjfufg\t\tJm#wkf#ab`h#jmwlmlqwkfbpwnbdbyjmfp=?pwqlmd=`lnnjwwffdlufqmjmddqlvsp#lepwlqfg#jmfpwbaojpkb#dfmfqbojwp#ejqpwwkfjq#ltmslsvobwfgbm#laif`w@bqjaafbmboolt#wkfgjpwqj`wptjp`lmpjmol`bwjlm-8#tjgwk9#jmkbajwfgPl`jbojpwIbmvbqz#2?,ellwfq=pjnjobqoz`klj`f#lewkf#pbnf#psf`jej`#avpjmfpp#Wkf#ejqpw-ofmdwk8#gfpjqf#wlgfbo#tjwkpjm`f#wkfvpfqBdfmw`lm`fjufgjmgf{-sksbp#%rvlw8fmdbdf#jmqf`fmwoz/eft#zfbqptfqf#bopl\t?kfbg=\t?fgjwfg#azbqf#hmltm`jwjfp#jmb``fpphfz`lmgfnmfgbopl#kbufpfquj`fp/ebnjoz#leP`kllo#le`lmufqwfgmbwvqf#le#obmdvbdfnjmjpwfqp?,laif`w=wkfqf#jp#b#slsvobqpfrvfm`fpbgul`bwfgWkfz#tfqfbmz#lwkfqol`bwjlm>fmwfq#wkfnv`k#nlqfqfeof`wfgtbp#mbnfglqjdjmbo#b#wzsj`botkfm#wkfzfmdjmffqp`lvog#mlwqfpjgfmwptfgmfpgbzwkf#wkjqg#sqlgv`wpIbmvbqz#1tkbw#wkfzb#`fqwbjmqfb`wjlmpsql`fpplqbewfq#kjpwkf#obpw#`lmwbjmfg!=?,gju=\t?,b=?,wg=gfsfmg#lmpfbq`k!=\tsjf`fp#le`lnsfwjmdQfefqfm`fwfmmfppfftkj`k#kbp#ufqpjlm>?,psbm=#??,kfbgfq=djufp#wkfkjpwlqjbmubovf>!!=sbggjmd93ujft#wkbwwldfwkfq/wkf#nlpw#tbp#elvmgpvapfw#lebwwb`h#lm`kjogqfm/sljmwp#lesfqplmbo#slpjwjlm9boofdfgoz@ofufobmgtbp#obwfqbmg#bewfqbqf#djufmtbp#pwjoop`qloojmdgfpjdm#lenbhfp#wkfnv`k#ofppBnfqj`bmp-\t\tBewfq#/#avw#wkfNvpfvn#leolvjpjbmb+eqln#wkfnjmmfplwbsbqwj`ofpb#sql`fppGlnjmj`bmulovnf#leqfwvqmjmdgfefmpjuf33s{qjdknbgf#eqlnnlvpflufq!#pwzof>!pwbwfp#le+tkj`k#jp`lmwjmvfpEqbm`jp`lavjogjmd#tjwklvw#btjwk#plnftkl#tlvogb#elqn#leb#sbqw#leafelqf#jwhmltm#bp##Pfquj`fpol`bwjlm#bmg#lewfmnfbpvqjmdbmg#jw#jpsbsfqab`hubovfp#le\t?wjwof=>#tjmglt-gfwfqnjmffq%rvlw8#sobzfg#azbmg#fbqoz?,`fmwfq=eqln#wkjpwkf#wkqffsltfq#bmgle#%rvlw8jmmfqKWNO?b#kqfe>!z9jmojmf8@kvq`k#lewkf#fufmwufqz#kjdkleej`jbo#.kfjdkw9#`lmwfmw>!,`dj.ajm,wl#`qfbwfbeqjhbbmpfpsfqbmwleqbm/Kbjpobwujf)Mvojfwvuj)_(`f)Mwjmb(af)Mwjmb\fUh\fT{\fTN\n{I\np@Fr\vBl\bQ\tA{\vUmGx\tA{ypYA\0zX\bTV\bWl\bUdBM\vB{\npV\v@xB\\\np@DbGz\tal\npa\tfM\tuD\bV~mx\vQ}\ndS\tp\\\bVK\bS]\bU|oD\tkV\ved\vHR\nb~M`\nJpoD|Q\nLPSw\bTl\nAI\nxC\bWt\tBqF`Cm\vLm\tKx\t}t\bPv\ny\\\naB\tV\nZdXUli\tfr\ti@\tBHBDBV\t`V\n[]\tp_\tTn\n~A\nxR\tuD\t`{\bV@\tTn\tHK\tAJ\vxsZf\nqIZf\vBM\v|j\t}t\bSM\nmC\vQ}pfquj`jlpbqw/A`volbqdfmwjmbabq`folmb`vborvjfqsvaoj`bglsqlgv`wlpslo/Awj`bqfpsvfpwbtjhjsfgjbpjdvjfmwfa/Vprvfgb`lnvmjgbgpfdvqjgbgsqjm`jsbosqfdvmwbp`lmwfmjglqfpslmgfqufmfyvfobsqlaofnbpgj`jfnaqfqfob`j/_mmlujfnaqfpjnjobqfpsqlzf`wlpsqldqbnbpjmpwjwvwlb`wjujgbgfm`vfmwqbf`lmln/Abjn/Mdfmfp`lmwb`wbqgfp`bqdbqmf`fpbqjlbwfm`j/_mwfo/Eelml`lnjpj/_m`bm`jlmfp`bsb`jgbgfm`lmwqbqbm/Mojpjpebulqjwlpw/Eqnjmlpsqlujm`jbfwjrvfwbpfofnfmwlpevm`jlmfpqfpvowbgl`bq/M`wfqsqlsjfgbgsqjm`jsjlmf`fpjgbgnvmj`jsbo`qfb`j/_mgfp`bqdbpsqfpfm`jb`lnfq`jbolsjmjlmfpfifq`j`jlfgjwlqjbopbobnbm`bdlmy/Mofygl`vnfmwlsfo/A`vobqf`jfmwfpdfmfqbofpwbqqbdlmbsq/M`wj`bmlufgbgfpsqlsvfpwbsb`jfmwfpw/E`mj`bplaifwjulp`lmwb`wlp\fHB\fIk\fHn\fH^\fHS\fHc\fHU\fId\fHn\fH{\fHC\fHR\fHT\fHR\fHI\fHc\fHY\fHn\fH\\\fHU\fIk\fHy\fIg\fHd\fHy\fIm\fHw\fH\\\fHU\fHR\fH@\fHR\fHJ\fHy\fHU\fHR\fHT\fHA\fIl\fHU\fIm\fHc\fH\\\fHU\fIl\fHB\fId\fHn\fHJ\fHS\fHD\fH@\fHR\fHHgjsolgl`p\fHT\fHB\fHC\fH\\\fIn\fHF\fHD\fHR\fHB\fHF\fHH\fHR\fHG\fHS\fH\\\fHx\fHT\fHH\fHH\fH\\\fHU\fH^\fIg\fH{\fHU\fIm\fHj\fH@\fHR\fH\\\fHJ\fIk\fHZ\fHU\fIm\fHd\fHz\fIk\fH^\fHC\fHJ\fHS\fHy\fHR\fHB\fHY\fIk\fH@\fHH\fIl\fHD\fH@\fIl\fHv\fHB\fI`\fHH\fHT\fHR\fH^\fH^\fIk\fHz\fHp\fIe\fH@\fHB\fHJ\fHJ\fHH\fHI\fHR\fHD\fHU\fIl\fHZ\fHU\fH\\\fHi\fH^\fH{\fHy\fHA\fIl\fHD\fH{\fH\\\fHF\fHR\fHT\fH\\\fHR\fHH\fHy\fHS\fHc\fHe\fHT\fIk\fH{\fHC\fIl\fHU\fIn\fHm\fHj\fH{\fIk\fHs\fIl\fHB\fHz\fIg\fHp\fHy\fHR\fH\\\fHi\fHA\fIl\fH{\fHC\fIk\fHH\fIm\fHB\fHY\fIg\fHs\fHJ\fIk\fHn\fHi\fH{\fH\\\fH|\fHT\fIk\fHB\fIk\fH^\fH^\fH{\fHR\fHU\fHR\fH^\fHf\fHF\fH\\\fHv\fHR\fH\\\fH|\fHT\fHR\fHJ\fIk\fH\\\fHp\fHS\fHT\fHJ\fHS\fH^\fH@\fHn\fHJ\fH@\fHD\fHR\fHU\fIn\fHn\fH^\fHR\fHz\fHp\fIl\fHH\fH@\fHs\fHD\fHB\fHS\fH^\fHk\fHT\fIk\fHj\fHD\fIk\fHD\fHC\fHR\fHy\fIm\fH^\fH^\fIe\fH{\fHA\fHR\fH{\fH\\\fIk\fH^\fHp\fH{\fHU\fH\\\fHR\fHB\fH^\fH{\fIk\fHF\fIk\fHp\fHU\fHR\fHI\fHk\fHT\fIl\fHT\fHU\fIl\fHy\fH^\fHR\fHL\fIl\fHy\fHU\fHR\fHm\fHJ\fIn\fH\\\fHH\fHU\fHH\fHT\fHR\fHH\fHC\fHR\fHJ\fHj\fHC\fHR\fHF\fHR\fHy\fHy\fI`\fHD\fHZ\fHR\fHB\fHJ\fIk\fHz\fHC\fHU\fIl\fH\\\fHR\fHC\fHz\fIm\fHJ\fH^\fH{\fIl`bwfdlqjfpf{sfqjfm`f?,wjwof=\t@lszqjdkw#ibubp`qjsw`lmgjwjlmpfufqzwkjmd?s#`obpp>!wf`kmloldzab`hdqlvmg?b#`obpp>!nbmbdfnfmw%`lsz8#132ibubP`qjsw`kbqb`wfqpaqfbg`qvnawkfnpfoufpklqjylmwbodlufqmnfmw@bojelqmjbb`wjujwjfpgjp`lufqfgMbujdbwjlmwqbmpjwjlm`lmmf`wjlmmbujdbwjlmbssfbqbm`f?,wjwof=?n`kf`hal{!#wf`kmjrvfpsqlwf`wjlmbssbqfmwozbp#tfoo#bpvmw$/#$VB.qfplovwjlmlsfqbwjlmpwfofujpjlmwqbmpobwfgTbpkjmdwlmmbujdbwlq-#>#tjmglt-jnsqfppjlm%ow8aq%dw8ojwfqbwvqfslsvobwjlmad`lolq>! fpsf`jbooz#`lmwfmw>!sqlgv`wjlmmftpofwwfqsqlsfqwjfpgfejmjwjlmofbgfqpkjsWf`kmloldzSbqojbnfmw`lnsbqjplmvo#`obpp>!-jmgf{Le+!`lm`ovpjlmgjp`vppjlm`lnslmfmwpajloldj`boQfulovwjlm\\`lmwbjmfqvmgfqpwllgmlp`qjsw=?sfqnjppjlmfb`k#lwkfqbwnlpskfqf#lmel`vp>!?elqn#jg>!sql`fppjmdwkjp-ubovfdfmfqbwjlm@lmefqfm`fpvapfrvfmwtfoo.hmltmubqjbwjlmpqfsvwbwjlmskfmlnfmlmgjp`jsojmfoldl-smd!#+gl`vnfmw/alvmgbqjfpf{sqfppjlmpfwwofnfmwAb`hdqlvmglvw#le#wkffmwfqsqjpf+!kwwsp9!#vmfp`bsf+!sbpptlqg!#gfnl`qbwj`?b#kqfe>!,tqbssfq!=\tnfnafqpkjsojmdvjpwj`s{8sbggjmdskjolplskzbppjpwbm`fvmjufqpjwzeb`jojwjfpqf`ldmjyfgsqfefqfm`fje#+wzsflenbjmwbjmfgul`bavobqzkzslwkfpjp-pvanjw+*8%bns8maps8bmmlwbwjlmafkjmg#wkfElvmgbwjlmsvaojpkfq!bppvnswjlmjmwqlgv`fg`lqqvswjlmp`jfmwjpwpf{soj`jwozjmpwfbg#legjnfmpjlmp#lm@oj`h>!`lmpjgfqfggfsbqwnfmwl``vsbwjlmpllm#bewfqjmufpwnfmwsqlmlvm`fgjgfmwjejfgf{sfqjnfmwNbmbdfnfmwdfldqbskj`!#kfjdkw>!ojmh#qfo>!-qfsob`f+,gfsqfppjlm`lmefqfm`fsvmjpknfmwfojnjmbwfgqfpjpwbm`fbgbswbwjlmlsslpjwjlmtfoo#hmltmpvssofnfmwgfwfqnjmfgk2#`obpp>!3s{8nbqdjmnf`kbmj`bopwbwjpwj`p`fofaqbwfgDlufqmnfmw\t\tGvqjmd#wgfufolsfqpbqwjej`jbofrvjubofmwlqjdjmbwfg@lnnjppjlmbwwb`knfmw?psbm#jg>!wkfqf#tfqfMfgfqobmgpafzlmg#wkfqfdjpwfqfgilvqmbojpweqfrvfmwozboo#le#wkfobmd>!fm!#?,pwzof=\tbaplovwf8#pvsslqwjmdf{wqfnfoz#nbjmpwqfbn?,pwqlmd=#slsvobqjwzfnsolznfmw?,wbaof=\t#`lopsbm>!?,elqn=\t##`lmufqpjlmbalvw#wkf#?,s=?,gju=jmwfdqbwfg!#obmd>!fmSlqwvdvfpfpvapwjwvwfjmgjujgvbojnslppjaofnvowjnfgjbbonlpw#boos{#plojg# bsbqw#eqlnpvaif`w#wljm#Fmdojpk`qjwj`jyfgf{`fsw#elqdvjgfojmfplqjdjmboozqfnbqhbaofwkf#pf`lmgk1#`obpp>!?b#wjwof>!+jm`ovgjmdsbqbnfwfqpsqlkjajwfg>#!kwws9,,gj`wjlmbqzsfq`fswjlmqfulovwjlmelvmgbwjlms{8kfjdkw9pv``fppevopvsslqwfqpnjoofmmjvnkjp#ebwkfqwkf#%rvlw8ml.qfsfbw8`lnnfq`jbojmgvpwqjbofm`lvqbdfgbnlvmw#le#vmleej`jbofeej`jfm`zQfefqfm`fp`llqgjmbwfgjp`objnfqf{sfgjwjlmgfufolsjmd`bo`vobwfgpjnsojejfgofdjwjnbwfpvapwqjmd+3!#`obpp>!`lnsofwfozjoovpwqbwfejuf#zfbqpjmpwqvnfmwSvaojpkjmd2!#`obpp>!spz`kloldz`lmejgfm`fmvnafq#le#bapfm`f#leel`vpfg#lmiljmfg#wkfpwqv`wvqfpsqfujlvpoz=?,jeqbnf=lm`f#bdbjmavw#qbwkfqjnnjdqbmwple#`lvqpf/b#dqlvs#leOjwfqbwvqfVmojhf#wkf?,b=%maps8\tevm`wjlm#jw#tbp#wkf@lmufmwjlmbvwlnlajofSqlwfpwbmwbddqfppjufbewfq#wkf#Pjnjobqoz/!#,=?,gju=`loof`wjlm\tevm`wjlmujpjajojwzwkf#vpf#leulovmwffqpbwwqb`wjlmvmgfq#wkf#wkqfbwfmfg)?"X@GBWBXjnslqwbm`fjm#dfmfqbowkf#obwwfq?,elqn=\t?,-jmgf{Le+$j#>#38#j#?gjeefqfm`fgfulwfg#wlwqbgjwjlmppfbq`k#elqvowjnbwfozwlvqmbnfmwbwwqjavwfppl.`boofg#~\t?,pwzof=fubovbwjlmfnskbpjyfgb``fppjaof?,pf`wjlm=pv``fppjlmbolmd#tjwkNfbmtkjof/jmgvpwqjfp?,b=?aq#,=kbp#af`lnfbpsf`wp#leWfofujpjlmpveej`jfmwabphfwabooalwk#pjgfp`lmwjmvjmdbm#bqwj`of?jnd#bow>!bgufmwvqfpkjp#nlwkfqnbm`kfpwfqsqjm`jsofpsbqwj`vobq`lnnfmwbqzfeef`wp#legf`jgfg#wl!=?pwqlmd=svaojpkfqpIlvqmbo#legjeej`vowzeb`jojwbwfb``fswbaofpwzof-`pp!\nevm`wjlm#jmmlubwjlm=@lszqjdkwpjwvbwjlmptlvog#kbufavpjmfppfpGj`wjlmbqzpwbwfnfmwplewfm#vpfgsfqpjpwfmwjm#Ibmvbqz`lnsqjpjmd?,wjwof=\t\ngjsolnbwj``lmwbjmjmdsfqelqnjmdf{wfmpjlmpnbz#mlw#af`lm`fsw#le#lm`oj`h>!Jw#jp#boplejmbm`jbo#nbhjmd#wkfOv{fnalvqdbggjwjlmbobqf#`boofgfmdbdfg#jm!p`qjsw!*8avw#jw#tbpfof`wqlmj`lmpvanjw>!\t?"..#Fmg#fof`wqj`boleej`jboozpvddfpwjlmwls#le#wkfvmojhf#wkfBvpwqbojbmLqjdjmboozqfefqfm`fp\t?,kfbg=\tqf`ldmjpfgjmjwjbojyfojnjwfg#wlBof{bmgqjbqfwjqfnfmwBgufmwvqfpelvq#zfbqp\t\t%ow8"..#jm`qfbpjmdgf`lqbwjlmk0#`obpp>!lqjdjmp#lelaojdbwjlmqfdvobwjlm`obppjejfg+evm`wjlm+bgubmwbdfpafjmd#wkf#kjpwlqjbmp?abpf#kqfeqfsfbwfgoztjoojmd#wl`lnsbqbaofgfpjdmbwfgmlnjmbwjlmevm`wjlmbojmpjgf#wkfqfufobwjlmfmg#le#wkfp#elq#wkf#bvwklqjyfgqfevpfg#wlwbhf#sob`fbvwlmlnlvp`lnsqlnjpfslojwj`bo#qfpwbvqbmwwtl#le#wkfEfaqvbqz#1rvbojwz#leptelaif`w-vmgfqpwbmgmfbqoz#bootqjwwfm#azjmwfqujftp!#tjgwk>!2tjwkgqbtboeolbw9ofewjp#vpvbooz`bmgjgbwfpmftpsbsfqpnzpwfqjlvpGfsbqwnfmwafpw#hmltmsbqojbnfmwpvssqfppfg`lmufmjfmwqfnfnafqfggjeefqfmw#pzpwfnbwj`kbp#ofg#wlsqlsbdbmgb`lmwqloofgjmeovfm`fp`fqfnlmjbosql`objnfgSqlwf`wjlmoj#`obpp>!P`jfmwjej``obpp>!ml.wqbgfnbqhpnlqf#wkbm#tjgfpsqfbgOjafqbwjlmwllh#sob`fgbz#le#wkfbp#olmd#bpjnsqjplmfgBggjwjlmbo\t?kfbg=\t?nObalqbwlqzMlufnafq#1f{`fswjlmpJmgvpwqjboubqjfwz#leeolbw9#ofeGvqjmd#wkfbppfppnfmwkbuf#affm#gfbop#tjwkPwbwjpwj`pl``vqqfm`f,vo=?,gju=`ofbqej{!=wkf#svaoj`nbmz#zfbqptkj`k#tfqflufq#wjnf/pzmlmznlvp`lmwfmw!=\tsqfpvnbaozkjp#ebnjozvpfqBdfmw-vmf{sf`wfgjm`ovgjmd#`kboofmdfgb#njmlqjwzvmgfejmfg!afolmdp#wlwbhfm#eqlnjm#L`wlafqslpjwjlm9#pbjg#wl#afqfojdjlvp#Efgfqbwjlm#qltpsbm>!lmoz#b#eftnfbmw#wkbwofg#wl#wkf..=\t?gju#?ejfogpfw=Bq`kajpkls#`obpp>!mlafjmd#vpfgbssqlb`kfpsqjujofdfpmlp`qjsw=\tqfpvowp#jmnbz#af#wkfFbpwfq#fddnf`kbmjpnpqfbplmbaofSlsvobwjlm@loof`wjlmpfof`wfg!=mlp`qjsw=,jmgf{-sksbqqjubo#le.ippgh$**8nbmbdfg#wljm`lnsofwf`bpvbowjfp`lnsofwjlm@kqjpwjbmpPfswfnafq#bqjwknfwj`sql`fgvqfpnjdkw#kbufSqlgv`wjlmjw#bssfbqpSkjolplskzeqjfmgpkjsofbgjmd#wldjujmd#wkfwltbqg#wkfdvbqbmwffggl`vnfmwfg`lolq9 333ujgfl#dbnf`lnnjppjlmqfeof`wjmd`kbmdf#wkfbppl`jbwfgpbmp.pfqjelmhfzsqfpp8#sbggjmd9Kf#tbp#wkfvmgfqozjmdwzsj`booz#/#bmg#wkf#pq`Fofnfmwpv``fppjufpjm`f#wkf#pklvog#af#mfwtlqhjmdb``lvmwjmdvpf#le#wkfoltfq#wkbmpkltp#wkbw?,psbm=\t\n\n`lnsobjmwp`lmwjmvlvprvbmwjwjfpbpwqlmlnfqkf#gjg#mlwgvf#wl#jwpbssojfg#wlbm#bufqbdffeelqwp#wlwkf#evwvqfbwwfnsw#wlWkfqfelqf/`bsbajojwzQfsvaoj`bmtbp#elqnfgFof`wqlmj`hjolnfwfqp`kboofmdfpsvaojpkjmdwkf#elqnfqjmgjdfmlvpgjqf`wjlmppvapjgjbqz`lmpsjqb`zgfwbjop#lebmg#jm#wkfbeelqgbaofpvapwbm`fpqfbplm#elq`lmufmwjlmjwfnwzsf>!baplovwfozpvsslpfgozqfnbjmfg#bbwwqb`wjufwqbufoojmdpfsbqbwfozel`vpfp#lmfofnfmwbqzbssoj`baofelvmg#wkbwpwzofpkffwnbmvp`qjswpwbmgp#elq#ml.qfsfbw+plnfwjnfp@lnnfq`jbojm#Bnfqj`bvmgfqwbhfmrvbqwfq#lebm#f{bnsofsfqplmboozjmgf{-sks!owqOjfvwfmbmw\t?gju#jg>!wkfz#tlvogbajojwz#lenbgf#vs#lemlwfg#wkbw`ofbq#wkbwbqdvf#wkbwwl#bmlwkfq`kjogqfm$psvqslpf#leelqnvobwfgabpfg#vslmwkf#qfdjlmpvaif`w#lesbppfmdfqpslppfppjlm-\t\tJm#wkf#Afelqf#wkfbewfqtbqgp`vqqfmwoz#b`qlpp#wkfp`jfmwjej``lnnvmjwz-`bsjwbojpnjm#Dfqnbmzqjdkw.tjmdwkf#pzpwfnPl`jfwz#leslojwj`jbmgjqf`wjlm9tfmw#lm#wlqfnlubo#le#Mft#Zlqh#bsbqwnfmwpjmgj`bwjlmgvqjmd#wkfvmofpp#wkfkjpwlqj`bokbg#affm#bgfejmjwjufjmdqfgjfmwbwwfmgbm`f@fmwfq#elqsqlnjmfm`fqfbgzPwbwfpwqbwfdjfpavw#jm#wkfbp#sbqw#le`lmpwjwvwf`objn#wkbwobalqbwlqz`lnsbwjaofebjovqf#le/#pv`k#bp#afdbm#tjwkvpjmd#wkf#wl#sqlujgfefbwvqf#leeqln#tkj`k,!#`obpp>!dfloldj`bopfufqbo#legfojafqbwfjnslqwbmw#klogp#wkbwjmd%rvlw8#ubojdm>wlswkf#Dfqnbmlvwpjgf#lemfdlwjbwfgkjp#`bqffqpfsbqbwjlmjg>!pfbq`ktbp#`boofgwkf#elvqwkqf`qfbwjlmlwkfq#wkbmsqfufmwjlmtkjof#wkf#fgv`bwjlm/`lmmf`wjmdb``vqbwfoztfqf#avjowtbp#hjoofgbdqffnfmwpnv`k#nlqf#Gvf#wl#wkftjgwk9#233plnf#lwkfqHjmdgln#lewkf#fmwjqfebnlvp#elqwl#`lmmf`wlaif`wjufpwkf#Eqfm`ksflsof#bmgefbwvqfg!=jp#pbjg#wlpwqv`wvqboqfefqfmgvnnlpw#lewfmb#pfsbqbwf.=\t?gju#jg#Leej`jbo#tlqogtjgf-bqjb.obafowkf#sobmfwbmg#jw#tbpg!#ubovf>!ollhjmd#bwafmfej`jbobqf#jm#wkfnlmjwlqjmdqfslqwfgozwkf#nlgfqmtlqhjmd#lmbooltfg#wltkfqf#wkf#jmmlubwjuf?,b=?,gju=plvmgwqb`hpfbq`kElqnwfmg#wl#afjmsvw#jg>!lsfmjmd#leqfpwqj`wfgbglswfg#azbggqfppjmdwkfloldjbmnfwklgp#leubqjbmw#le@kqjpwjbm#ufqz#obqdfbvwlnlwjufaz#ebq#wkfqbmdf#eqlnsvqpvjw#leeloolt#wkfaqlvdkw#wljm#Fmdobmgbdqff#wkbwb``vpfg#le`lnfp#eqlnsqfufmwjmdgju#pwzof>kjp#lq#kfqwqfnfmglvpeqffgln#le`lm`fqmjmd3#2fn#2fn8Abphfwaboo,pwzof-`ppbm#fbqojfqfufm#bewfq,!#wjwof>!-`ln,jmgf{wbhjmd#wkfsjwwpavqdk`lmwfmw!=?p`qjsw=+ewvqmfg#lvwkbujmd#wkf?,psbm=\t#l``bpjlmboaf`bvpf#jwpwbqwfg#wlskzpj`booz=?,gju=\t##`qfbwfg#az@vqqfmwoz/#ad`lolq>!wbajmgf{>!gjpbpwqlvpBmbozwj`p#bopl#kbp#b=?gju#jg>!?,pwzof=\t?`boofg#elqpjmdfq#bmg-pq`#>#!,,ujlobwjlmpwkjp#sljmw`lmpwbmwozjp#ol`bwfgqf`lqgjmdpg#eqln#wkfmfgfqobmgpslqwvdv/Fp;N;};D;u;F5m4K4]4_7`gfpbqqlool`lnfmwbqjlfgv`b`j/_mpfswjfnaqfqfdjpwqbglgjqf``j/_mvaj`b`j/_msvaoj`jgbgqfpsvfpwbpqfpvowbglpjnslqwbmwfqfpfqubglpbqw/A`volpgjefqfmwfppjdvjfmwfpqfs/Vaoj`bpjwvb`j/_mnjmjpwfqjlsqjub`jgbggjqf`wlqjlelqnb`j/_mslaob`j/_msqfpjgfmwf`lmw','fmjglpb``fplqjlpwf`kmlqbwjsfqplmbofp`bwfdlq/Abfpsf`jbofpgjpslmjaofb`wvbojgbgqfefqfm`jbuboobglojgajaojlwf`bqfob`jlmfp`bofmgbqjlslo/Awj`bpbmwfqjlqfpgl`vnfmwlpmbwvqbofybnbwfqjbofpgjefqfm`jbf`lm/_nj`bwqbmpslqwfqlgq/Advfysbqwj`jsbqfm`vfmwqbmgjp`vpj/_mfpwqv`wvqbevmgb`j/_meqf`vfmwfpsfqnbmfmwfwlwbonfmwf!2s{#plojg# -dje!#bow>!wqbmpsbqfmwjmelqnbwjlmbssoj`bwjlm!#lm`oj`h>!fpwbaojpkfgbgufqwjpjmd-smd!#bow>!fmujqlmnfmwsfqelqnbm`fbssqlsqjbwf%bns8ngbpk8jnnfgjbwfoz?,pwqlmd=?,qbwkfq#wkbmwfnsfqbwvqfgfufolsnfmw`lnsfwjwjlmsob`fklogfqujpjajojwz9`lszqjdkw!=3!#kfjdkw>!fufm#wklvdkqfsob`fnfmwgfpwjmbwjlm@lqslqbwjlm?vo#`obpp>!Bppl`jbwjlmjmgjujgvbopsfqpsf`wjufpfwWjnflvw+vqo+kwws9,,nbwkfnbwj`pnbqdjm.wls9fufmwvbooz#gfp`qjswjlm*#ml.qfsfbw`loof`wjlmp-ISDwkvnasbqwj`jsbwf,kfbg=?algzeolbw9ofew8?oj#`obpp>!kvmgqfgp#le\t\tKltfufq/#`lnslpjwjlm`ofbq9alwk8`llsfqbwjlmtjwkjm#wkf#obafo#elq>!alqgfq.wls9Mft#Yfbobmgqf`lnnfmgfgsklwldqbskzjmwfqfpwjmd%ow8pvs%dw8`lmwqlufqpzMfwkfqobmgpbowfqmbwjufnb{ofmdwk>!ptjwyfqobmgGfufolsnfmwfppfmwjbooz\t\tBowklvdk#?,wf{wbqfb=wkvmgfqajqgqfsqfpfmwfg%bns8mgbpk8psf`vobwjlm`lnnvmjwjfpofdjpobwjlmfof`wqlmj`p\t\n?gju#jg>!joovpwqbwfgfmdjmffqjmdwfqqjwlqjfpbvwklqjwjfpgjpwqjavwfg5!#kfjdkw>!pbmp.pfqje8`bsbaof#le#gjpbssfbqfgjmwfqb`wjufollhjmd#elqjw#tlvog#afBedkbmjpwbmtbp#`qfbwfgNbwk-eollq+pvqqlvmgjmd`bm#bopl#aflapfqubwjlmnbjmwfmbm`ffm`lvmwfqfg?k1#`obpp>!nlqf#qf`fmwjw#kbp#affmjmubpjlm#le*-dfwWjnf+*evmgbnfmwboGfpsjwf#wkf!=?gju#jg>!jmpsjqbwjlmf{bnjmbwjlmsqfsbqbwjlmf{sobmbwjlm?jmsvw#jg>!?,b=?,psbm=ufqpjlmp#lejmpwqvnfmwpafelqf#wkf##>#$kwws9,,Gfp`qjswjlmqfobwjufoz#-pvapwqjmd+fb`k#le#wkff{sfqjnfmwpjmeovfmwjbojmwfdqbwjlmnbmz#sflsofgvf#wl#wkf#`lnajmbwjlmgl#mlw#kbufNjggof#Fbpw?mlp`qjsw=?`lszqjdkw!#sfqkbsp#wkfjmpwjwvwjlmjm#Gf`fnafqbqqbmdfnfmwnlpw#ebnlvpsfqplmbojwz`qfbwjlm#leojnjwbwjlmpf{`ovpjufozplufqfjdmwz.`lmwfmw!=\t?wg#`obpp>!vmgfqdqlvmgsbqboofo#wlgl`wqjmf#lel``vsjfg#azwfqnjmloldzQfmbjppbm`fb#mvnafq#lepvsslqw#elqf{solqbwjlmqf`ldmjwjlmsqfgf`fpplq?jnd#pq`>!,?k2#`obpp>!svaoj`bwjlmnbz#bopl#afpsf`jbojyfg?,ejfogpfw=sqldqfppjufnjoojlmp#lepwbwfp#wkbwfmelq`fnfmwbqlvmg#wkf#lmf#bmlwkfq-sbqfmwMlgfbdqj`vowvqfBowfqmbwjufqfpfbq`kfqpwltbqgp#wkfNlpw#le#wkfnbmz#lwkfq#+fpsf`jbooz?wg#tjgwk>!8tjgwk9233&jmgfsfmgfmw?k0#`obpp>!#lm`kbmdf>!*-bgg@obpp+jmwfqb`wjlmLmf#le#wkf#gbvdkwfq#leb``fpplqjfpaqbm`kfp#le\t?gju#jg>!wkf#obqdfpwgf`obqbwjlmqfdvobwjlmpJmelqnbwjlmwqbmpobwjlmgl`vnfmwbqzjm#lqgfq#wl!=\t?kfbg=\t?!#kfjdkw>!2b`qlpp#wkf#lqjfmwbwjlm*8?,p`qjsw=jnsofnfmwfg`bm#af#pffmwkfqf#tbp#bgfnlmpwqbwf`lmwbjmfq!=`lmmf`wjlmpwkf#Aqjwjpktbp#tqjwwfm"jnslqwbmw8s{8#nbqdjm.elooltfg#azbajojwz#wl#`lnsoj`bwfggvqjmd#wkf#jnnjdqbwjlmbopl#`boofg?k7#`obpp>!gjpwjm`wjlmqfsob`fg#azdlufqmnfmwpol`bwjlm#lejm#Mlufnafqtkfwkfq#wkf?,s=\t?,gju=b`rvjpjwjlm`boofg#wkf#sfqpf`vwjlmgfpjdmbwjlmxelmw.pjyf9bssfbqfg#jmjmufpwjdbwff{sfqjfm`fgnlpw#ojhfoztjgfoz#vpfggjp`vppjlmpsqfpfm`f#le#+gl`vnfmw-f{wfmpjufozJw#kbp#affmjw#glfp#mlw`lmwqbqz#wljmkbajwbmwpjnsqlufnfmwp`klobqpkjs`lmpvnswjlmjmpwqv`wjlmelq#f{bnsoflmf#lq#nlqfs{8#sbggjmdwkf#`vqqfmwb#pfqjfp#lebqf#vpvboozqlof#jm#wkfsqfujlvpoz#gfqjubwjufpfujgfm`f#lef{sfqjfm`fp`lolqp`kfnfpwbwfg#wkbw`fqwjej`bwf?,b=?,gju=\t#pfof`wfg>!kjdk#p`klloqfpslmpf#wl`lnelqwbaofbglswjlm#lewkqff#zfbqpwkf#`lvmwqzjm#Efaqvbqzpl#wkbw#wkfsflsof#tkl#sqlujgfg#az?sbqbn#mbnfbeef`wfg#azjm#wfqnp#lebssljmwnfmwJPL.;;6:.2!tbp#alqm#jmkjpwlqj`bo#qfdbqgfg#bpnfbpvqfnfmwjp#abpfg#lm#bmg#lwkfq#9#evm`wjlm+pjdmjej`bmw`fofaqbwjlmwqbmpnjwwfg,ip,irvfqz-jp#hmltm#bpwkflqfwj`bo#wbajmgf{>!jw#`lvog#af?mlp`qjsw=\tkbujmd#affm\t?kfbg=\t?#%rvlw8Wkf#`lnsjobwjlmkf#kbg#affmsqlgv`fg#azskjolplskfq`lmpwqv`wfgjmwfmgfg#wlbnlmd#lwkfq`lnsbqfg#wlwl#pbz#wkbwFmdjmffqjmdb#gjeefqfmwqfefqqfg#wlgjeefqfm`fpafojfe#wkbwsklwldqbskpjgfmwjezjmdKjpwlqz#le#Qfsvaoj`#lemf`fppbqjozsqlabajojwzwf`kmj`boozofbujmd#wkfpsf`wb`vobqeqb`wjlm#lefof`wqj`jwzkfbg#le#wkfqfpwbvqbmwpsbqwmfqpkjsfnskbpjp#lmnlpw#qf`fmwpkbqf#tjwk#pbzjmd#wkbwejoofg#tjwkgfpjdmfg#wljw#jp#lewfm!=?,jeqbnf=bp#elooltp9nfqdfg#tjwkwkqlvdk#wkf`lnnfq`jbo#sljmwfg#lvwlsslqwvmjwzujft#le#wkfqfrvjqfnfmwgjujpjlm#lesqldqbnnjmdkf#qf`fjufgpfwJmwfqubo!=?,psbm=?,jm#Mft#Zlqhbggjwjlmbo#`lnsqfppjlm\t\t?gju#jg>!jm`lqslqbwf8?,p`qjsw=?bwwb`kFufmwaf`bnf#wkf#!#wbqdfw>!\\`bqqjfg#lvwPlnf#le#wkfp`jfm`f#bmgwkf#wjnf#le@lmwbjmfq!=nbjmwbjmjmd@kqjpwlskfqNv`k#le#wkftqjwjmdp#le!#kfjdkw>!1pjyf#le#wkfufqpjlm#le#nj{wvqf#le#afwtffm#wkfF{bnsofp#lefgv`bwjlmbo`lnsfwjwjuf#lmpvanjw>!gjqf`wlq#legjpwjm`wjuf,GWG#[KWNO#qfobwjmd#wlwfmgfm`z#wlsqlujm`f#letkj`k#tlvoggfpsjwf#wkfp`jfmwjej`#ofdjpobwvqf-jmmfqKWNO#boofdbwjlmpBdqj`vowvqftbp#vpfg#jmbssqlb`k#wljmwfoojdfmwzfbqp#obwfq/pbmp.pfqjegfwfqnjmjmdSfqelqnbm`fbssfbqbm`fp/#tkj`k#jp#elvmgbwjlmpbaaqfujbwfgkjdkfq#wkbmp#eqln#wkf#jmgjujgvbo#`lnslpfg#lepvsslpfg#wl`objnp#wkbwbwwqjavwjlmelmw.pjyf92fofnfmwp#leKjpwlqj`bo#kjp#aqlwkfqbw#wkf#wjnfbmmjufqpbqzdlufqmfg#azqfobwfg#wl#vowjnbwfoz#jmmlubwjlmpjw#jp#pwjoo`bm#lmoz#afgfejmjwjlmpwlDNWPwqjmdB#mvnafq#lejnd#`obpp>!Fufmwvbooz/tbp#`kbmdfgl``vqqfg#jmmfjdkalqjmdgjpwjmdvjpktkfm#kf#tbpjmwqlgv`jmdwfqqfpwqjboNbmz#le#wkfbqdvfp#wkbwbm#Bnfqj`bm`lmrvfpw#letjgfpsqfbg#tfqf#hjoofgp`qffm#bmg#Jm#lqgfq#wlf{sf`wfg#wlgfp`fmgbmwpbqf#ol`bwfgofdjpobwjufdfmfqbwjlmp#ab`hdqlvmgnlpw#sflsofzfbqp#bewfqwkfqf#jp#mlwkf#kjdkfpweqfrvfmwoz#wkfz#gl#mlwbqdvfg#wkbwpkltfg#wkbwsqfglnjmbmwwkfloldj`boaz#wkf#wjnf`lmpjgfqjmdpklqw.ojufg?,psbm=?,b=`bm#af#vpfgufqz#ojwwoflmf#le#wkf#kbg#boqfbgzjmwfqsqfwfg`lnnvmj`bwfefbwvqfp#ledlufqmnfmw/?,mlp`qjsw=fmwfqfg#wkf!#kfjdkw>!0Jmgfsfmgfmwslsvobwjlmpobqdf.p`bof-#Bowklvdk#vpfg#jm#wkfgfpwqv`wjlmslppjajojwzpwbqwjmd#jmwtl#lq#nlqff{sqfppjlmppvalqgjmbwfobqdfq#wkbmkjpwlqz#bmg?,lswjlm=\t@lmwjmfmwbofojnjmbwjmdtjoo#mlw#afsqb`wj`f#lejm#eqlmw#lepjwf#le#wkffmpvqf#wkbwwl#`qfbwf#bnjppjppjssjslwfmwjboozlvwpwbmgjmdafwwfq#wkbmtkbw#jp#mltpjwvbwfg#jmnfwb#mbnf>!WqbgjwjlmbopvddfpwjlmpWqbmpobwjlmwkf#elqn#lebwnlpskfqj`jgfloldj`bofmwfqsqjpfp`bo`vobwjmdfbpw#le#wkfqfnmbmwp#lesovdjmpsbdf,jmgf{-sks!Wkjp#jp#wkf#?b#kqfe>!,slsvobqjyfgjmuloufg#jmbqf#vpfg#wlbmg#pfufqbonbgf#az#wkfpffnp#wl#afojhfoz#wkbwSbofpwjmjbmmbnfg#bewfqjw#kbg#affmnlpw#`lnnlmwl#qfefq#wlavw#wkjp#jp`lmpf`vwjufwfnslqbqjozJm#dfmfqbo/`lmufmwjlmpwbhfp#sob`fpvagjujpjlmwfqqjwlqjbolsfqbwjlmbosfqnbmfmwoztbp#obqdfozlvwaqfbh#lejm#wkf#sbpwelooltjmd#b#{nomp9ld>!=?b#`obpp>!`obpp>!wf{w@lmufqpjlm#nbz#af#vpfgnbmveb`wvqfbewfq#afjmd`ofbqej{!=\trvfpwjlm#letbp#fof`wfgwl#af`lnf#baf`bvpf#le#plnf#sflsofjmpsjqfg#azpv``fppevo#b#wjnf#tkfmnlqf#`lnnlmbnlmdpw#wkfbm#leej`jbotjgwk9233&8wf`kmloldz/tbp#bglswfgwl#hffs#wkfpfwwofnfmwpojuf#ajqwkpjmgf{-kwno!@lmmf`wj`vwbppjdmfg#wl%bns8wjnfp8b``lvmw#elqbojdm>qjdkwwkf#`lnsbmzbotbzp#affmqfwvqmfg#wljmuloufnfmwAf`bvpf#wkfwkjp#sfqjlg!#mbnf>!r!#`lmejmfg#wlb#qfpvow#leubovf>!!#,=jp#b`wvboozFmujqlmnfmw\t?,kfbg=\t@lmufqpfoz/=\t?gju#jg>!3!#tjgwk>!2jp#sqlabaozkbuf#af`lnf`lmwqloojmdwkf#sqlaofn`jwjyfmp#leslojwj`jbmpqfb`kfg#wkfbp#fbqoz#bp9mlmf8#lufq?wbaof#`fooubojgjwz#legjqf`woz#wllmnlvpfgltmtkfqf#jw#jptkfm#jw#tbpnfnafqp#le#qfobwjlm#wlb``lnnlgbwfbolmd#tjwk#Jm#wkf#obwfwkf#Fmdojpkgfoj`jlvp!=wkjp#jp#mlwwkf#sqfpfmwje#wkfz#bqfbmg#ejmboozb#nbwwfq#le\t\n?,gju=\t\t?,p`qjsw=ebpwfq#wkbmnbilqjwz#lebewfq#tkj`k`lnsbqbwjufwl#nbjmwbjmjnsqluf#wkfbtbqgfg#wkffq!#`obpp>!eqbnfalqgfqqfpwlqbwjlmjm#wkf#pbnfbmbozpjp#lewkfjq#ejqpwGvqjmd#wkf#`lmwjmfmwbopfrvfm`f#leevm`wjlm+*xelmw.pjyf9#tlqh#lm#wkf?,p`qjsw=\t?afdjmp#tjwkibubp`qjsw9`lmpwjwvfmwtbp#elvmgfgfrvjojaqjvnbppvnf#wkbwjp#djufm#azmffgp#wl#af`llqgjmbwfpwkf#ubqjlvpbqf#sbqw#lelmoz#jm#wkfpf`wjlmp#lejp#b#`lnnlmwkflqjfp#legjp`lufqjfpbppl`jbwjlmfgdf#le#wkfpwqfmdwk#leslpjwjlm#jmsqfpfmw.gbzvmjufqpboozwl#elqn#wkfavw#jmpwfbg`lqslqbwjlmbwwb`kfg#wljp#`lnnlmozqfbplmp#elq#%rvlw8wkf#`bm#af#nbgftbp#baof#wltkj`k#nfbmpavw#gjg#mlwlmNlvpfLufqbp#slppjaoflsfqbwfg#az`lnjmd#eqlnwkf#sqjnbqzbggjwjlm#leelq#pfufqbowqbmpefqqfgb#sfqjlg#lebqf#baof#wlkltfufq/#jwpklvog#kbufnv`k#obqdfq\t\n?,p`qjsw=bglswfg#wkfsqlsfqwz#legjqf`wfg#azfeef`wjufoztbp#aqlvdkw`kjogqfm#leSqldqbnnjmdolmdfq#wkbmnbmvp`qjswptbq#bdbjmpwaz#nfbmp#lebmg#nlpw#lepjnjobq#wl#sqlsqjfwbqzlqjdjmbwjmdsqfpwjdjlvpdqbnnbwj`bof{sfqjfm`f-wl#nbhf#wkfJw#tbp#bopljp#elvmg#jm`lnsfwjwlqpjm#wkf#V-P-qfsob`f#wkfaqlvdkw#wkf`bo`vobwjlmeboo#le#wkfwkf#dfmfqbosqb`wj`boozjm#klmlq#leqfofbpfg#jmqfpjgfmwjbobmg#plnf#lehjmd#le#wkfqfb`wjlm#wl2pw#Fbqo#le`vowvqf#bmgsqjm`jsbooz?,wjwof=\t##wkfz#`bm#afab`h#wl#wkfplnf#le#kjpf{slpvqf#wlbqf#pjnjobqelqn#le#wkfbggEbulqjwf`jwjyfmpkjssbqw#jm#wkfsflsof#tjwkjm#sqb`wj`fwl#`lmwjmvf%bns8njmvp8bssqlufg#az#wkf#ejqpw#booltfg#wkfbmg#elq#wkfevm`wjlmjmdsobzjmd#wkfplovwjlm#wlkfjdkw>!3!#jm#kjp#allhnlqf#wkbm#belooltp#wkf`qfbwfg#wkfsqfpfm`f#jm%maps8?,wg=mbwjlmbojpwwkf#jgfb#leb#`kbqb`wfqtfqf#elq`fg#`obpp>!awmgbzp#le#wkfefbwvqfg#jmpkltjmd#wkfjmwfqfpw#jmjm#sob`f#lewvqm#le#wkfwkf#kfbg#leOlqg#le#wkfslojwj`boozkbp#jwp#ltmFgv`bwjlmbobssqlubo#leplnf#le#wkffb`k#lwkfq/afkbujlq#lebmg#af`bvpfbmg#bmlwkfqbssfbqfg#lmqf`lqgfg#jmaob`h%rvlw8nbz#jm`ovgfwkf#tlqog$p`bm#ofbg#wlqfefqp#wl#balqgfq>!3!#dlufqmnfmw#tjmmjmd#wkfqfpvowfg#jm#tkjof#wkf#Tbpkjmdwlm/wkf#pvaif`w`jwz#jm#wkf=?,gju=\t\n\nqfeof`w#wkfwl#`lnsofwfaf`bnf#nlqfqbgjlb`wjufqfif`wfg#aztjwklvw#bmzkjp#ebwkfq/tkj`k#`lvog`lsz#le#wkfwl#jmgj`bwfb#slojwj`bob``lvmwp#le`lmpwjwvwfptlqhfg#tjwkfq?,b=?,oj=le#kjp#ojefb``lnsbmjfg`ojfmwTjgwksqfufmw#wkfOfdjpobwjufgjeefqfmwozwldfwkfq#jmkbp#pfufqboelq#bmlwkfqwf{w#le#wkfelvmgfg#wkff#tjwk#wkf#jp#vpfg#elq`kbmdfg#wkfvpvbooz#wkfsob`f#tkfqftkfqfbp#wkf=#?b#kqfe>!!=?b#kqfe>!wkfnpfoufp/bowklvdk#kfwkbw#`bm#afwqbgjwjlmboqlof#le#wkfbp#b#qfpvowqfnluf@kjoggfpjdmfg#aztfpw#le#wkfPlnf#sflsofsqlgv`wjlm/pjgf#le#wkfmftpofwwfqpvpfg#az#wkfgltm#wl#wkfb``fswfg#azojuf#jm#wkfbwwfnswp#wllvwpjgf#wkfeqfrvfm`jfpKltfufq/#jmsqldqbnnfqpbw#ofbpw#jmbssql{jnbwfbowklvdk#jwtbp#sbqw#lebmg#ubqjlvpDlufqmlq#lewkf#bqwj`ofwvqmfg#jmwl=?b#kqfe>!,wkf#f`lmlnzjp#wkf#nlpwnlpw#tjgfoztlvog#obwfqbmg#sfqkbspqjpf#wl#wkfl``vqp#tkfmvmgfq#tkj`k`lmgjwjlmp-wkf#tfpwfqmwkflqz#wkbwjp#sqlgv`fgwkf#`jwz#lejm#tkj`k#kfpffm#jm#wkfwkf#`fmwqboavjogjmd#lenbmz#le#kjpbqfb#le#wkfjp#wkf#lmoznlpw#le#wkfnbmz#le#wkfwkf#TfpwfqmWkfqf#jp#mlf{wfmgfg#wlPwbwjpwj`bo`lopsbm>1#pklqw#pwlqzslppjaof#wlwlsloldj`bo`qjwj`bo#leqfslqwfg#wlb#@kqjpwjbmgf`jpjlm#wljp#frvbo#wlsqlaofnp#leWkjp#`bm#afnfq`kbmgjpfelq#nlpw#leml#fujgfm`ffgjwjlmp#lefofnfmwp#jm%rvlw8-#Wkf`ln,jnbdfp,tkj`k#nbhfpwkf#sql`fppqfnbjmp#wkfojwfqbwvqf/jp#b#nfnafqwkf#slsvobqwkf#bm`jfmwsqlaofnp#jmwjnf#le#wkfgfefbwfg#azalgz#le#wkfb#eft#zfbqpnv`k#le#wkfwkf#tlqh#le@bojelqmjb/pfqufg#bp#bdlufqmnfmw-`lm`fswp#lenlufnfmw#jm\n\n?gju#jg>!jw!#ubovf>!obmdvbdf#lebp#wkfz#bqfsqlgv`fg#jmjp#wkbw#wkff{sobjm#wkfgju=?,gju=\tKltfufq#wkfofbg#wl#wkf\n?b#kqfe>!,tbp#dqbmwfgsflsof#kbuf`lmwjmvbooztbp#pffm#bpbmg#qfobwfgwkf#qlof#lesqlslpfg#azle#wkf#afpwfb`k#lwkfq-@lmpwbmwjmfsflsof#eqlngjbof`wp#lewl#qfujpjlmtbp#qfmbnfgb#plvq`f#lewkf#jmjwjboobvm`kfg#jmsqlujgf#wkfwl#wkf#tfpwtkfqf#wkfqfbmg#pjnjobqafwtffm#wtljp#bopl#wkfFmdojpk#bmg`lmgjwjlmp/wkbw#jw#tbpfmwjwofg#wlwkfnpfoufp-rvbmwjwz#leqbmpsbqfm`zwkf#pbnf#bpwl#iljm#wkf`lvmwqz#bmgwkjp#jp#wkfWkjp#ofg#wlb#pwbwfnfmw`lmwqbpw#wlobpwJmgf{Lewkqlvdk#kjpjp#gfpjdmfgwkf#wfqn#jpjp#sqlujgfgsqlwf`w#wkfmd?,b=?,oj=Wkf#`vqqfmwwkf#pjwf#lepvapwbmwjbof{sfqjfm`f/jm#wkf#Tfpwwkfz#pklvogpolufm(ajmb`lnfmwbqjlpvmjufqpjgbg`lmgj`jlmfpb`wjujgbgfpf{sfqjfm`jbwf`mlold/Absqlgv``j/_msvmwvb`j/_mbsoj`b`j/_m`lmwqbpf/]b`bwfdlq/Abpqfdjpwqbqpfsqlefpjlmbowqbwbnjfmwlqfd/Apwqbwfpf`qfwbq/Absqjm`jsbofpsqlwf``j/_mjnslqwbmwfpjnslqwbm`jbslpjajojgbgjmwfqfpbmwf`qf`jnjfmwlmf`fpjgbgfppvp`qjajqpfbpl`jb`j/_mgjpslmjaofpfubovb`j/_mfpwvgjbmwfpqfpslmpbaofqfplov`j/_mdvbgbobibqbqfdjpwqbglplslqwvmjgbg`lnfq`jbofpelwldqbe/Abbvwlqjgbgfpjmdfmjfq/Abwfofujpj/_m`lnsfwfm`jblsfqb`jlmfpfpwbaof`jglpjnsofnfmwfb`wvbonfmwfmbufdb`j/_m`lmelqnjgbgojmf.kfjdkw9elmw.ebnjoz9!#9#!kwws9,,bssoj`bwjlmpojmh!#kqfe>!psf`jej`booz,,?"X@GBWBX\tLqdbmjybwjlmgjpwqjavwjlm3s{8#kfjdkw9qfobwjlmpkjsgfuj`f.tjgwk?gju#`obpp>!?obafo#elq>!qfdjpwqbwjlm?,mlp`qjsw=\t,jmgf{-kwno!tjmglt-lsfm+#"jnslqwbmw8bssoj`bwjlm,jmgfsfmgfm`f,,ttt-dlldoflqdbmjybwjlmbvwl`lnsofwfqfrvjqfnfmwp`lmpfqubwjuf?elqn#mbnf>!jmwfoof`wvbonbqdjm.ofew92;wk#`fmwvqzbm#jnslqwbmwjmpwjwvwjlmpbaaqfujbwjlm?jnd#`obpp>!lqdbmjpbwjlm`jujojybwjlm2:wk#`fmwvqzbq`kjwf`wvqfjm`lqslqbwfg13wk#`fmwvqz.`lmwbjmfq!=nlpw#mlwbaoz,=?,b=?,gju=mlwjej`bwjlm$vmgfejmfg$*Evqwkfqnlqf/afojfuf#wkbwjmmfqKWNO#>#sqjlq#wl#wkfgqbnbwj`boozqfefqqjmd#wlmfdlwjbwjlmpkfbgrvbqwfqpPlvwk#Beqj`bvmpv``fppevoSfmmpzoubmjbBp#b#qfpvow/?kwno#obmd>!%ow8,pvs%dw8gfbojmd#tjwkskjobgfoskjbkjpwlqj`booz*8?,p`qjsw=\tsbggjmd.wls9f{sfqjnfmwbodfwBwwqjavwfjmpwqv`wjlmpwf`kmloldjfpsbqw#le#wkf#>evm`wjlm+*xpvap`qjswjlmo-gwg!=\t?kwdfldqbskj`bo@lmpwjwvwjlm$/#evm`wjlm+pvsslqwfg#azbdqj`vowvqbo`lmpwqv`wjlmsvaoj`bwjlmpelmw.pjyf9#2b#ubqjfwz#le?gju#pwzof>!Fm`z`olsfgjbjeqbnf#pq`>!gfnlmpwqbwfgb``lnsojpkfgvmjufqpjwjfpGfnldqbskj`p*8?,p`qjsw=?gfgj`bwfg#wlhmltofgdf#lepbwjpeb`wjlmsbqwj`vobqoz?,gju=?,gju=Fmdojpk#+VP*bssfmg@kjog+wqbmpnjppjlmp-#Kltfufq/#jmwfoojdfm`f!#wbajmgf{>!eolbw9qjdkw8@lnnlmtfbowkqbmdjmd#eqlnjm#tkj`k#wkfbw#ofbpw#lmfqfsqlgv`wjlmfm`z`olsfgjb8elmw.pjyf92ivqjpgj`wjlmbw#wkbw#wjnf!=?b#`obpp>!Jm#bggjwjlm/gfp`qjswjlm(`lmufqpbwjlm`lmwb`w#tjwkjp#dfmfqboozq!#`lmwfmw>!qfsqfpfmwjmd%ow8nbwk%dw8sqfpfmwbwjlml``bpjlmbooz?jnd#tjgwk>!mbujdbwjlm!=`lnsfmpbwjlm`kbnsjlmpkjsnfgjb>!boo!#ujlobwjlm#leqfefqfm`f#wlqfwvqm#wqvf8Pwqj`w,,FM!#wqbmpb`wjlmpjmwfqufmwjlmufqjej`bwjlmJmelqnbwjlm#gjeej`vowjfp@kbnsjlmpkjs`bsbajojwjfp?"Xfmgje^..=~\t?,p`qjsw=\t@kqjpwjbmjwzelq#f{bnsof/Sqlefppjlmboqfpwqj`wjlmppvddfpw#wkbwtbp#qfofbpfg+pv`k#bp#wkfqfnluf@obpp+vmfnsolznfmwwkf#Bnfqj`bmpwqv`wvqf#le,jmgf{-kwno#svaojpkfg#jmpsbm#`obpp>!!=?b#kqfe>!,jmwqlgv`wjlmafolmdjmd#wl`objnfg#wkbw`lmpfrvfm`fp?nfwb#mbnf>!Dvjgf#wl#wkflufqtkfonjmdbdbjmpw#wkf#`lm`fmwqbwfg/\t-mlmwlv`k#lapfqubwjlmp?,b=\t?,gju=\te#+gl`vnfmw-alqgfq9#2s{#xelmw.pjyf92wqfbwnfmw#le3!#kfjdkw>!2nlgjej`bwjlmJmgfsfmgfm`fgjujgfg#jmwldqfbwfq#wkbmb`kjfufnfmwpfpwbaojpkjmdIbubP`qjsw!#mfufqwkfofpppjdmjej`bm`fAqlbg`bpwjmd=%maps8?,wg=`lmwbjmfq!=\tpv`k#bp#wkf#jmeovfm`f#leb#sbqwj`vobqpq`>$kwws9,,mbujdbwjlm!#kboe#le#wkf#pvapwbmwjbo#%maps8?,gju=bgubmwbdf#legjp`lufqz#leevmgbnfmwbo#nfwqlslojwbmwkf#lsslpjwf!#{no9obmd>!gfojafqbwfozbojdm>`fmwfqfulovwjlm#lesqfpfqubwjlmjnsqlufnfmwpafdjmmjmd#jmIfpvp#@kqjpwSvaoj`bwjlmpgjpbdqffnfmwwf{w.bojdm9q/#evm`wjlm+*pjnjobqjwjfpalgz=?,kwno=jp#`vqqfmwozboskbafwj`bojp#plnfwjnfpwzsf>!jnbdf,nbmz#le#wkf#eolt9kjggfm8bubjobaof#jmgfp`qjaf#wkff{jpwfm`f#leboo#lufq#wkfwkf#Jmwfqmfw\n?vo#`obpp>!jmpwboobwjlmmfjdkalqkllgbqnfg#elq`fpqfgv`jmd#wkf`lmwjmvfp#wlMlmfwkfofpp/wfnsfqbwvqfp\t\n\n?b#kqfe>!`olpf#wl#wkff{bnsofp#le#jp#balvw#wkf+pff#afolt*-!#jg>!pfbq`ksqlefppjlmbojp#bubjobaofwkf#leej`jbo\n\n?,p`qjsw=\t\t\n\n?gju#jg>!b``fofqbwjlmwkqlvdk#wkf#Kboo#le#Ebnfgfp`qjswjlmpwqbmpobwjlmpjmwfqefqfm`f#wzsf>$wf{w,qf`fmw#zfbqpjm#wkf#tlqogufqz#slsvobqxab`hdqlvmg9wqbgjwjlmbo#plnf#le#wkf#`lmmf`wfg#wlf{soljwbwjlmfnfqdfm`f#le`lmpwjwvwjlmB#Kjpwlqz#lepjdmjej`bmw#nbmveb`wvqfgf{sf`wbwjlmp=?mlp`qjsw=?`bm#af#elvmgaf`bvpf#wkf#kbp#mlw#affmmfjdkalvqjmdtjwklvw#wkf#bggfg#wl#wkf\n?oj#`obpp>!jmpwqvnfmwboPlujfw#Vmjlmb`hmltofgdfgtkj`k#`bm#afmbnf#elq#wkfbwwfmwjlm#wlbwwfnswp#wl#gfufolsnfmwpJm#eb`w/#wkf?oj#`obpp>!bjnsoj`bwjlmppvjwbaof#elqnv`k#le#wkf#`lolmjybwjlmsqfpjgfmwjbo`bm`foAvaaof#Jmelqnbwjlmnlpw#le#wkf#jp#gfp`qjafgqfpw#le#wkf#nlqf#lq#ofppjm#PfswfnafqJmwfoojdfm`fpq`>!kwws9,,s{8#kfjdkw9#bubjobaof#wlnbmveb`wvqfqkvnbm#qjdkwpojmh#kqfe>!,bubjobajojwzsqlslqwjlmbolvwpjgf#wkf#bpwqlmlnj`bokvnbm#afjmdpmbnf#le#wkf#bqf#elvmg#jmbqf#abpfg#lmpnboofq#wkbmb#sfqplm#tklf{sbmpjlm#lebqdvjmd#wkbwmlt#hmltm#bpJm#wkf#fbqozjmwfqnfgjbwfgfqjufg#eqlnP`bmgjmbujbm?,b=?,gju=\t`lmpjgfq#wkfbm#fpwjnbwfgwkf#Mbwjlmbo?gju#jg>!sbdqfpvowjmd#jm`lnnjppjlmfgbmboldlvp#wlbqf#qfrvjqfg,vo=\t?,gju=\ttbp#abpfg#lmbmg#af`bnf#b%maps8%maps8w!#ubovf>!!#tbp#`bswvqfgml#nlqf#wkbmqfpsf`wjufoz`lmwjmvf#wl#=\t?kfbg=\t?tfqf#`qfbwfgnlqf#dfmfqbojmelqnbwjlm#vpfg#elq#wkfjmgfsfmgfmw#wkf#Jnsfqjbo`lnslmfmw#lewl#wkf#mlqwkjm`ovgf#wkf#@lmpwqv`wjlmpjgf#le#wkf#tlvog#mlw#afelq#jmpwbm`fjmufmwjlm#lenlqf#`lnsof{`loof`wjufozab`hdqlvmg9#wf{w.bojdm9#jwp#lqjdjmbojmwl#b``lvmwwkjp#sql`fppbm#f{wfmpjufkltfufq/#wkfwkfz#bqf#mlwqfif`wfg#wkf`qjwj`jpn#legvqjmd#tkj`ksqlabaoz#wkfwkjp#bqwj`of+evm`wjlm+*xJw#pklvog#afbm#bdqffnfmwb``jgfmwboozgjeefqp#eqlnBq`kjwf`wvqfafwwfq#hmltmbqqbmdfnfmwpjmeovfm`f#lmbwwfmgfg#wkfjgfmwj`bo#wlplvwk#le#wkfsbpp#wkqlvdk{no!#wjwof>!tfjdkw9alog8`qfbwjmd#wkfgjpsobz9mlmfqfsob`fg#wkf?jnd#pq`>!,jkwwsp9,,ttt-Tlqog#Tbq#JJwfpwjnlmjbopelvmg#jm#wkfqfrvjqfg#wl#bmg#wkbw#wkfafwtffm#wkf#tbp#gfpjdmfg`lmpjpwp#le#`lmpjgfqbaozsvaojpkfg#azwkf#obmdvbdf@lmpfqubwjlm`lmpjpwfg#leqfefq#wl#wkfab`h#wl#wkf#`pp!#nfgjb>!Sflsof#eqln#bubjobaof#lmsqlufg#wl#afpvddfpwjlmp!tbp#hmltm#bpubqjfwjfp#leojhfoz#wl#af`lnsqjpfg#lepvsslqw#wkf#kbmgp#le#wkf`lvsofg#tjwk`lmmf`w#bmg#alqgfq9mlmf8sfqelqnbm`fpafelqf#afjmdobwfq#af`bnf`bo`vobwjlmplewfm#`boofgqfpjgfmwp#lenfbmjmd#wkbw=?oj#`obpp>!fujgfm`f#elqf{sobmbwjlmpfmujqlmnfmwp!=?,b=?,gju=tkj`k#booltpJmwqlgv`wjlmgfufolsfg#azb#tjgf#qbmdflm#afkboe#leubojdm>!wls!sqjm`jsof#lebw#wkf#wjnf/?,mlp`qjsw=pbjg#wl#kbufjm#wkf#ejqpwtkjof#lwkfqpkzslwkfwj`boskjolplskfqpsltfq#le#wkf`lmwbjmfg#jmsfqelqnfg#azjmbajojwz#wltfqf#tqjwwfmpsbm#pwzof>!jmsvw#mbnf>!wkf#rvfpwjlmjmwfmgfg#elqqfif`wjlm#lejnsojfp#wkbwjmufmwfg#wkfwkf#pwbmgbqgtbp#sqlabaozojmh#afwtffmsqlefpplq#lejmwfqb`wjlmp`kbmdjmd#wkfJmgjbm#L`fbm#`obpp>!obpwtlqhjmd#tjwk$kwws9,,ttt-zfbqp#afelqfWkjp#tbp#wkfqf`qfbwjlmbofmwfqjmd#wkfnfbpvqfnfmwpbm#f{wqfnfozubovf#le#wkfpwbqw#le#wkf\t?,p`qjsw=\t\tbm#feelqw#wljm`qfbpf#wkfwl#wkf#plvwkpsb`jmd>!3!=pveej`jfmwozwkf#Fvqlsfbm`lmufqwfg#wl`ofbqWjnflvwgjg#mlw#kbuf`lmpfrvfmwozelq#wkf#mf{wf{wfmpjlm#lef`lmlnj`#bmgbowklvdk#wkfbqf#sqlgv`fgbmg#tjwk#wkfjmpveej`jfmwdjufm#az#wkfpwbwjmd#wkbwf{sfmgjwvqfp?,psbm=?,b=\twklvdkw#wkbwlm#wkf#abpjp`foosbggjmd>jnbdf#le#wkfqfwvqmjmd#wljmelqnbwjlm/pfsbqbwfg#azbppbppjmbwfgp!#`lmwfmw>!bvwklqjwz#lemlqwktfpwfqm?,gju=\t?gju#!=?,gju=\t##`lmpvowbwjlm`lnnvmjwz#lewkf#mbwjlmbojw#pklvog#afsbqwj`jsbmwp#bojdm>!ofewwkf#dqfbwfpwpfof`wjlm#lepvsfqmbwvqbogfsfmgfmw#lmjp#nfmwjlmfgbooltjmd#wkftbp#jmufmwfgb``lnsbmzjmdkjp#sfqplmbobubjobaof#bwpwvgz#le#wkflm#wkf#lwkfqf{f`vwjlm#leKvnbm#Qjdkwpwfqnp#le#wkfbppl`jbwjlmpqfpfbq`k#bmgpv``ffgfg#azgfefbwfg#wkfbmg#eqln#wkfavw#wkfz#bqf`lnnbmgfq#lepwbwf#le#wkfzfbqp#le#bdfwkf#pwvgz#le?vo#`obpp>!psob`f#jm#wkftkfqf#kf#tbp?oj#`obpp>!ewkfqf#bqf#mltkj`k#af`bnfkf#svaojpkfgf{sqfppfg#jmwl#tkj`k#wkf`lnnjppjlmfqelmw.tfjdkw9wfqqjwlqz#lef{wfmpjlmp!=Qlnbm#Fnsjqffrvbo#wl#wkfJm#`lmwqbpw/kltfufq/#bmgjp#wzsj`boozbmg#kjp#tjef+bopl#`boofg=?vo#`obpp>!feef`wjufoz#fuloufg#jmwlpffn#wl#kbuftkj`k#jp#wkfwkfqf#tbp#mlbm#f{`foofmwboo#le#wkfpfgfp`qjafg#azJm#sqb`wj`f/aqlbg`bpwjmd`kbqdfg#tjwkqfeof`wfg#jmpvaif`wfg#wlnjojwbqz#bmgwl#wkf#sljmwf`lmlnj`boozpfwWbqdfwjmdbqf#b`wvboozuj`wlqz#lufq+*8?,p`qjsw=`lmwjmvlvpozqfrvjqfg#elqfulovwjlmbqzbm#feef`wjufmlqwk#le#wkf/#tkj`k#tbp#eqlmw#le#wkflq#lwkfqtjpfplnf#elqn#lekbg#mlw#affmdfmfqbwfg#azjmelqnbwjlm-sfqnjwwfg#wljm`ovgfp#wkfgfufolsnfmw/fmwfqfg#jmwlwkf#sqfujlvp`lmpjpwfmwozbqf#hmltm#bpwkf#ejfog#lewkjp#wzsf#ledjufm#wl#wkfwkf#wjwof#le`lmwbjmp#wkfjmpwbm`fp#lejm#wkf#mlqwkgvf#wl#wkfjqbqf#gfpjdmfg`lqslqbwjlmptbp#wkbw#wkflmf#le#wkfpfnlqf#slsvobqpv``ffgfg#jmpvsslqw#eqlnjm#gjeefqfmwglnjmbwfg#azgfpjdmfg#elqltmfqpkjs#lebmg#slppjaozpwbmgbqgjyfgqfpslmpfWf{wtbp#jmwfmgfgqf`fjufg#wkfbppvnfg#wkbwbqfbp#le#wkfsqjnbqjoz#jmwkf#abpjp#lejm#wkf#pfmpfb``lvmwp#elqgfpwqlzfg#azbw#ofbpw#wtltbp#gf`obqfg`lvog#mlw#afPf`qfwbqz#lebssfbq#wl#afnbqdjm.wls92,]_p(_p(\',df*xwkqlt#f~8wkf#pwbqw#lewtl#pfsbqbwfobmdvbdf#bmgtkl#kbg#affmlsfqbwjlm#legfbwk#le#wkfqfbo#mvnafqp\n?ojmh#qfo>!sqlujgfg#wkfwkf#pwlqz#le`lnsfwjwjlmpfmdojpk#+VH*fmdojpk#+VP*#evm`wjlm+*-isd!#tjgwk>!`lmejdvqbwjlm-smd!#tjgwk>!?algz#`obpp>!Nbwk-qbmgln+*`lmwfnslqbqz#Vmjwfg#Pwbwfp`jq`vnpwbm`fp-bssfmg@kjog+lqdbmjybwjlmp?psbm#`obpp>!!=?jnd#pq`>!,gjpwjmdvjpkfgwklvpbmgp#le#`lnnvmj`bwjlm`ofbq!=?,gju=jmufpwjdbwjlmebuj`lm-j`l!#nbqdjm.qjdkw9abpfg#lm#wkf#Nbppb`kvpfwwpwbaof#alqgfq>jmwfqmbwjlmbobopl#hmltm#bpsqlmvm`jbwjlmab`hdqlvmg9 esbggjmd.ofew9Elq#f{bnsof/#njp`foobmflvp%ow8,nbwk%dw8spz`kloldj`bojm#sbqwj`vobqfbq`k!#wzsf>!elqn#nfwklg>!bp#lsslpfg#wlPvsqfnf#@lvqwl``bpjlmbooz#Bggjwjlmbooz/Mlqwk#Bnfqj`bs{8ab`hdqlvmglsslqwvmjwjfpFmwfqwbjmnfmw-wlOltfq@bpf+nbmveb`wvqjmdsqlefppjlmbo#`lnajmfg#tjwkElq#jmpwbm`f/`lmpjpwjmd#le!#nb{ofmdwk>!qfwvqm#ebopf8`lmp`jlvpmfppNfgjwfqqbmfbmf{wqblqgjmbqzbppbppjmbwjlmpvapfrvfmwoz#avwwlm#wzsf>!wkf#mvnafq#lewkf#lqjdjmbo#`lnsqfkfmpjufqfefqp#wl#wkf?,vo=\t?,gju=\tskjolplskj`bool`bwjlm-kqfetbp#svaojpkfgPbm#Eqbm`jp`l+evm`wjlm+*x\t?gju#jg>!nbjmplskjpwj`bwfgnbwkfnbwj`bo#,kfbg=\t?algzpvddfpwp#wkbwgl`vnfmwbwjlm`lm`fmwqbwjlmqfobwjlmpkjspnbz#kbuf#affm+elq#f{bnsof/Wkjp#bqwj`of#jm#plnf#`bpfpsbqwp#le#wkf#gfejmjwjlm#leDqfbw#Aqjwbjm#`foosbggjmd>frvjubofmw#wlsob`fklogfq>!8#elmw.pjyf9#ivpwjej`bwjlmafojfufg#wkbwpveefqfg#eqlnbwwfnswfg#wl#ofbgfq#le#wkf`qjsw!#pq`>!,+evm`wjlm+*#xbqf#bubjobaof\t\n?ojmh#qfo>!#pq`>$kwws9,,jmwfqfpwfg#jm`lmufmwjlmbo#!#bow>!!#,=?,bqf#dfmfqboozkbp#bopl#affmnlpw#slsvobq#`lqqfpslmgjmd`qfgjwfg#tjwkwzof>!alqgfq9?,b=?,psbm=?,-dje!#tjgwk>!?jeqbnf#pq`>!wbaof#`obpp>!jmojmf.aol`h8b``lqgjmd#wl#wldfwkfq#tjwkbssql{jnbwfozsbqojbnfmwbqznlqf#bmg#nlqfgjpsobz9mlmf8wqbgjwjlmboozsqfglnjmbmwoz%maps8%maps8%maps8?,psbm=#`foopsb`jmd>?jmsvw#mbnf>!lq!#`lmwfmw>!`lmwqlufqpjbosqlsfqwz>!ld9,{.pkl`htbuf.gfnlmpwqbwjlmpvqqlvmgfg#azMfufqwkfofpp/tbp#wkf#ejqpw`lmpjgfqbaof#Bowklvdk#wkf#`loobalqbwjlmpklvog#mlw#afsqlslqwjlm#le?psbm#pwzof>!hmltm#bp#wkf#pklqwoz#bewfqelq#jmpwbm`f/gfp`qjafg#bp#,kfbg=\t?algz#pwbqwjmd#tjwkjm`qfbpjmdoz#wkf#eb`w#wkbwgjp`vppjlm#lenjggof#le#wkfbm#jmgjujgvbogjeej`vow#wl#sljmw#le#ujftklnlpf{vbojwzb``fswbm`f#le?,psbm=?,gju=nbmveb`wvqfqplqjdjm#le#wkf`lnnlmoz#vpfgjnslqwbm`f#legfmlnjmbwjlmpab`hdqlvmg9# ofmdwk#le#wkfgfwfqnjmbwjlmb#pjdmjej`bmw!#alqgfq>!3!=qfulovwjlmbqzsqjm`jsofp#lejp#`lmpjgfqfgtbp#gfufolsfgJmgl.Fvqlsfbmuvomfqbaof#wlsqlslmfmwp#lebqf#plnfwjnfp`olpfq#wl#wkfMft#Zlqh#@jwz#mbnf>!pfbq`kbwwqjavwfg#wl`lvqpf#le#wkfnbwkfnbwj`jbmaz#wkf#fmg#lebw#wkf#fmg#le!#alqgfq>!3!#wf`kmloldj`bo-qfnluf@obpp+aqbm`k#le#wkffujgfm`f#wkbw"Xfmgje^..=\tJmpwjwvwf#le#jmwl#b#pjmdofqfpsf`wjufoz-bmg#wkfqfelqfsqlsfqwjfp#lejp#ol`bwfg#jmplnf#le#tkj`kWkfqf#jp#bopl`lmwjmvfg#wl#bssfbqbm`f#le#%bns8mgbpk8#gfp`qjafp#wkf`lmpjgfqbwjlmbvwklq#le#wkfjmgfsfmgfmwozfrvjssfg#tjwkglfp#mlw#kbuf?,b=?b#kqfe>!`lmevpfg#tjwk?ojmh#kqfe>!,bw#wkf#bdf#lebssfbq#jm#wkfWkfpf#jm`ovgfqfdbqgofpp#le`lvog#af#vpfg#pwzof>%rvlw8pfufqbo#wjnfpqfsqfpfmw#wkfalgz=\t?,kwno=wklvdkw#wl#afslsvobwjlm#leslppjajojwjfpsfq`fmwbdf#leb``fpp#wl#wkfbm#bwwfnsw#wlsqlgv`wjlm#leirvfqz,irvfqzwtl#gjeefqfmwafolmd#wl#wkffpwbaojpknfmwqfsob`jmd#wkfgfp`qjswjlm!#gfwfqnjmf#wkfbubjobaof#elqB``lqgjmd#wl#tjgf#qbmdf#le\n?gju#`obpp>!nlqf#`lnnlmozlqdbmjpbwjlmpevm`wjlmbojwztbp#`lnsofwfg#%bns8ngbpk8#sbqwj`jsbwjlmwkf#`kbqb`wfqbm#bggjwjlmbobssfbqp#wl#afeb`w#wkbw#wkfbm#f{bnsof#lepjdmjej`bmwozlmnlvpflufq>!af`bvpf#wkfz#bpzm`#>#wqvf8sqlaofnp#tjwkpffnp#wl#kbufwkf#qfpvow#le#pq`>!kwws9,,ebnjojbq#tjwkslppfppjlm#leevm`wjlm#+*#xwllh#sob`f#jmbmg#plnfwjnfppvapwbmwjbooz?psbm=?,psbm=jp#lewfm#vpfgjm#bm#bwwfnswdqfbw#gfbo#leFmujqlmnfmwbopv``fppevooz#ujqwvbooz#boo13wk#`fmwvqz/sqlefppjlmbopmf`fppbqz#wl#gfwfqnjmfg#az`lnsbwjajojwzaf`bvpf#jw#jpGj`wjlmbqz#lenlgjej`bwjlmpWkf#elooltjmdnbz#qfefq#wl9@lmpfrvfmwoz/Jmwfqmbwjlmbobowklvdk#plnfwkbw#tlvog#aftlqog$p#ejqpw`obppjejfg#bpalwwln#le#wkf+sbqwj`vobqozbojdm>!ofew!#nlpw#`lnnlmozabpjp#elq#wkfelvmgbwjlm#le`lmwqjavwjlmpslsvobqjwz#le`fmwfq#le#wkfwl#qfgv`f#wkfivqjpgj`wjlmpbssql{jnbwjlm#lmnlvpflvw>!Mft#Wfpwbnfmw`loof`wjlm#le?,psbm=?,b=?,jm#wkf#Vmjwfgejon#gjqf`wlq.pwqj`w-gwg!=kbp#affm#vpfgqfwvqm#wl#wkfbowklvdk#wkjp`kbmdf#jm#wkfpfufqbo#lwkfqavw#wkfqf#bqfvmsqf`fgfmwfgjp#pjnjobq#wlfpsf`jbooz#jmtfjdkw9#alog8jp#`boofg#wkf`lnsvwbwjlmbojmgj`bwf#wkbwqfpwqj`wfg#wl\n?nfwb#mbnf>!bqf#wzsj`booz`lmeoj`w#tjwkKltfufq/#wkf#Bm#f{bnsof#le`lnsbqfg#tjwkrvbmwjwjfp#leqbwkfq#wkbm#b`lmpwfoobwjlmmf`fppbqz#elqqfslqwfg#wkbwpsf`jej`bwjlmslojwj`bo#bmg%maps8%maps8?qfefqfm`fp#wlwkf#pbnf#zfbqDlufqmnfmw#ledfmfqbwjlm#lekbuf#mlw#affmpfufqbo#zfbqp`lnnjwnfmw#wl\n\n?vo#`obpp>!ujpvbojybwjlm2:wk#`fmwvqz/sqb`wjwjlmfqpwkbw#kf#tlvogbmg#`lmwjmvfgl``vsbwjlm#lejp#gfejmfg#bp`fmwqf#le#wkfwkf#bnlvmw#le=?gju#pwzof>!frvjubofmw#legjeefqfmwjbwfaqlvdkw#balvwnbqdjm.ofew9#bvwlnbwj`boozwklvdkw#le#bpPlnf#le#wkfpf\t?gju#`obpp>!jmsvw#`obpp>!qfsob`fg#tjwkjp#lmf#le#wkffgv`bwjlm#bmgjmeovfm`fg#azqfsvwbwjlm#bp\t?nfwb#mbnf>!b``lnnlgbwjlm?,gju=\t?,gju=obqdf#sbqw#leJmpwjwvwf#elqwkf#pl.`boofg#bdbjmpw#wkf#Jm#wkjp#`bpf/tbp#bssljmwfg`objnfg#wl#afKltfufq/#wkjpGfsbqwnfmw#lewkf#qfnbjmjmdfeef`w#lm#wkfsbqwj`vobqoz#gfbo#tjwk#wkf\t?gju#pwzof>!bonlpw#botbzpbqf#`vqqfmwozf{sqfppjlm#leskjolplskz#leelq#nlqf#wkbm`jujojybwjlmplm#wkf#jpobmgpfof`wfgJmgf{`bm#qfpvow#jm!#ubovf>!!#,=wkf#pwqv`wvqf#,=?,b=?,gju=Nbmz#le#wkfpf`bvpfg#az#wkfle#wkf#Vmjwfgpsbm#`obpp>!n`bm#af#wqb`fgjp#qfobwfg#wlaf`bnf#lmf#lejp#eqfrvfmwozojujmd#jm#wkfwkflqfwj`boozElooltjmd#wkfQfulovwjlmbqzdlufqmnfmw#jmjp#gfwfqnjmfgwkf#slojwj`bojmwqlgv`fg#jmpveej`jfmw#wlgfp`qjswjlm!=pklqw#pwlqjfppfsbqbwjlm#lebp#wl#tkfwkfqhmltm#elq#jwptbp#jmjwjboozgjpsobz9aol`hjp#bm#f{bnsofwkf#sqjm`jsbo`lmpjpwp#le#bqf`ldmjyfg#bp,algz=?,kwno=b#pvapwbmwjboqf`lmpwqv`wfgkfbg#le#pwbwfqfpjpwbm`f#wlvmgfqdqbgvbwfWkfqf#bqf#wtldqbujwbwjlmbobqf#gfp`qjafgjmwfmwjlmboozpfqufg#bp#wkf`obpp>!kfbgfqlsslpjwjlm#wlevmgbnfmwboozglnjmbwfg#wkfbmg#wkf#lwkfqboojbm`f#tjwktbp#elq`fg#wlqfpsf`wjufoz/bmg#slojwj`bojm#pvsslqw#lesflsof#jm#wkf13wk#`fmwvqz-bmg#svaojpkfgolbg@kbqwafbwwl#vmgfqpwbmgnfnafq#pwbwfpfmujqlmnfmwboejqpw#kboe#le`lvmwqjfp#bmgbq`kjwf`wvqboaf#`lmpjgfqfg`kbqb`wfqjyfg`ofbqJmwfqubobvwklqjwbwjufEfgfqbwjlm#letbp#pv``ffgfgbmg#wkfqf#bqfb#`lmpfrvfm`fwkf#Sqfpjgfmwbopl#jm`ovgfgeqff#plewtbqfpv``fppjlm#legfufolsfg#wkftbp#gfpwqlzfgbtbz#eqln#wkf8\t?,p`qjsw=\t?bowklvdk#wkfzelooltfg#az#bnlqf#sltfqevoqfpvowfg#jm#bVmjufqpjwz#leKltfufq/#nbmzwkf#sqfpjgfmwKltfufq/#plnfjp#wklvdkw#wlvmwjo#wkf#fmgtbp#bmmlvm`fgbqf#jnslqwbmwbopl#jm`ovgfp=?jmsvw#wzsf>wkf#`fmwfq#le#GL#MLW#BOWFQvpfg#wl#qfefqwkfnfp,wkbw#kbg#affmwkf#abpjp#elqkbp#gfufolsfgjm#wkf#pvnnfq`lnsbqbwjufozgfp`qjafg#wkfpv`k#bp#wklpfwkf#qfpvowjmdjp#jnslppjaofubqjlvp#lwkfqPlvwk#Beqj`bmkbuf#wkf#pbnffeef`wjufmfppjm#tkj`k#`bpf8#wf{w.bojdm9pwqv`wvqf#bmg8#ab`hdqlvmg9qfdbqgjmd#wkfpvsslqwfg#wkfjp#bopl#hmltmpwzof>!nbqdjmjm`ovgjmd#wkfabkbpb#Nfobzvmlqph#alhn/Iomlqph#mzmlqphpolufm)M(ajmbjmwfqmb`jlmbo`bojej`b`j/_m`lnvmj`b`j/_m`lmpwqv``j/_m!=?gju#`obpp>!gjpbnajdvbwjlmGlnbjmMbnf$/#$bgnjmjpwqbwjlmpjnvowbmflvpozwqbmpslqwbwjlmJmwfqmbwjlmbo#nbqdjm.alwwln9qfpslmpjajojwz?"Xfmgje^..=\t?,=?nfwb#mbnf>!jnsofnfmwbwjlmjmeqbpwqv`wvqfqfsqfpfmwbwjlmalqgfq.alwwln9?,kfbg=\t?algz=>kwws&0B&1E&1E?elqn#nfwklg>!nfwklg>!slpw!#,ebuj`lm-j`l!#~*8\t?,p`qjsw=\t-pfwBwwqjavwf+Bgnjmjpwqbwjlm>#mft#Bqqbz+*8?"Xfmgje^..=\tgjpsobz9aol`h8Vmelqwvmbwfoz/!=%maps8?,gju=,ebuj`lm-j`l!=>$pwzofpkffw$#jgfmwjej`bwjlm/#elq#f{bnsof/?oj=?b#kqfe>!,bm#bowfqmbwjufbp#b#qfpvow#lesw!=?,p`qjsw=\twzsf>!pvanjw!#\t+evm`wjlm+*#xqf`lnnfmgbwjlmelqn#b`wjlm>!,wqbmpelqnbwjlmqf`lmpwqv`wjlm-pwzof-gjpsobz#B``lqgjmd#wl#kjggfm!#mbnf>!bolmd#tjwk#wkfgl`vnfmw-algz-bssql{jnbwfoz#@lnnvmj`bwjlmpslpw!#b`wjlm>!nfbmjmd#%rvlw8..?"Xfmgje^..=Sqjnf#Njmjpwfq`kbqb`wfqjpwj`?,b=#?b#`obpp>wkf#kjpwlqz#le#lmnlvpflufq>!wkf#dlufqmnfmwkqfe>!kwwsp9,,tbp#lqjdjmbooztbp#jmwqlgv`fg`obppjej`bwjlmqfsqfpfmwbwjufbqf#`lmpjgfqfg?"Xfmgje^..=\t\tgfsfmgp#lm#wkfVmjufqpjwz#le#jm#`lmwqbpw#wl#sob`fklogfq>!jm#wkf#`bpf#lejmwfqmbwjlmbo#`lmpwjwvwjlmbopwzof>!alqgfq.9#evm`wjlm+*#xAf`bvpf#le#wkf.pwqj`w-gwg!=\t?wbaof#`obpp>!b``lnsbmjfg#azb``lvmw#le#wkf?p`qjsw#pq`>!,mbwvqf#le#wkf#wkf#sflsof#jm#jm#bggjwjlm#wlp*8#ip-jg#>#jg!#tjgwk>!233&!qfdbqgjmd#wkf#Qlnbm#@bwkloj`bm#jmgfsfmgfmwelooltjmd#wkf#-dje!#tjgwk>!2wkf#elooltjmd#gjp`qjnjmbwjlmbq`kbfloldj`bosqjnf#njmjpwfq-ip!=?,p`qjsw=`lnajmbwjlm#le#nbqdjmtjgwk>!`qfbwfFofnfmw+t-bwwb`kFufmw+?,b=?,wg=?,wq=pq`>!kwwsp9,,bJm#sbqwj`vobq/#bojdm>!ofew!#@yf`k#Qfsvaoj`Vmjwfg#Hjmdgln`lqqfpslmgfm`f`lm`ovgfg#wkbw-kwno!#wjwof>!+evm`wjlm#+*#x`lnfp#eqln#wkfbssoj`bwjlm#le?psbm#`obpp>!pafojfufg#wl#affnfmw+$p`qjsw$?,b=\t?,oj=\t?ojufqz#gjeefqfmw=?psbm#`obpp>!lswjlm#ubovf>!+bopl#hmltm#bp\n?oj=?b#kqfe>!=?jmsvw#mbnf>!pfsbqbwfg#eqlnqfefqqfg#wl#bp#ubojdm>!wls!=elvmgfq#le#wkfbwwfnswjmd#wl#`bqalm#gjl{jgf\t\t?gju#`obpp>!`obpp>!pfbq`k.,algz=\t?,kwno=lsslqwvmjwz#wl`lnnvmj`bwjlmp?,kfbg=\t?algz#pwzof>!tjgwk9Wj\rVSmd#Uj\rWkw`kbmdfp#jm#wkfalqgfq.`lolq9 3!#alqgfq>!3!#?,psbm=?,gju=?tbp#gjp`lufqfg!#wzsf>!wf{w!#*8\t?,p`qjsw=\t\tGfsbqwnfmw#le#f``ofpjbpwj`bowkfqf#kbp#affmqfpvowjmd#eqln?,algz=?,kwno=kbp#mfufq#affmwkf#ejqpw#wjnfjm#qfpslmpf#wlbvwlnbwj`booz#?,gju=\t\t?gju#jtbp#`lmpjgfqfgsfq`fmw#le#wkf!#,=?,b=?,gju=`loof`wjlm#le#gfp`fmgfg#eqlnpf`wjlm#le#wkfb``fsw.`kbqpfwwl#af#`lmevpfgnfnafq#le#wkf#sbggjmd.qjdkw9wqbmpobwjlm#lejmwfqsqfwbwjlm#kqfe>$kwws9,,tkfwkfq#lq#mlwWkfqf#bqf#boplwkfqf#bqf#nbmzb#pnboo#mvnafqlwkfq#sbqwp#lejnslppjaof#wl##`obpp>!avwwlmol`bwfg#jm#wkf-#Kltfufq/#wkfbmg#fufmwvboozBw#wkf#fmg#le#af`bvpf#le#jwpqfsqfpfmwp#wkf?elqn#b`wjlm>!#nfwklg>!slpw!jw#jp#slppjaofnlqf#ojhfoz#wlbm#jm`qfbpf#jmkbuf#bopl#affm`lqqfpslmgp#wlbmmlvm`fg#wkbwbojdm>!qjdkw!=nbmz#`lvmwqjfpelq#nbmz#zfbqpfbqojfpw#hmltmaf`bvpf#jw#tbpsw!=?,p`qjsw=#ubojdm>!wls!#jmkbajwbmwp#leelooltjmd#zfbq\t?gju#`obpp>!njoojlm#sflsof`lmwqlufqpjbo#`lm`fqmjmd#wkfbqdvf#wkbw#wkfdlufqmnfmw#bmgb#qfefqfm`f#wlwqbmpefqqfg#wlgfp`qjajmd#wkf#pwzof>!`lolq9bowklvdk#wkfqfafpw#hmltm#elqpvanjw!#mbnf>!nvowjsoj`bwjlmnlqf#wkbm#lmf#qf`ldmjwjlm#le@lvm`jo#le#wkffgjwjlm#le#wkf##?nfwb#mbnf>!Fmwfqwbjmnfmw#btbz#eqln#wkf#8nbqdjm.qjdkw9bw#wkf#wjnf#lejmufpwjdbwjlmp`lmmf`wfg#tjwkbmg#nbmz#lwkfqbowklvdk#jw#jpafdjmmjmd#tjwk#?psbm#`obpp>!gfp`fmgbmwp#le?psbm#`obpp>!j#bojdm>!qjdkw!?,kfbg=\t?algz#bpsf`wp#le#wkfkbp#pjm`f#affmFvqlsfbm#Vmjlmqfnjmjp`fmw#lenlqf#gjeej`vowUj`f#Sqfpjgfmw`lnslpjwjlm#lesbppfg#wkqlvdknlqf#jnslqwbmwelmw.pjyf922s{f{sobmbwjlm#lewkf#`lm`fsw#letqjwwfm#jm#wkf\n?psbm#`obpp>!jp#lmf#le#wkf#qfpfnaobm`f#wllm#wkf#dqlvmgptkj`k#`lmwbjmpjm`ovgjmd#wkf#gfejmfg#az#wkfsvaoj`bwjlm#lenfbmp#wkbw#wkflvwpjgf#le#wkfpvsslqw#le#wkf?jmsvw#`obpp>!?psbm#`obpp>!w+Nbwk-qbmgln+*nlpw#sqlnjmfmwgfp`qjswjlm#le@lmpwbmwjmlsoftfqf#svaojpkfg?gju#`obpp>!pfbssfbqp#jm#wkf2!#kfjdkw>!2!#nlpw#jnslqwbmwtkj`k#jm`ovgfptkj`k#kbg#affmgfpwqv`wjlm#lewkf#slsvobwjlm\t\n?gju#`obpp>!slppjajojwz#leplnfwjnfp#vpfgbssfbq#wl#kbufpv``fpp#le#wkfjmwfmgfg#wl#afsqfpfmw#jm#wkfpwzof>!`ofbq9a\t?,p`qjsw=\t?tbp#elvmgfg#jmjmwfqujft#tjwk\\jg!#`lmwfmw>!`bsjwbo#le#wkf\t?ojmh#qfo>!pqfofbpf#le#wkfsljmw#lvw#wkbw{NOKwwsQfrvfpwbmg#pvapfrvfmwpf`lmg#obqdfpwufqz#jnslqwbmwpsf`jej`bwjlmppvqeb`f#le#wkfbssojfg#wl#wkfelqfjdm#sloj`z\\pfwGlnbjmMbnffpwbaojpkfg#jmjp#afojfufg#wlJm#bggjwjlm#wlnfbmjmd#le#wkfjp#mbnfg#bewfqwl#sqlwf`w#wkfjp#qfsqfpfmwfgGf`obqbwjlm#lenlqf#feej`jfmw@obppjej`bwjlmlwkfq#elqnp#lekf#qfwvqmfg#wl?psbm#`obpp>!`sfqelqnbm`f#le+evm`wjlm+*#xje#bmg#lmoz#jeqfdjlmp#le#wkfofbgjmd#wl#wkfqfobwjlmp#tjwkVmjwfg#Mbwjlmppwzof>!kfjdkw9lwkfq#wkbm#wkfzsf!#`lmwfmw>!Bppl`jbwjlm#le\t?,kfbg=\t?algzol`bwfg#lm#wkfjp#qfefqqfg#wl+jm`ovgjmd#wkf`lm`fmwqbwjlmpwkf#jmgjujgvbobnlmd#wkf#nlpwwkbm#bmz#lwkfq,=\t?ojmh#qfo>!#qfwvqm#ebopf8wkf#svqslpf#lewkf#bajojwz#wl8`lolq9 eee~\t-\t?psbm#`obpp>!wkf#pvaif`w#legfejmjwjlmp#le=\t?ojmh#qfo>!`objn#wkbw#wkfkbuf#gfufolsfg?wbaof#tjgwk>!`fofaqbwjlm#leElooltjmd#wkf#wl#gjpwjmdvjpk?psbm#`obpp>!awbhfp#sob`f#jmvmgfq#wkf#mbnfmlwfg#wkbw#wkf=?"Xfmgje^..=\tpwzof>!nbqdjm.jmpwfbg#le#wkfjmwqlgv`fg#wkfwkf#sql`fpp#lejm`qfbpjmd#wkfgjeefqfm`fp#jmfpwjnbwfg#wkbwfpsf`jbooz#wkf,gju=?gju#jg>!tbp#fufmwvboozwkqlvdklvw#kjpwkf#gjeefqfm`fplnfwkjmd#wkbwpsbm=?,psbm=?,pjdmjej`bmwoz#=?,p`qjsw=\t\tfmujqlmnfmwbo#wl#sqfufmw#wkfkbuf#affm#vpfgfpsf`jbooz#elqvmgfqpwbmg#wkfjp#fppfmwjbooztfqf#wkf#ejqpwjp#wkf#obqdfpwkbuf#affm#nbgf!#pq`>!kwws9,,jmwfqsqfwfg#bppf`lmg#kboe#le`qloojmd>!ml!#jp#`lnslpfg#leJJ/#Kloz#Qlnbmjp#f{sf`wfg#wlkbuf#wkfjq#ltmgfejmfg#bp#wkfwqbgjwjlmbooz#kbuf#gjeefqfmwbqf#lewfm#vpfgwl#fmpvqf#wkbwbdqffnfmw#tjwk`lmwbjmjmd#wkfbqf#eqfrvfmwozjmelqnbwjlm#lmf{bnsof#jp#wkfqfpvowjmd#jm#b?,b=?,oj=?,vo=#`obpp>!ellwfqbmg#fpsf`jboozwzsf>!avwwlm!#?,psbm=?,psbm=tkj`k#jm`ovgfg=\t?nfwb#mbnf>!`lmpjgfqfg#wkf`bqqjfg#lvw#azKltfufq/#jw#jpaf`bnf#sbqw#lejm#qfobwjlm#wlslsvobq#jm#wkfwkf#`bsjwbo#letbp#leej`jbooztkj`k#kbp#affmwkf#Kjpwlqz#lebowfqmbwjuf#wlgjeefqfmw#eqlnwl#pvsslqw#wkfpvddfpwfg#wkbwjm#wkf#sql`fpp##?gju#`obpp>!wkf#elvmgbwjlmaf`bvpf#le#kjp`lm`fqmfg#tjwkwkf#vmjufqpjwzlsslpfg#wl#wkfwkf#`lmwf{w#le?psbm#`obpp>!swf{w!#mbnf>!r!\n\n?gju#`obpp>!wkf#p`jfmwjej`qfsqfpfmwfg#aznbwkfnbwj`jbmpfof`wfg#az#wkfwkbw#kbuf#affm=?gju#`obpp>!`gju#jg>!kfbgfqjm#sbqwj`vobq/`lmufqwfg#jmwl*8\t?,p`qjsw=\t?skjolplskj`bo#pqsphlkqubwphjwj\rVSmd#Uj\rWkw!kwws9,,!=?psbm#`obpp>!nfnafqp#le#wkf#tjmglt-ol`bwjlmufqwj`bo.bojdm9,b=##?b#kqfe>!?"gl`wzsf#kwno=nfgjb>!p`qffm!#?lswjlm#ubovf>!ebuj`lm-j`l!#,=\t\n\n?gju#`obpp>!`kbqb`wfqjpwj`p!#nfwklg>!dfw!#,algz=\t?,kwno=\tpklqw`vw#j`lm!#gl`vnfmw-tqjwf+sbggjmd.alwwln9qfsqfpfmwbwjufppvanjw!#ubovf>!bojdm>!`fmwfq!#wkqlvdklvw#wkf#p`jfm`f#ej`wjlm\t##?gju#`obpp>!pvanjw!#`obpp>!lmf#le#wkf#nlpw#ubojdm>!wls!=?tbp#fpwbaojpkfg*8\t?,p`qjsw=\tqfwvqm#ebopf8!=*-pwzof-gjpsobzaf`bvpf#le#wkf#gl`vnfmw-`llhjf?elqn#b`wjlm>!,~algzxnbqdjm938Fm`z`olsfgjb#leufqpjlm#le#wkf#-`qfbwfFofnfmw+mbnf!#`lmwfmw>!?,gju=\t?,gju=\t\tbgnjmjpwqbwjuf#?,algz=\t?,kwno=kjpwlqz#le#wkf#!=?jmsvw#wzsf>!slqwjlm#le#wkf#bp#sbqw#le#wkf#%maps8?b#kqfe>!lwkfq#`lvmwqjfp!=\t?gju#`obpp>!?,psbm=?,psbm=?Jm#lwkfq#tlqgp/gjpsobz9#aol`h8`lmwqlo#le#wkf#jmwqlgv`wjlm#le,=\t?nfwb#mbnf>!bp#tfoo#bp#wkf#jm#qf`fmw#zfbqp\t\n?gju#`obpp>!?,gju=\t\n?,gju=\tjmpsjqfg#az#wkfwkf#fmg#le#wkf#`lnsbwjaof#tjwkaf`bnf#hmltm#bp#pwzof>!nbqdjm9-ip!=?,p`qjsw=?#Jmwfqmbwjlmbo#wkfqf#kbuf#affmDfqnbm#obmdvbdf#pwzof>!`lolq9 @lnnvmjpw#Sbqwz`lmpjpwfmw#tjwkalqgfq>!3!#`foo#nbqdjmkfjdkw>!wkf#nbilqjwz#le!#bojdm>!`fmwfqqfobwfg#wl#wkf#nbmz#gjeefqfmw#Lqwklgl{#@kvq`kpjnjobq#wl#wkf#,=\t?ojmh#qfo>!ptbp#lmf#le#wkf#vmwjo#kjp#gfbwk~*+*8\t?,p`qjsw=lwkfq#obmdvbdfp`lnsbqfg#wl#wkfslqwjlmp#le#wkfwkf#Mfwkfqobmgpwkf#nlpw#`lnnlmab`hdqlvmg9vqo+bqdvfg#wkbw#wkfp`qloojmd>!ml!#jm`ovgfg#jm#wkfMlqwk#Bnfqj`bm#wkf#mbnf#le#wkfjmwfqsqfwbwjlmpwkf#wqbgjwjlmbogfufolsnfmw#le#eqfrvfmwoz#vpfgb#`loof`wjlm#leufqz#pjnjobq#wlpvqqlvmgjmd#wkff{bnsof#le#wkjpbojdm>!`fmwfq!=tlvog#kbuf#affmjnbdf\\`bswjlm#>bwwb`kfg#wl#wkfpvddfpwjmd#wkbwjm#wkf#elqn#le#jmuloufg#jm#wkfjp#gfqjufg#eqlnmbnfg#bewfq#wkfJmwqlgv`wjlm#wlqfpwqj`wjlmp#lm#pwzof>!tjgwk9#`bm#af#vpfg#wl#wkf#`qfbwjlm#lenlpw#jnslqwbmw#jmelqnbwjlm#bmgqfpvowfg#jm#wkf`loobspf#le#wkfWkjp#nfbmp#wkbwfofnfmwp#le#wkftbp#qfsob`fg#azbmbozpjp#le#wkfjmpsjqbwjlm#elqqfdbqgfg#bp#wkfnlpw#pv``fppevohmltm#bp#%rvlw8b#`lnsqfkfmpjufKjpwlqz#le#wkf#tfqf#`lmpjgfqfgqfwvqmfg#wl#wkfbqf#qfefqqfg#wlVmplvq`fg#jnbdf=\t\n?gju#`obpp>!`lmpjpwp#le#wkfpwlsSqlsbdbwjlmjmwfqfpw#jm#wkfbubjobajojwz#lebssfbqp#wl#kbuffof`wqlnbdmfwj`fmbaofPfquj`fp+evm`wjlm#le#wkfJw#jp#jnslqwbmw?,p`qjsw=?,gju=evm`wjlm+*xubq#qfobwjuf#wl#wkfbp#b#qfpvow#le#wkf#slpjwjlm#leElq#f{bnsof/#jm#nfwklg>!slpw!#tbp#elooltfg#az%bns8ngbpk8#wkfwkf#bssoj`bwjlmip!=?,p`qjsw=\tvo=?,gju=?,gju=bewfq#wkf#gfbwktjwk#qfpsf`w#wlpwzof>!sbggjmd9jp#sbqwj`vobqozgjpsobz9jmojmf8#wzsf>!pvanjw!#jp#gjujgfg#jmwl\bTA\nzk#+\vBl\bQ*qfpslmpbajojgbgbgnjmjpwqb`j/_mjmwfqmb`jlmbofp`lqqfpslmgjfmwf\fHe\fHF\fHC\fIg\fH{\fHF\fIn\fH\\\fIa\fHY\fHU\fHB\fHR\fH\\\fIk\fH^\fIg\fH{\fIg\fHn\fHv\fIm\fHD\fHR\fHY\fH^\fIk\fHy\fHS\fHD\fHT\fH\\\fHy\fHR\fH\\\fHF\fIm\fH^\fHS\fHT\fHz\fIg\fHp\fIk\fHn\fHv\fHR\fHU\fHS\fHc\fHA\fIk\fHp\fIk\fHn\fHZ\fHR\fHB\fHS\fH^\fHU\fHB\fHR\fH\\\fIl\fHp\fHR\fH{\fH\\\fHO\fH@\fHD\fHR\fHD\fIk\fHy\fIm\fHB\fHR\fH\\\fH@\fIa\fH^\fIe\fH{\fHB\fHR\fH^\fHS\fHy\fHB\fHU\fHS\fH^\fHR\fHF\fIo\fH[\fIa\fHL\fH@\fHN\fHP\fHH\fIk\fHA\fHR\fHp\fHF\fHR\fHy\fIa\fH^\fHS\fHy\fHs\fIa\fH\\\fIk\fHD\fHz\fHS\fH^\fHR\fHG\fHJ\fI`\fH\\\fHR\fHD\fHB\fHR\fHB\fH^\fIk\fHB\fHH\fHJ\fHR\fHD\fH@\fHR\fHp\fHR\fH\\\fHY\fHS\fHy\fHR\fHT\fHy\fIa\fHC\fIg\fHn\fHv\fHR\fHU\fHH\fIk\fHF\fHU\fIm\fHm\fHv\fH@\fHH\fHR\fHC\fHR\fHT\fHn\fHY\fHR\fHJ\fHJ\fIk\fHz\fHD\fIk\fHF\fHS\fHw\fH^\fIk\fHY\fHS\fHZ\fIk\fH[\fH\\\fHR\fHp\fIa\fHC\fHe\fHH\fIa\fHH\fH\\\fHB\fIm\fHn\fH@\fHd\fHJ\fIg\fHD\fIg\fHn\fHe\fHF\fHy\fH\\\fHO\fHF\fHN\fHP\fIk\fHn\fHT\fIa\fHI\fHS\fHH\fHG\fHS\fH^\fIa\fHB\fHB\fIm\fHz\fIa\fHC\fHi\fHv\fIa\fHw\fHR\fHw\fIn\fHs\fHH\fIl\fHT\fHn\fH{\fIl\fHH\fHp\fHR\fHc\fH{\fHR\fHY\fHS\fHA\fHR\fH{\fHt\fHO\fIa\fHs\fIk\fHJ\fIn\fHT\fH\\\fIk\fHJ\fHS\fHD\fIg\fHn\fHU\fHH\fIa\fHC\fHR\fHT\fIk\fHy\fIa\fHT\fH{\fHR\fHn\fHK\fIl\fHY\fHS\fHZ\fIa\fHY\fH\\\fHR\fHH\fIk\fHn\fHJ\fId\fHs\fIa\fHT\fHD\fHy\fIa\fHZ\fHR\fHT\fHR\fHB\fHD\fIk\fHi\fHJ\fHR\fH^\fHH\fH@\fHS\fHp\fH^\fIl\fHF\fIm\fH\\\fIn\fH[\fHU\fHS\fHn\fHJ\fIl\fHB\fHS\fHH\fIa\fH\\\fHy\fHY\fHS\fHH\fHR\fH\\\fIm\fHF\fHC\fIk\fHT\fIa\fHI\fHR\fHD\fHy\fH\\\fIg\fHM\fHP\fHB\fIm\fHy\fIa\fHH\fHC\fIg\fHp\fHD\fHR\fHy\fIo\fHF\fHC\fHR\fHF\fIg\fHT\fIa\fHs\fHt\fH\\\fIk\fH^\fIn\fHy\fHR\fH\\\fIa\fHC\fHY\fHS\fHv\fHR\fH\\\fHT\fIn\fHv\fHD\fHR\fHB\fIn\fH^\fIa\fHC\fHJ\fIk\fHz\fIk\fHn\fHU\fHB\fIk\fHZ\fHR\fHT\fIa\fHy\fIn\fH^\fHB\fId\fHn\fHD\fIk\fHH\fId\fHC\fHR\fH\\\fHp\fHS\fHT\fHy\fIkqpp({no!#wjwof>!.wzsf!#`lmwfmw>!wjwof!#`lmwfmw>!bw#wkf#pbnf#wjnf-ip!=?,p`qjsw=\t?!#nfwklg>!slpw!#?,psbm=?,b=?,oj=ufqwj`bo.bojdm9w,irvfqz-njm-ip!=-`oj`h+evm`wjlm+#pwzof>!sbggjmd.~*+*8\t?,p`qjsw=\t?,psbm=?b#kqfe>!?b#kqfe>!kwws9,,*8#qfwvqm#ebopf8wf{w.gf`lqbwjlm9#p`qloojmd>!ml!#alqgfq.`loobspf9bppl`jbwfg#tjwk#Abkbpb#JmglmfpjbFmdojpk#obmdvbdf?wf{w#{no9psb`f>-dje!#alqgfq>!3!?,algz=\t?,kwno=\tlufqeolt9kjggfm8jnd#pq`>!kwws9,,bggFufmwOjpwfmfqqfpslmpjaof#elq#p-ip!=?,p`qjsw=\t,ebuj`lm-j`l!#,=lsfqbwjmd#pzpwfn!#pwzof>!tjgwk92wbqdfw>!\\aobmh!=Pwbwf#Vmjufqpjwzwf{w.bojdm9ofew8\tgl`vnfmw-tqjwf+/#jm`ovgjmd#wkf#bqlvmg#wkf#tlqog*8\t?,p`qjsw=\t?!#pwzof>!kfjdkw98lufqeolt9kjggfmnlqf#jmelqnbwjlmbm#jmwfqmbwjlmbob#nfnafq#le#wkf#lmf#le#wkf#ejqpw`bm#af#elvmg#jm#?,gju=\t\n\n?,gju=\tgjpsobz9#mlmf8!=!#,=\t?ojmh#qfo>!\t##+evm`wjlm+*#xwkf#26wk#`fmwvqz-sqfufmwGfebvow+obqdf#mvnafq#le#Azybmwjmf#Fnsjqf-isdwkvnaofewubpw#nbilqjwz#lenbilqjwz#le#wkf##bojdm>!`fmwfq!=Vmjufqpjwz#Sqfppglnjmbwfg#az#wkfPf`lmg#Tlqog#Tbqgjpwqjavwjlm#le#pwzof>!slpjwjlm9wkf#qfpw#le#wkf#`kbqb`wfqjyfg#az#qfo>!mleloolt!=gfqjufp#eqln#wkfqbwkfq#wkbm#wkf#b#`lnajmbwjlm#lepwzof>!tjgwk9233Fmdojpk.psfbhjmd`lnsvwfq#p`jfm`falqgfq>!3!#bow>!wkf#f{jpwfm`f#leGfnl`qbwj`#Sbqwz!#pwzof>!nbqdjm.Elq#wkjp#qfbplm/-ip!=?,p`qjsw=\t\npAzWbdMbnf+p*X3^ip!=?,p`qjsw=\t?-ip!=?,p`qjsw=\tojmh#qfo>!j`lm!#$#bow>$$#`obpp>$elqnbwjlm#le#wkfufqpjlmp#le#wkf#?,b=?,gju=?,gju=,sbdf=\t##?sbdf=\t?gju#`obpp>!`lmwaf`bnf#wkf#ejqpwabkbpb#Jmglmfpjbfmdojpk#+pjnsof*"y"W"W"["Q"U"V"@=i=l<^<\\=n=m!?gju#jg>!ellwfq!=wkf#Vmjwfg#Pwbwfp?jnd#pq`>!kwws9,,-isdqjdkwwkvna-ip!=?,p`qjsw=\t?ol`bwjlm-sqlwl`loeqbnfalqgfq>!3!#p!#,=\t?nfwb#mbnf>!?,b=?,gju=?,gju=?elmw.tfjdkw9alog8%rvlw8#bmg#%rvlw8gfsfmgjmd#lm#wkf#nbqdjm938sbggjmd9!#qfo>!mleloolt!#Sqfpjgfmw#le#wkf#wtfmwjfwk#`fmwvqzfujpjlm=\t##?,sbdfJmwfqmfw#F{solqfqb-bpzm`#>#wqvf8\tjmelqnbwjlm#balvw?gju#jg>!kfbgfq!=!#b`wjlm>!kwws9,,?b#kqfe>!kwwsp9,,?gju#jg>!`lmwfmw!?,gju=\t?,gju=\t?gfqjufg#eqln#wkf#?jnd#pq`>$kwws9,,b``lqgjmd#wl#wkf#\t?,algz=\t?,kwno=\tpwzof>!elmw.pjyf9p`qjsw#obmdvbdf>!Bqjbo/#Kfoufwj`b/?,b=?psbm#`obpp>!?,p`qjsw=?p`qjsw#slojwj`bo#sbqwjfpwg=?,wq=?,wbaof=?kqfe>!kwws9,,ttt-jmwfqsqfwbwjlm#leqfo>!pwzofpkffw!#gl`vnfmw-tqjwf+$?`kbqpfw>!vwe.;!=\tafdjmmjmd#le#wkf#qfufbofg#wkbw#wkfwfofujpjlm#pfqjfp!#qfo>!mleloolt!=#wbqdfw>!\\aobmh!=`objnjmd#wkbw#wkfkwws&0B&1E&1Ettt-nbmjefpwbwjlmp#leSqjnf#Njmjpwfq#lejmeovfm`fg#az#wkf`obpp>!`ofbqej{!=,gju=\t?,gju=\t\twkqff.gjnfmpjlmbo@kvq`k#le#Fmdobmgle#Mlqwk#@bqlojmbprvbqf#hjolnfwqfp-bggFufmwOjpwfmfqgjpwjm`w#eqln#wkf`lnnlmoz#hmltm#bpSklmfwj`#Boskbafwgf`obqfg#wkbw#wkf`lmwqloofg#az#wkfAfmibnjm#Eqbmhojmqlof.sobzjmd#dbnfwkf#Vmjufqpjwz#lejm#Tfpwfqm#Fvqlsfsfqplmbo#`lnsvwfqSqlif`w#Dvwfmafqdqfdbqgofpp#le#wkfkbp#affm#sqlslpfgwldfwkfq#tjwk#wkf=?,oj=?oj#`obpp>!jm#plnf#`lvmwqjfpnjm-ip!=?,p`qjsw=le#wkf#slsvobwjlmleej`jbo#obmdvbdf?jnd#pq`>!jnbdfp,jgfmwjejfg#az#wkfmbwvqbo#qfplvq`fp`obppjej`bwjlm#le`bm#af#`lmpjgfqfgrvbmwvn#nf`kbmj`pMfufqwkfofpp/#wkfnjoojlm#zfbqp#bdl?,algz=\t?,kwno="y"W"W"["Q"U"V"@\twbhf#bgubmwbdf#lebmg/#b``lqgjmd#wlbwwqjavwfg#wl#wkfNj`qlplew#Tjmgltpwkf#ejqpw#`fmwvqzvmgfq#wkf#`lmwqlogju#`obpp>!kfbgfqpklqwoz#bewfq#wkfmlwbaof#f{`fswjlmwfmp#le#wklvpbmgppfufqbo#gjeefqfmwbqlvmg#wkf#tlqog-qfb`kjmd#njojwbqzjplobwfg#eqln#wkflsslpjwjlm#wl#wkfwkf#Log#WfpwbnfmwBeqj`bm#Bnfqj`bmpjmpfqwfg#jmwl#wkfpfsbqbwf#eqln#wkfnfwqlslojwbm#bqfbnbhfp#jw#slppjaofb`hmltofgdfg#wkbwbqdvbaoz#wkf#nlpwwzsf>!wf{w,`pp!=\twkf#JmwfqmbwjlmboB``lqgjmd#wl#wkf#sf>!wf{w,`pp!#,=\t`ljm`jgf#tjwk#wkfwtl.wkjqgp#le#wkfGvqjmd#wkjp#wjnf/gvqjmd#wkf#sfqjlgbmmlvm`fg#wkbw#kfwkf#jmwfqmbwjlmbobmg#nlqf#qf`fmwozafojfufg#wkbw#wkf`lmp`jlvpmfpp#bmgelqnfqoz#hmltm#bppvqqlvmgfg#az#wkfejqpw#bssfbqfg#jml``bpjlmbooz#vpfgslpjwjlm9baplovwf8!#wbqdfw>!\\aobmh!#slpjwjlm9qfobwjuf8wf{w.bojdm9`fmwfq8ib{,ojap,irvfqz,2-ab`hdqlvmg.`lolq9 wzsf>!bssoj`bwjlm,bmdvbdf!#`lmwfmw>!?nfwb#kwws.frvju>!Sqjub`z#Sloj`z?,b=f+!&0@p`qjsw#pq`>$!#wbqdfw>!\\aobmh!=Lm#wkf#lwkfq#kbmg/-isdwkvnaqjdkw1?,gju=?gju#`obpp>!?gju#pwzof>!eolbw9mjmfwffmwk#`fmwvqz?,algz=\t?,kwno=\t?jnd#pq`>!kwws9,,p8wf{w.bojdm9`fmwfqelmw.tfjdkw9#alog8#B``lqgjmd#wl#wkf#gjeefqfm`f#afwtffm!#eqbnfalqgfq>!3!#!#pwzof>!slpjwjlm9ojmh#kqfe>!kwws9,,kwno7,ollpf-gwg!=\tgvqjmd#wkjp#sfqjlg?,wg=?,wq=?,wbaof=`olpfoz#qfobwfg#wlelq#wkf#ejqpw#wjnf8elmw.tfjdkw9alog8jmsvw#wzsf>!wf{w!#?psbm#pwzof>!elmw.lmqfbgzpwbwf`kbmdf\n?gju#`obpp>!`ofbqgl`vnfmw-ol`bwjlm-#Elq#f{bnsof/#wkf#b#tjgf#ubqjfwz#le#?"GL@WZSF#kwno=\t?%maps8%maps8%maps8!=?b#kqfe>!kwws9,,pwzof>!eolbw9ofew8`lm`fqmfg#tjwk#wkf>kwws&0B&1E&1Ettt-jm#slsvobq#`vowvqfwzsf>!wf{w,`pp!#,=jw#jp#slppjaof#wl#Kbqubqg#Vmjufqpjwzwzofpkffw!#kqfe>!,wkf#nbjm#`kbqb`wfqL{elqg#Vmjufqpjwz##mbnf>!hfztlqgp!#`pwzof>!wf{w.bojdm9wkf#Vmjwfg#Hjmdglnefgfqbo#dlufqmnfmw?gju#pwzof>!nbqdjm#gfsfmgjmd#lm#wkf#gfp`qjswjlm#le#wkf?gju#`obpp>!kfbgfq-njm-ip!=?,p`qjsw=gfpwqv`wjlm#le#wkfpojdkwoz#gjeefqfmwjm#b``lqgbm`f#tjwkwfof`lnnvmj`bwjlmpjmgj`bwfp#wkbw#wkfpklqwoz#wkfqfbewfqfpsf`jbooz#jm#wkf#Fvqlsfbm#`lvmwqjfpKltfufq/#wkfqf#bqfpq`>!kwws9,,pwbwj`pvddfpwfg#wkbw#wkf!#pq`>!kwws9,,ttt-b#obqdf#mvnafq#le#Wfof`lnnvmj`bwjlmp!#qfo>!mleloolt!#wKloz#Qlnbm#Fnsfqlqbonlpw#f{`ovpjufoz!#alqgfq>!3!#bow>!Pf`qfwbqz#le#Pwbwf`vonjmbwjmd#jm#wkf@JB#Tlqog#Eb`wallhwkf#nlpw#jnslqwbmwbmmjufqpbqz#le#wkfpwzof>!ab`hdqlvmg.?oj=?fn=?b#kqfe>!,wkf#Bwobmwj`#L`fbmpwqj`woz#psfbhjmd/pklqwoz#afelqf#wkfgjeefqfmw#wzsfp#lewkf#Lwwlnbm#Fnsjqf=?jnd#pq`>!kwws9,,Bm#Jmwqlgv`wjlm#wl`lmpfrvfm`f#le#wkfgfsbqwvqf#eqln#wkf@lmefgfqbwf#Pwbwfpjmgjdfmlvp#sflsofpSql`ffgjmdp#le#wkfjmelqnbwjlm#lm#wkfwkflqjfp#kbuf#affmjmuloufnfmw#jm#wkfgjujgfg#jmwl#wkqffbgib`fmw#`lvmwqjfpjp#qfpslmpjaof#elqgjpplovwjlm#le#wkf`loobalqbwjlm#tjwktjgfoz#qfdbqgfg#bpkjp#`lmwfnslqbqjfpelvmgjmd#nfnafq#leGlnjmj`bm#Qfsvaoj`dfmfqbooz#b``fswfgwkf#slppjajojwz#lebqf#bopl#bubjobaofvmgfq#`lmpwqv`wjlmqfpwlqbwjlm#le#wkfwkf#dfmfqbo#svaoj`jp#bonlpw#fmwjqfozsbppfp#wkqlvdk#wkfkbp#affm#pvddfpwfg`lnsvwfq#bmg#ujgflDfqnbmj`#obmdvbdfp#b``lqgjmd#wl#wkf#gjeefqfmw#eqln#wkfpklqwoz#bewfqtbqgpkqfe>!kwwsp9,,ttt-qf`fmw#gfufolsnfmwAlbqg#le#Gjqf`wlqp?gju#`obpp>!pfbq`k#?b#kqfe>!kwws9,,Jm#sbqwj`vobq/#wkfNvowjsof#ellwmlwfplq#lwkfq#pvapwbm`fwklvpbmgp#le#zfbqpwqbmpobwjlm#le#wkf?,gju=\t?,gju=\t\t?b#kqfe>!jmgf{-skstbp#fpwbaojpkfg#jmnjm-ip!=?,p`qjsw=\tsbqwj`jsbwf#jm#wkfb#pwqlmd#jmeovfm`fpwzof>!nbqdjm.wls9qfsqfpfmwfg#az#wkfdqbgvbwfg#eqln#wkfWqbgjwjlmbooz/#wkfFofnfmw+!p`qjsw!*8Kltfufq/#pjm`f#wkf,gju=\t?,gju=\t?gju#ofew8#nbqdjm.ofew9sqlwf`wjlm#bdbjmpw38#ufqwj`bo.bojdm9Vmelqwvmbwfoz/#wkfwzsf>!jnbdf,{.j`lm,gju=\t?gju#`obpp>!#`obpp>!`ofbqej{!=?gju#`obpp>!ellwfq\n\n?,gju=\t\n\n?,gju=\twkf#nlwjlm#sj`wvqf<}=f!t0-lqd,2:::,{kwno!=?b#wbqdfw>!\\aobmh!#wf{w,kwno8#`kbqpfw>!#wbqdfw>!\\aobmh!=?wbaof#`foosbggjmd>!bvwl`lnsofwf>!lee!#wf{w.bojdm9#`fmwfq8wl#obpw#ufqpjlm#az#ab`hdqlvmg.`lolq9# !#kqfe>!kwws9,,ttt-,gju=?,gju=?gju#jg>?b#kqfe>! !#`obpp>!!=?jnd#pq`>!kwws9,,`qjsw!#pq`>!kwws9,,\t?p`qjsw#obmdvbdf>!,,FM!#!kwws9,,ttt-tfm`lgfVQJ@lnslmfmw+!#kqfe>!ibubp`qjsw9?gju#`obpp>!`lmwfmwgl`vnfmw-tqjwf+$?p`slpjwjlm9#baplovwf8p`qjsw#pq`>!kwws9,,#pwzof>!nbqdjm.wls9-njm-ip!=?,p`qjsw=\t?,gju=\t?gju#`obpp>!t0-lqd,2:::,{kwno!#\t\t?,algz=\t?,kwno=gjpwjm`wjlm#afwtffm,!#wbqdfw>!\\aobmh!=?ojmh#kqfe>!kwws9,,fm`lgjmd>!vwe.;!<=\tt-bggFufmwOjpwfmfq!kwws9,,ttt-j`lm!#kqfe>!kwws9,,#pwzof>!ab`hdqlvmg9wzsf>!wf{w,`pp!#,=\tnfwb#sqlsfqwz>!ld9w?jmsvw#wzsf>!wf{w!##pwzof>!wf{w.bojdm9wkf#gfufolsnfmw#le#wzofpkffw!#wzsf>!wfkwno8#`kbqpfw>vwe.;jp#`lmpjgfqfg#wl#afwbaof#tjgwk>!233&!#Jm#bggjwjlm#wl#wkf#`lmwqjavwfg#wl#wkf#gjeefqfm`fp#afwtffmgfufolsnfmw#le#wkf#Jw#jp#jnslqwbmw#wl#?,p`qjsw=\t\t?p`qjsw##pwzof>!elmw.pjyf92=?,psbm=?psbm#jg>daOjaqbqz#le#@lmdqfpp?jnd#pq`>!kwws9,,jnFmdojpk#wqbmpobwjlmB`bgfnz#le#P`jfm`fpgju#pwzof>!gjpsobz9`lmpwqv`wjlm#le#wkf-dfwFofnfmwAzJg+jg*jm#`lmivm`wjlm#tjwkFofnfmw+$p`qjsw$*8#?nfwb#sqlsfqwz>!ld9<}=f!wf{w!#mbnf>!=Sqjub`z#Sloj`z?,b=bgnjmjpwfqfg#az#wkffmbaofPjmdofQfrvfpwpwzof>%rvlw8nbqdjm9?,gju=?,gju=?,gju=?=?jnd#pq`>!kwws9,,j#pwzof>%rvlw8eolbw9qfefqqfg#wl#bp#wkf#wlwbo#slsvobwjlm#lejm#Tbpkjmdwlm/#G-@-#pwzof>!ab`hdqlvmg.bnlmd#lwkfq#wkjmdp/lqdbmjybwjlm#le#wkfsbqwj`jsbwfg#jm#wkfwkf#jmwqlgv`wjlm#lejgfmwjejfg#tjwk#wkfej`wjlmbo#`kbqb`wfq#L{elqg#Vmjufqpjwz#njpvmgfqpwbmgjmd#leWkfqf#bqf/#kltfufq/pwzofpkffw!#kqfe>!,@lovnajb#Vmjufqpjwzf{sbmgfg#wl#jm`ovgfvpvbooz#qfefqqfg#wljmgj`bwjmd#wkbw#wkfkbuf#pvddfpwfg#wkbwbeejojbwfg#tjwk#wkf`lqqfobwjlm#afwtffmmvnafq#le#gjeefqfmw=?,wg=?,wq=?,wbaof=Qfsvaoj`#le#Jqfobmg\t?,p`qjsw=\t?p`qjsw#vmgfq#wkf#jmeovfm`f`lmwqjavwjlm#wl#wkfLeej`jbo#tfapjwf#lekfbgrvbqwfqp#le#wkf`fmwfqfg#bqlvmg#wkfjnsoj`bwjlmp#le#wkfkbuf#affm#gfufolsfgEfgfqbo#Qfsvaoj`#leaf`bnf#jm`qfbpjmdoz`lmwjmvbwjlm#le#wkfMlwf/#kltfufq/#wkbwpjnjobq#wl#wkbw#le#`bsbajojwjfp#le#wkfb``lqgbm`f#tjwk#wkfsbqwj`jsbmwp#jm#wkfevqwkfq#gfufolsnfmwvmgfq#wkf#gjqf`wjlmjp#lewfm#`lmpjgfqfgkjp#zlvmdfq#aqlwkfq?,wg=?,wq=?,wbaof=?b#kwws.frvju>![.VB.skzpj`bo#sqlsfqwjfple#Aqjwjpk#@lovnajbkbp#affm#`qjwj`jyfg+tjwk#wkf#f{`fswjlmrvfpwjlmp#balvw#wkfsbppjmd#wkqlvdk#wkf3!#`foosbggjmd>!3!#wklvpbmgp#le#sflsofqfgjqf`wp#kfqf-#Elqkbuf#`kjogqfm#vmgfq&0F&0@,p`qjsw&0F!**8?b#kqfe>!kwws9,,ttt-?oj=?b#kqfe>!kwws9,,pjwf\\mbnf!#`lmwfmw>!wf{w.gf`lqbwjlm9mlmfpwzof>!gjpsobz9#mlmf?nfwb#kwws.frvju>![.mft#Gbwf+*-dfwWjnf+*#wzsf>!jnbdf,{.j`lm!?,psbm=?psbm#`obpp>!obmdvbdf>!ibubp`qjswtjmglt-ol`bwjlm-kqfe?b#kqfe>!ibubp`qjsw9..=\t?p`qjsw#wzsf>!w?b#kqfe>$kwws9,,ttt-klqw`vw#j`lm!#kqfe>!?,gju=\t?gju#`obpp>!?p`qjsw#pq`>!kwws9,,!#qfo>!pwzofpkffw!#w?,gju=\t?p`qjsw#wzsf>,b=#?b#kqfe>!kwws9,,#booltWqbmpsbqfm`z>![.VB.@lnsbwjaof!#`lmqfobwjlmpkjs#afwtffm\t?,p`qjsw=\t?p`qjsw#?,b=?,oj=?,vo=?,gju=bppl`jbwfg#tjwk#wkf#sqldqbnnjmd#obmdvbdf?,b=?b#kqfe>!kwws9,,?,b=?,oj=?oj#`obpp>!elqn#b`wjlm>!kwws9,,?gju#pwzof>!gjpsobz9wzsf>!wf{w!#mbnf>!r!?wbaof#tjgwk>!233&!#ab`hdqlvmg.slpjwjlm9!#alqgfq>!3!#tjgwk>!qfo>!pklqw`vw#j`lm!#k5=?vo=?oj=?b#kqfe>!##?nfwb#kwws.frvju>!`pp!#nfgjb>!p`qffm!#qfpslmpjaof#elq#wkf#!#wzsf>!bssoj`bwjlm,!#pwzof>!ab`hdqlvmg.kwno8#`kbqpfw>vwe.;!#booltwqbmpsbqfm`z>!pwzofpkffw!#wzsf>!wf\t?nfwb#kwws.frvju>!=?,psbm=?psbm#`obpp>!3!#`foopsb`jmd>!3!=8\t?,p`qjsw=\t?p`qjsw#plnfwjnfp#`boofg#wkfglfp#mlw#mf`fppbqjozElq#nlqf#jmelqnbwjlmbw#wkf#afdjmmjmd#le#?"GL@WZSF#kwno=?kwnosbqwj`vobqoz#jm#wkf#wzsf>!kjggfm!#mbnf>!ibubp`qjsw9uljg+3*8!feef`wjufmfpp#le#wkf#bvwl`lnsofwf>!lee!#dfmfqbooz#`lmpjgfqfg=?jmsvw#wzsf>!wf{w!#!=?,p`qjsw=\t?p`qjswwkqlvdklvw#wkf#tlqog`lnnlm#njp`lm`fswjlmbppl`jbwjlm#tjwk#wkf?,gju=\t?,gju=\t?gju#`gvqjmd#kjp#ojefwjnf/`lqqfpslmgjmd#wl#wkfwzsf>!jnbdf,{.j`lm!#bm#jm`qfbpjmd#mvnafqgjsolnbwj`#qfobwjlmpbqf#lewfm#`lmpjgfqfgnfwb#`kbqpfw>!vwe.;!#?jmsvw#wzsf>!wf{w!#f{bnsofp#jm`ovgf#wkf!=?jnd#pq`>!kwws9,,jsbqwj`jsbwjlm#jm#wkfwkf#fpwbaojpknfmw#le\t?,gju=\t?gju#`obpp>!%bns8maps8%bns8maps8wl#gfwfqnjmf#tkfwkfqrvjwf#gjeefqfmw#eqlnnbqhfg#wkf#afdjmmjmdgjpwbm`f#afwtffm#wkf`lmwqjavwjlmp#wl#wkf`lmeoj`w#afwtffm#wkftjgfoz#`lmpjgfqfg#wltbp#lmf#le#wkf#ejqpwtjwk#ubqzjmd#gfdqffpkbuf#psf`vobwfg#wkbw+gl`vnfmw-dfwFofnfmwsbqwj`jsbwjmd#jm#wkflqjdjmbooz#gfufolsfgfwb#`kbqpfw>!vwe.;!=#wzsf>!wf{w,`pp!#,=\tjmwfq`kbmdfbaoz#tjwknlqf#`olpfoz#qfobwfgpl`jbo#bmg#slojwj`bowkbw#tlvog#lwkfqtjpfsfqsfmgj`vobq#wl#wkfpwzof#wzsf>!wf{w,`ppwzsf>!pvanjw!#mbnf>!ebnjojfp#qfpjgjmd#jmgfufolsjmd#`lvmwqjfp`lnsvwfq#sqldqbnnjmdf`lmlnj`#gfufolsnfmwgfwfqnjmbwjlm#le#wkfelq#nlqf#jmelqnbwjlmlm#pfufqbo#l``bpjlmpslqwvdv/Fp#+Fvqlsfv*VWE.;!#pfwWjnflvw+evm`wjlm+*gjpsobz9jmojmf.aol`h8?jmsvw#wzsf>!pvanjw!#wzsf#>#$wf{w,ibubp`qj?jnd#pq`>!kwws9,,ttt-!#!kwws9,,ttt-t0-lqd,pklqw`vw#j`lm!#kqfe>!!#bvwl`lnsofwf>!lee!#?,b=?,gju=?gju#`obpp>?,b=?,oj=\t?oj#`obpp>!`pp!#wzsf>!wf{w,`pp!#?elqn#b`wjlm>!kwws9,,{w,`pp!#kqfe>!kwws9,,ojmh#qfo>!bowfqmbwf!#\t?p`qjsw#wzsf>!wf{w,#lm`oj`h>!ibubp`qjsw9+mft#Gbwf*-dfwWjnf+*~kfjdkw>!2!#tjgwk>!2!#Sflsof$p#Qfsvaoj`#le##?b#kqfe>!kwws9,,ttt-wf{w.gf`lqbwjlm9vmgfqwkf#afdjmmjmd#le#wkf#?,gju=\t?,gju=\t?,gju=\tfpwbaojpknfmw#le#wkf#?,gju=?,gju=?,gju=?,g ujftslqwxnjm.kfjdkw9\t?p`qjsw#pq`>!kwws9,,lswjlm=?lswjlm#ubovf>lewfm#qfefqqfg#wl#bp#,lswjlm=\t?lswjlm#ubov?"GL@WZSF#kwno=\t?"..XJmwfqmbwjlmbo#Bjqslqw=\t?b#kqfe>!kwws9,,ttt?,b=?b#kqfe>!kwws9,,t\fTL\fT^\fTE\fT^\fUh\fT{\fTN\roI\ro|\roL\ro{\roO\rov\rot\nAOGx\bTA\nzk#+\vUmGx*\fHD\fHS\fH\\\fIa\fHJ\fIk\fHZ\fHM\fHR\fHe\fHD\fH^\fIg\fHM\fHy\fIa\fH[\fIk\fHH\fIa\fH\\\fHp\fHR\fHD\fHy\fHR\fH\\\fIl\fHT\fHn\fH@\fHn\fHK\fHS\fHH\fHT\fIa\fHI\fHR\fHF\fHD\fHR\fHT\fIa\fHY\fIl\fHy\fHR\fH\\\fHT\fHn\fHT\fIa\fHy\fH\\\fHO\fHT\fHR\fHB\fH{\fIa\fH\\\fIl\fHv\fHS\fHs\fIa\fHL\fIg\fHn\fHY\fHS\fHp\fIa\fHr\fHR\fHD\fHi\fHB\fIk\fH\\\fHS\fHy\fHR\fHY\fHS\fHA\fHS\fHD\fIa\fHD\fH{\fHR\fHM\fHS\fHC\fHR\fHm\fHy\fIa\fHC\fIg\fHn\fHy\fHS\fHT\fIm\fH\\\fHy\fIa\fH[\fHR\fHF\fHU\fIm\fHm\fHv\fHH\fIl\fHF\fIa\fH\\\fH@\fHn\fHK\fHD\fHs\fHS\fHF\fIa\fHF\fHO\fIl\fHy\fIa\fH\\\fHS\fHy\fIk\fHs\fHF\fIa\fH\\\fHR\fH\\\fHn\fHA\fHF\fIa\fH\\\fHR\fHF\fIa\fHH\fHB\fHR\fH^\fHS\fHy\fIg\fHn\fH\\\fHG\fHP\fIa\fHH\fHR\fH\\\fHD\fHS\fH\\\fIa\fHB\fHR\fHO\fH^\fHS\fHB\fHS\fHs\fIk\fHMgfp`qjswjlm!#`lmwfmw>!gl`vnfmw-ol`bwjlm-sqlw-dfwFofnfmwpAzWbdMbnf+?"GL@WZSF#kwno=\t?kwno#?nfwb#`kbqpfw>!vwe.;!=9vqo!#`lmwfmw>!kwws9,,-`pp!#qfo>!pwzofpkffw!pwzof#wzsf>!wf{w,`pp!=wzsf>!wf{w,`pp!#kqfe>!t0-lqd,2:::,{kwno!#{nowzsf>!wf{w,ibubp`qjsw!#nfwklg>!dfw!#b`wjlm>!ojmh#qfo>!pwzofpkffw!##>#gl`vnfmw-dfwFofnfmwwzsf>!jnbdf,{.j`lm!#,=`foosbggjmd>!3!#`foops-`pp!#wzsf>!wf{w,`pp!#?,b=?,oj=?oj=?b#kqfe>!!#tjgwk>!2!#kfjdkw>!2!!=?b#kqfe>!kwws9,,ttt-pwzof>!gjpsobz9mlmf8!=bowfqmbwf!#wzsf>!bssoj.,,T0@,,GWG#[KWNO#2-3#foopsb`jmd>!3!#`foosbg#wzsf>!kjggfm!#ubovf>!,b=%maps8?psbm#qlof>!p\t?jmsvw#wzsf>!kjggfm!#obmdvbdf>!IbubP`qjsw!##gl`vnfmw-dfwFofnfmwpAd>!3!#`foopsb`jmd>!3!#zsf>!wf{w,`pp!#nfgjb>!wzsf>$wf{w,ibubp`qjsw$tjwk#wkf#f{`fswjlm#le#zsf>!wf{w,`pp!#qfo>!pw#kfjdkw>!2!#tjgwk>!2!#>$(fm`lgfVQJ@lnslmfmw+?ojmh#qfo>!bowfqmbwf!#\talgz/#wq/#jmsvw/#wf{wnfwb#mbnf>!qlalwp!#`lmnfwklg>!slpw!#b`wjlm>!=\t?b#kqfe>!kwws9,,ttt-`pp!#qfo>!pwzofpkffw!#?,gju=?,gju=?gju#`obppobmdvbdf>!ibubp`qjsw!=bqjb.kjggfm>!wqvf!=.[?qjsw!#wzsf>!wf{w,ibubpo>38~*+*8\t+evm`wjlm+*xab`hdqlvmg.jnbdf9#vqo+,b=?,oj=?oj=?b#kqfe>!k\n\n?oj=?b#kqfe>!kwws9,,bwlq!#bqjb.kjggfm>!wqv=#?b#kqfe>!kwws9,,ttt-obmdvbdf>!ibubp`qjsw!#,lswjlm=\t?lswjlm#ubovf,gju=?,gju=?gju#`obpp>qbwlq!#bqjb.kjggfm>!wqf>+mft#Gbwf*-dfwWjnf+*slqwvdv/Fp#+gl#Aqbpjo*!wf{w,?nfwb#kwws.frvju>!@lmwfqbmpjwjlmbo,,FM!#!kwws9?kwno#{nomp>!kwws9,,ttt.,,T0@,,GWG#[KWNO#2-3#WGWG,{kwno2.wqbmpjwjlmbo,,ttt-t0-lqd,WQ,{kwno2,sf#>#$wf{w,ibubp`qjsw$8?nfwb#mbnf>!gfp`qjswjlmsbqfmwMlgf-jmpfqwAfelqf?jmsvw#wzsf>!kjggfm!#mbip!#wzsf>!wf{w,ibubp`qj+gl`vnfmw*-qfbgz+evm`wjp`qjsw#wzsf>!wf{w,ibubpjnbdf!#`lmwfmw>!kwws9,,VB.@lnsbwjaof!#`lmwfmw>wno8#`kbqpfw>vwe.;!#,=\tojmh#qfo>!pklqw`vw#j`lm?ojmh#qfo>!pwzofpkffw!#?,p`qjsw=\t?p`qjsw#wzsf>>#gl`vnfmw-`qfbwfFofnfm?b#wbqdfw>!\\aobmh!#kqfe>#gl`vnfmw-dfwFofnfmwpAjmsvw#wzsf>!wf{w!#mbnf>b-wzsf#>#$wf{w,ibubp`qjmsvw#wzsf>!kjggfm!#mbnfkwno8#`kbqpfw>vwe.;!#,=gwg!=\t?kwno#{nomp>!kwws.,,T0@,,GWG#KWNO#7-32#WfmwpAzWbdMbnf+$p`qjsw$*jmsvw#wzsf>!kjggfm!#mbn?p`qjsw#wzsf>!wf{w,ibubp!#pwzof>!gjpsobz9mlmf8!=gl`vnfmw-dfwFofnfmwAzJg+>gl`vnfmw-`qfbwfFofnfmw+$#wzsf>$wf{w,ibubp`qjsw$jmsvw#wzsf>!wf{w!#mbnf>!g-dfwFofnfmwpAzWbdMbnf+pmj`bo!#kqfe>!kwws9,,ttt-@,,GWG#KWNO#7-32#Wqbmpjw?pwzof#wzsf>!wf{w,`pp!=\t\t?pwzof#wzsf>!wf{w,`pp!=jlmbo-gwg!=\t?kwno#{nomp>kwws.frvju>!@lmwfmw.Wzsfgjmd>!3!#`foopsb`jmd>!3!kwno8#`kbqpfw>vwe.;!#,=\t#pwzof>!gjpsobz9mlmf8!=??oj=?b#kqfe>!kwws9,,ttt-#wzsf>$wf{w,ibubp`qjsw$=&*&'&^&ˆŸా&ƭ&ƒ&)&^&%&'&‚&P&1&±&3&]&m&u&E&t&C&Ï&V&V&/&>&6&ྲྀ᝼o&p&@&E&M&P&x&@&F&e&Ì&7&:&(&D&0&C&)&.&F&-&1&(&L&F&1ɞ*Ϫ⇳&፲&K&;&)&E&H&P&0&?&9&V&&-&v&a&,&E&)&?&=&'&'&B&മ&ԃ&̖*&*8&%&%&&&%,)&š&>&†&7&]&F&2&>&J&6&n&2&%&?&Ž&2&6&J&g&-&0&,&*&J&*&O&)&6&(&<&B&N&.&P&@&2&.&W&M&%Լ„(,(<&,&Ϛ&ᣇ&-&,(%&(&%&(Ļ0&X&D&&j&'&J&(&.&B&3&Z&R&h&3&E&E&<Æ-͠ỳ&%8?&@&,&Z&@&0&J&,&^&x&_&6&C&6&Cܬ⨥&f&-&-&-&-&,&J&2&8&z&8&C&Y&8&-&d&ṸÌ-&7&1&F&7&t&W&7&I&.&.&^&=ྜ᧓&8(>&/&/&ݻ')'ၥ')'%@/&0&%оী*&*@&CԽהɴ׫4෗ܚӑ6඄&/Ÿ̃Z&*%ɆϿ&Ĵ&1¨ҴŴ",g,"AAAAKKLLKKKKKJJIHHIHHGGFF");!function setData(e,t){const a=m,n=p;for(let e=0;e=0)return t;e.runningState>=0&&(e.runningState=t);throw new Error("Brotli error code: "+t)}return function decode(e,t){let a=new State;a.input=new InputStream(e);initState(a);if(t){let e=t.customDictionary;e&&function attachDictionaryChunk(e,t){if(1!==e.runningState)return makeError(e,-24);if(0===e.cdNumChunks){e.cdChunks=new Array(16);e.cdChunkOffsets=new Int32Array(16);e.cdBlockBits=-1}if(15===e.cdNumChunks)return makeError(e,-27);e.cdChunks[e.cdNumChunks]=t;e.cdNumChunks++;e.cdTotalSize+=t.length;e.cdChunkOffsets[e.cdNumChunks]=e.cdTotalSize;return 0}(a,e)}let n=0,s=[];for(;;){let e=new Int8Array(16384);s.push(e);a.output=e;a.outputOffset=0;a.outputLength=16384;a.outputUsed=0;decompress(a);n+=a.outputUsed;if(a.outputUsed<16384)break}!function close(e){if(0===e.runningState)return makeError(e,-25);e.runningState>0&&(e.runningState=11);return 0}(a);!function closeInput(e){e.input=new InputStream(new Int8Array(0))}(a);let r=new Int8Array(n),i=0;for(let e=0;e>e;this.codeSize=n-=e;return a}getCode(e){const t=this.stream,a=e[0],n=e[1];let s,r=this.codeSize,i=this.codeBuf;for(;r>16,f=65535&o;if(l<1||r>l;this.codeSize=r-l;return f}generateHuffmanTable(e){const t=e.length;let a,n=0;for(a=0;an&&(n=e[a]);const s=1<>=1}for(a=e;a>=1;if(0===t){let t;if(-1===(t=n.getByte())){this.#K("Bad block header in flate stream");return}let a=t;if(-1===(t=n.getByte())){this.#K("Bad block header in flate stream");return}a|=t<<8;if(-1===(t=n.getByte())){this.#K("Bad block header in flate stream");return}let s=t;if(-1===(t=n.getByte())){this.#K("Bad block header in flate stream");return}s|=t<<8;if(s!==(65535&~a)&&(0!==a||0!==s))throw new FormatError("Bad uncompressed block length in flate stream");this.codeBuf=0;this.codeSize=0;const r=this.bufferLength,i=r+a;e=this.ensureBuffer(i);this.bufferLength=i;if(0===a)-1===n.peekByte()&&(this.eof=!0);else{const t=n.getBytes(a);e.set(t,r);t.length0;)c[o++]=m}s=this.generateHuffmanTable(c.subarray(0,e));r=this.generateHuffmanTable(c.subarray(e,f))}}e=this.buffer;let i=e?e.length:0,o=this.bufferLength;for(;;){let t=this.getCode(s);if(t<256){if(o+1>=i){e=this.ensureBuffer(o+1);i=e.length}e[o++]=t;continue}if(256===t){this.bufferLength=o;return}t-=257;t=Qa[t];let n=t>>16;n>0&&(n=this.getBits(n));a=(65535&t)+n;t=this.getCode(r);t=Za[t];n=t>>16;n>0&&(n=this.getBits(n));const l=(65535&t)+n;if(o+a>=i){e=this.ensureBuffer(o+a);i=e.length}for(let t=0;t>>t&(1<0;if(e<256){u[0]=e;m=1}else{if(!(e>=258)){if(256===e){c=9;i=258;m=0;continue}this.eof=!0;delete this.lzwState;break}if(e=0;t--){u[t]=o[a];a=f[a]}}else u[m++]=u[0]}if(s){f[i]=h;l[i]=l[h]+1;o[i]=u[0];i++;c=i+r&i+r-1?c:0|Math.min(Math.log(i+r)/.6931471805599453+1,12)}h=e;p+=m;if(n15))throw new FormatError(`Unsupported predictor: ${n}`);this.readBlock=2===n?this.readBlockTiff:this.readBlockPng;this.stream=e;this.dict=e.dict;const s=this.colors=a.get("Colors")||1,r=this.bits=a.get("BPC","BitsPerComponent")||8,i=this.columns=a.get("Columns")||1;this.pixBytes=s*r+7>>3;this.rowBytes=i*s*r+7>>3;return this}readBlockTiff(){const e=this.rowBytes,t=this.bufferLength,a=this.ensureBuffer(t+e),n=this.bits,s=this.colors,r=this.stream.getBytes(e);this.eof=!r.length;if(this.eof)return;let i,o=0,l=0,f=0,c=0,h=t;if(1===n&&1===s)for(i=0;i>1;e^=e>>2;e^=e>>4;o=(1&e)<<7;a[h++]=e}else if(8===n){for(i=0;i>8&255;a[h++]=255&e}}else{const e=new Uint8Array(s+1),h=(1<>f-n)&h;f-=n;l=l<=8){a[m++]=l>>c-8&255;c-=8}}c>0&&(a[m++]=(l<<8-c)+(o&(1<<8-c)-1))}this.bufferLength+=e}readBlockPng(){const e=this.rowBytes,t=this.pixBytes,a=this.stream.getByte(),n=this.stream.getBytes(e);this.eof=!n.length;if(this.eof)return;const s=this.bufferLength,r=this.ensureBuffer(s+e);let i=r.subarray(s-e,s);0===i.length&&(i=new Uint8Array(e));let o,l,f,c=s;switch(a){case 0:for(o=0;o>1)+n[o];for(;o>1)+n[o]&255;c++}break;case 4:for(o=0;o0){const e=this.stream.getBytes(n);t.set(e,a);a+=n}}else{n=257-n;t=this.ensureBuffer(a+n+1);t.fill(e[1],a,a+n);a+=n}this.bufferLength=a}}class Parser{constructor({lexer:e,xref:t,allowStreams:a=!1,recoveryMode:n=!1}){this.lexer=e;this.xref=t;this.allowStreams=a;this.recoveryMode=n;this.imageCache=Object.create(null);this._imageId=0;this.refill()}refill(){this.buf1=this.lexer.getObj();this.buf2=this.lexer.getObj()}shift(){if(this.buf2 instanceof Cmd&&"ID"===this.buf2.cmd){this.buf1=this.buf2;this.buf2=null}else{this.buf1=this.buf2;this.buf2=this.lexer.getObj()}}tryShift(){try{this.shift();return!0}catch(e){if(e instanceof MissingDataException)throw e;return!1}}getObj(e=null){const t=this.buf1;this.shift();if(t instanceof Cmd)switch(t.cmd){case"BI":return this.makeInlineImage(e);case"[":const a=[];for(;!isCmd(this.buf1,"]")&&this.buf1!==oa;)a.push(this.getObj(e));if(this.buf1===oa){if(this.recoveryMode)return a;throw new ParserEOFException("End of file inside array.")}this.shift();return a;case"<<":const n=new Dict(this.xref);for(;!isCmd(this.buf1,">>")&&this.buf1!==oa;){if(!(this.buf1 instanceof Name)){info("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===oa)break;n.set(t,this.getObj(e))}if(this.buf1===oa){if(this.recoveryMode)return n;throw new ParserEOFException("End of file inside dictionary.")}if(isCmd(this.buf2,"stream"))return this.allowStreams?this.makeStream(n,e):n;this.shift();return n;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&isCmd(this.buf2,"R")){const e=Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,a=e.pos;let n,s,r=0;for(;-1!==(n=e.getByte());)if(0===r)r=69===n?1:0;else if(1===r)r=73===n?2:0;else if(32===n||10===n||13===n){s=e.pos;const a=e.peekBytes(15),i=a.length;if(0===i)break;for(let e=0;e127))){r=0;break}}if(2!==r)continue;if(!t){warn("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const o=new Lexer(new Stream(e.peekBytes(75)),t);o._hexStringWarn=()=>{};let l=0;for(;;){const e=o.getObj();if(e===oa){r=0;break}if(e instanceof Cmd){const a=t[e.cmd];if(!a){r=0;break}if(a.variableArgs?l<=a.numArgs:l===a.numArgs)break;l=0;continue}l++}if(2===r)break}else r=0;if(-1===n){warn("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(s){warn('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-s))}}let i=4;e.skip(-i);n=e.peekByte();e.skip(i);isWhiteSpace(n)||i--;return e.pos-i-a}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let a,n,s=!1;for(;-1!==(a=e.getByte());)if(255===a){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:s=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:n=e.getUint16();n>2?e.skip(n-2):e.skip(-2)}if(s)break}const r=e.pos-t;if(-1===a){warn("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte());)if(126===a){const t=e.pos;a=e.peekByte();for(;isWhiteSpace(a);){e.skip();a=e.peekByte()}if(62===a){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const n=e.pos-t;if(-1===a){warn("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte())&&62!==a;);const n=e.pos-t;if(-1===a){warn("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}inlineStreamSkipEI(e){let t,a=0;for(;-1!==(t=e.getByte());)if(0===a)a=69===t?1:0;else if(1===a)a=73===t?2:0;else if(2===a)break}makeInlineImage(e){const t=this.lexer,a=t.stream,n=Object.create(null);let s;for(;!isCmd(this.buf1,"ID")&&this.buf1!==oa;){if(!(this.buf1 instanceof Name))throw new FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===oa)break;n[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(s=a.pos-t.beginInlineImagePos);const r=this.xref.fetchIfRef(n.F||n.Filter);let i;if(r instanceof Name)i=r.name;else if(Array.isArray(r)){const e=this.xref.fetchIfRef(r[0]);e instanceof Name&&(i=e.name)}const o=a.pos;let l,f;switch(i){case"DCT":case"DCTDecode":l=this.findDCTDecodeInlineStreamEnd(a);break;case"A85":case"ASCII85Decode":l=this.findASCII85DecodeInlineStreamEnd(a);break;case"AHx":case"ASCIIHexDecode":l=this.findASCIIHexDecodeInlineStreamEnd(a);break;default:l=this.findDefaultInlineStreamEnd(a)}if(l<1e3&&s>0){const e=a.pos;a.pos=t.beginInlineImagePos;f=function getInlineImageCacheKey(e){const t=[],a=e.length;let n=0;for(;n=n){let n=!1;for(const e of s){const t=e.length;let s=0;for(;s=r){n=!0;break}if(s>=t){if(isWhiteSpace(i[l+o+s])){info(`Found "${bytesToString([...a,...e])}" when searching for endstream command.`);n=!0}break}}if(n){t.pos+=l;return t.pos-e}}l++}t.pos+=o}return-1}makeStream(e,t){const a=this.lexer;let n=a.stream;a.skipToNextLine();const s=n.pos-1;let r=e.get("Length");if(!Number.isInteger(r)){info(`Bad length "${r&&r.toString()}" in stream.`);r=0}n.pos=s+r;a.nextChar();if(this.tryShift()&&isCmd(this.buf2,"endstream"))this.shift();else{r=this.#G(s);if(r<0)throw new FormatError("Missing endstream command.");a.nextChar();this.shift();this.shift()}this.shift();n=n.makeSubStream(s,r,e);t&&(n=t.createStream(n,r));n=this.filter(n,e,r);n.dict=e;return n}filter(e,t,a){let n=t.get("F","Filter"),s=t.get("DP","DecodeParms");if(n instanceof Name){Array.isArray(s)&&warn("/DecodeParms should not be an Array, when /Filter is a Name.");return this.makeFilter(e,n.name,a,s)}let r=a;if(Array.isArray(n)){const t=n,a=s;for(let i=0,o=t.length;i=48&&e<=57?15&e:e>=65&&e<=70||e>=97&&e<=102?9+(15&e):-1}class Lexer{constructor(e,t=null){this.stream=e;this.nextChar();this.strBuf=[];this.knownCommands=t;this._hexStringNumWarn=0;this.beginInlineImagePos=-1}nextChar(){return this.currentChar=this.stream.getByte()}peekChar(){return this.stream.peekByte()}getNumber(){let e=this.currentChar,t=0,a=1;if(45===e){a=-1;e=this.nextChar();45===e&&(e=this.nextChar())}else 43===e&&(e=this.nextChar());if(10===e||13===e)do{e=this.nextChar()}while(10===e||13===e);if(46===e){t=10;e=this.nextChar()}if(e<48||e>57){const t=`Invalid number: ${String.fromCharCode(e)} (charCode ${e})`;if(isWhiteSpace(e)||40===e||60===e||-1===e){info(`Lexer.getNumber - "${t}".`);return 0}throw new FormatError(t)}let n=e-48;for(;(e=this.nextChar())>=0;)if(e>=48&&e<=57){0!==t&&(t*=10);n=10*n+(e-48)}else if(46===e){if(0!==t)break;t=1}else{if(45!==e)break;warn("Badly formatted number: minus sign in the middle")}0!==t&&(n/=t);return a*n}getString(){let e=1,t=!1;const a=this.strBuf;a.length=0;let n=this.nextChar();for(;;){let s=!1;switch(0|n){case-1:warn("Unterminated string");t=!0;break;case 40:++e;a.push("(");break;case 41:if(0===--e){this.nextChar();t=!0}else a.push(")");break;case 92:n=this.nextChar();switch(n){case-1:warn("Unterminated string");t=!0;break;case 110:a.push("\n");break;case 114:a.push("\r");break;case 116:a.push("\t");break;case 98:a.push("\b");break;case 102:a.push("\f");break;case 92:case 40:case 41:a.push(String.fromCharCode(n));break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:let e=15&n;n=this.nextChar();s=!0;if(n>=48&&n<=55){e=(e<<3)+(15&n);n=this.nextChar();if(n>=48&&n<=55){s=!1;e=(e<<3)+(15&n)}}a.push(String.fromCharCode(e));break;case 13:10===this.peekChar()&&this.nextChar();break;case 10:break;default:a.push(String.fromCharCode(n))}break;default:a.push(String.fromCharCode(n))}if(t)break;s||(n=this.nextChar())}return a.join("")}getName(){let e,t;const a=this.strBuf;a.length=0;for(;(e=this.nextChar())>=0&&!an[e];)if(35===e){e=this.nextChar();if(an[e]){warn("Lexer_getName: NUMBER SIGN (#) should be followed by a hexadecimal number.");a.push("#");break}const n=toHexDigit(e);if(-1!==n){t=e;e=this.nextChar();const s=toHexDigit(e);if(-1===s){warn(`Lexer_getName: Illegal digit (${String.fromCharCode(e)}) in hexadecimal number.`);a.push("#",String.fromCharCode(t));if(an[e])break;a.push(String.fromCharCode(e));continue}a.push(String.fromCharCode(n<<4|s))}else a.push("#",String.fromCharCode(e))}else a.push(String.fromCharCode(e));a.length>127&&warn(`Name token is longer than allowed by the spec: ${a.length}`);return Name.get(a.join(""))}_hexStringWarn(e){5!==this._hexStringNumWarn++?this._hexStringNumWarn>5||warn(`getHexString - ignoring invalid character: ${e}`):warn("getHexString - ignoring additional invalid characters.")}getHexString(){const e=this.strBuf;e.length=0;let t=this.currentChar,a=-1,n=-1;this._hexStringNumWarn=0;for(;;){if(t<0){warn("Unterminated hex string");break}if(62===t){this.nextChar();break}if(1!==an[t]){n=toHexDigit(t);if(-1===n)this._hexStringWarn(t);else if(-1===a)a=n;else{e.push(String.fromCharCode(a<<4|n));a=-1}t=this.nextChar()}else t=this.nextChar()}-1!==a&&e.push(String.fromCharCode(a<<4));return e.join("")}getObj(){let e=!1,t=this.currentChar;for(;;){if(t<0)return oa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(1!==an[t])break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return this.getNumber();case 40:return this.getString();case 47:return this.getName();case 91:this.nextChar();return Cmd.get("[");case 93:this.nextChar();return Cmd.get("]");case 60:t=this.nextChar();if(60===t){this.nextChar();return Cmd.get("<<")}return this.getHexString();case 62:t=this.nextChar();if(62===t){this.nextChar();return Cmd.get(">>")}return Cmd.get(">");case 123:this.nextChar();return Cmd.get("{");case 125:this.nextChar();return Cmd.get("}");case 41:this.nextChar();throw new FormatError(`Illegal character: ${t}`)}let a=String.fromCharCode(t);if(t<32||t>127){const e=this.peekChar();if(e>=32&&e<=127){this.nextChar();return Cmd.get(a)}}const n=this.knownCommands;let s=void 0!==n?.[a];for(;(t=this.nextChar())>=0&&!an[t];){const e=a+String.fromCharCode(t);if(s&&void 0===n[e])break;if(128===a.length)throw new FormatError(`Command token too long: ${a.length}`);a=e;s=void 0!==n?.[a]}if("true"===a)return!0;if("false"===a)return!1;if("null"===a)return null;"BI"===a&&(this.beginInlineImagePos=this.stream.pos);return Cmd.get(a)}skipToNextLine(){let e=this.currentChar;for(;e>=0;){if(13===e){e=this.nextChar();10===e&&this.nextChar();break}if(10===e){this.nextChar();break}e=this.nextChar()}}}class Linearization{static create(e){function getInt(e,t,a=!1){const n=e.get(t);if(Number.isInteger(n)&&(a?n>=0:n>0))return n;throw new Error(`The "${t}" parameter in the linearization dictionary is invalid.`)}const t=new Parser({lexer:new Lexer(e),xref:null}),a=t.getObj(),n=t.getObj(),s=t.getObj(),r=t.getObj();let i,o;if(!(Number.isInteger(a)&&Number.isInteger(n)&&isCmd(s,"obj")&&r instanceof Dict&&"number"==typeof(i=r.get("Linearized"))&&i>0))return null;if((o=getInt(r,"L"))!==e.length)throw new Error('The "L" parameter in the linearization dictionary does not equal the stream length.');return{length:o,hints:function getHints(e){const t=e.get("H");let a;if(Array.isArray(t)&&(2===(a=t.length)||4===a)){for(let e=0;e0))throw new Error(`Hint (${e}) in the linearization dictionary is invalid.`)}return t}throw new Error("Hint array in the linearization dictionary is invalid.")}(r),objectNumberFirst:getInt(r,"O"),endFirst:getInt(r,"E"),numPages:getInt(r,"N"),mainXRefEntriesOffset:getInt(r,"T"),pageFirst:r.has("P")?getInt(r,"P",!0):0}}}const nn=["Adobe-GB1-UCS2","Adobe-CNS1-UCS2","Adobe-Japan1-UCS2","Adobe-Korea1-UCS2","78-EUC-H","78-EUC-V","78-H","78-RKSJ-H","78-RKSJ-V","78-V","78ms-RKSJ-H","78ms-RKSJ-V","83pv-RKSJ-H","90ms-RKSJ-H","90ms-RKSJ-V","90msp-RKSJ-H","90msp-RKSJ-V","90pv-RKSJ-H","90pv-RKSJ-V","Add-H","Add-RKSJ-H","Add-RKSJ-V","Add-V","Adobe-CNS1-0","Adobe-CNS1-1","Adobe-CNS1-2","Adobe-CNS1-3","Adobe-CNS1-4","Adobe-CNS1-5","Adobe-CNS1-6","Adobe-GB1-0","Adobe-GB1-1","Adobe-GB1-2","Adobe-GB1-3","Adobe-GB1-4","Adobe-GB1-5","Adobe-Japan1-0","Adobe-Japan1-1","Adobe-Japan1-2","Adobe-Japan1-3","Adobe-Japan1-4","Adobe-Japan1-5","Adobe-Japan1-6","Adobe-Korea1-0","Adobe-Korea1-1","Adobe-Korea1-2","B5-H","B5-V","B5pc-H","B5pc-V","CNS-EUC-H","CNS-EUC-V","CNS1-H","CNS1-V","CNS2-H","CNS2-V","ETHK-B5-H","ETHK-B5-V","ETen-B5-H","ETen-B5-V","ETenms-B5-H","ETenms-B5-V","EUC-H","EUC-V","Ext-H","Ext-RKSJ-H","Ext-RKSJ-V","Ext-V","GB-EUC-H","GB-EUC-V","GB-H","GB-V","GBK-EUC-H","GBK-EUC-V","GBK2K-H","GBK2K-V","GBKp-EUC-H","GBKp-EUC-V","GBT-EUC-H","GBT-EUC-V","GBT-H","GBT-V","GBTpc-EUC-H","GBTpc-EUC-V","GBpc-EUC-H","GBpc-EUC-V","H","HKdla-B5-H","HKdla-B5-V","HKdlb-B5-H","HKdlb-B5-V","HKgccs-B5-H","HKgccs-B5-V","HKm314-B5-H","HKm314-B5-V","HKm471-B5-H","HKm471-B5-V","HKscs-B5-H","HKscs-B5-V","Hankaku","Hiragana","KSC-EUC-H","KSC-EUC-V","KSC-H","KSC-Johab-H","KSC-Johab-V","KSC-V","KSCms-UHC-H","KSCms-UHC-HW-H","KSCms-UHC-HW-V","KSCms-UHC-V","KSCpc-EUC-H","KSCpc-EUC-V","Katakana","NWP-H","NWP-V","RKSJ-H","RKSJ-V","Roman","UniCNS-UCS2-H","UniCNS-UCS2-V","UniCNS-UTF16-H","UniCNS-UTF16-V","UniCNS-UTF32-H","UniCNS-UTF32-V","UniCNS-UTF8-H","UniCNS-UTF8-V","UniGB-UCS2-H","UniGB-UCS2-V","UniGB-UTF16-H","UniGB-UTF16-V","UniGB-UTF32-H","UniGB-UTF32-V","UniGB-UTF8-H","UniGB-UTF8-V","UniJIS-UCS2-H","UniJIS-UCS2-HW-H","UniJIS-UCS2-HW-V","UniJIS-UCS2-V","UniJIS-UTF16-H","UniJIS-UTF16-V","UniJIS-UTF32-H","UniJIS-UTF32-V","UniJIS-UTF8-H","UniJIS-UTF8-V","UniJIS2004-UTF16-H","UniJIS2004-UTF16-V","UniJIS2004-UTF32-H","UniJIS2004-UTF32-V","UniJIS2004-UTF8-H","UniJIS2004-UTF8-V","UniJISPro-UCS2-HW-V","UniJISPro-UCS2-V","UniJISPro-UTF8-V","UniJISX0213-UTF32-H","UniJISX0213-UTF32-V","UniJISX02132004-UTF32-H","UniJISX02132004-UTF32-V","UniKS-UCS2-H","UniKS-UCS2-V","UniKS-UTF16-H","UniKS-UTF16-V","UniKS-UTF32-H","UniKS-UTF32-V","UniKS-UTF8-H","UniKS-UTF8-V","V","WP-Symbol"],sn=2**24-1;class CMap{constructor(e=!1){this.codespaceRanges=[[],[],[],[]];this.numCodespaceRanges=0;this._map=[];this.name="";this.vertical=!1;this.useCMap=null;this.builtInCMap=e}addCodespaceRange(e,t,a){this.codespaceRanges[e-1].push(t,a);this.numCodespaceRanges++}mapCidRange(e,t,a){if(t-e>sn)throw new Error("mapCidRange - ignoring data above MAX_MAP_RANGE.");for(;e<=t;)this._map[e++]=a++}mapBfRange(e,t,a){if(t-e>sn)throw new Error("mapBfRange - ignoring data above MAX_MAP_RANGE.");const n=a.length-1;for(;e<=t;){this._map[e++]=a;const t=a.charCodeAt(n)+1;t>255?a=a.substring(0,n-1)+String.fromCharCode(a.charCodeAt(n-1)+1)+"\0":a=a.substring(0,n)+String.fromCharCode(t)}}mapBfRangeToArray(e,t,a){if(t-e>sn)throw new Error("mapBfRangeToArray - ignoring data above MAX_MAP_RANGE.");const n=a.length;let s=0;for(;e<=t&&s>>0;const i=s[r];for(let e=0,t=i.length;e=t&&n<=s){a.charcode=n;a.length=r+1;return}}}a.charcode=0;a.length=1}getCharCodeLength(e){const t=this.codespaceRanges;for(let a=0,n=t.length;a=s&&e<=r)return a+1}}return 1}get length(){return this._map.length}get isIdentityCMap(){if("Identity-H"!==this.name&&"Identity-V"!==this.name)return!1;if(65536!==this._map.length)return!1;for(let e=0;e<65536;e++)if(this._map[e]!==e)return!1;return!0}}class IdentityCMap extends CMap{constructor(e,t){super();this.vertical=e;this.addCodespaceRange(t,0,65535)}mapCidRange(e,t,a){unreachable("should not call mapCidRange")}mapBfRange(e,t,a){unreachable("should not call mapBfRange")}mapBfRangeToArray(e,t,a){unreachable("should not call mapBfRangeToArray")}mapOne(e,t){unreachable("should not call mapCidOne")}lookup(e){return Number.isInteger(e)&&e<=65535?e:void 0}contains(e){return Number.isInteger(e)&&e<=65535}forEach(e){for(let t=0;t<=65535;t++)e(t,t)}charCodeOf(e){return Number.isInteger(e)&&e<=65535?e:-1}getMap(){const e=new Array(65536);for(let t=0;t<=65535;t++)e[t]=t;return e}get length(){return 65536}get isIdentityCMap(){unreachable("should not access .isIdentityCMap")}}function strToInt(e){let t=0;for(let a=0;a>>0}function expectString(e){if("string"!=typeof e)throw new FormatError("Malformed CMap: expected string.")}function expectInt(e){if(!Number.isInteger(e))throw new FormatError("Malformed CMap: expected int.")}function parseBfChar(e,t){for(;;){let a=t.getObj();if(a===oa)break;if(isCmd(a,"endbfchar"))return;expectString(a);const n=strToInt(a);a=t.getObj();expectString(a);const s=a;e.mapOne(n,s)}}function parseBfRange(e,t){for(;;){let a=t.getObj();if(a===oa)break;if(isCmd(a,"endbfrange"))return;expectString(a);const n=strToInt(a);a=t.getObj();expectString(a);const s=strToInt(a);a=t.getObj();if(Number.isInteger(a)||"string"==typeof a){const t=Number.isInteger(a)?String.fromCharCode(a):a;e.mapBfRange(n,s,t)}else{if(!isCmd(a,"["))break;{a=t.getObj();const r=[];for(;!isCmd(a,"]")&&a!==oa;){r.push(a);a=t.getObj()}e.mapBfRangeToArray(n,s,r)}}}throw new FormatError("Invalid bf range.")}function parseCidChar(e,t){for(;;){let a=t.getObj();if(a===oa)break;if(isCmd(a,"endcidchar"))return;expectString(a);const n=strToInt(a);a=t.getObj();expectInt(a);const s=a;e.mapOne(n,s)}}function parseCidRange(e,t){for(;;){let a=t.getObj();if(a===oa)break;if(isCmd(a,"endcidrange"))return;expectString(a);const n=strToInt(a);a=t.getObj();expectString(a);const s=strToInt(a);a=t.getObj();expectInt(a);const r=a;e.mapCidRange(n,s,r)}}function parseCodespaceRange(e,t){for(;;){let a=t.getObj();if(a===oa)break;if(isCmd(a,"endcodespacerange"))return;if("string"!=typeof a)break;const n=strToInt(a);a=t.getObj();if("string"!=typeof a)break;const s=strToInt(a);e.addCodespaceRange(a.length,n,s)}throw new FormatError("Invalid codespace range.")}function parseWMode(e,t){const a=t.getObj();Number.isInteger(a)&&(e.vertical=!!a)}function parseCMapName(e,t){const a=t.getObj();a instanceof Name&&(e.name=a.name)}async function parseCMap(e,t,a,n){let s,r;e:for(;;)try{const a=t.getObj();if(a===oa)break;if(a instanceof Name){"WMode"===a.name?parseWMode(e,t):"CMapName"===a.name&&parseCMapName(e,t);s=a}else if(a instanceof Cmd)switch(a.cmd){case"endcmap":break e;case"usecmap":s instanceof Name&&(r=s.name);break;case"begincodespacerange":parseCodespaceRange(e,t);break;case"beginbfchar":parseBfChar(e,t);break;case"begincidchar":parseCidChar(e,t);break;case"beginbfrange":parseBfRange(e,t);break;case"begincidrange":parseCidRange(e,t)}}catch(e){if(e instanceof MissingDataException)throw e;warn("Invalid cMap data: "+e);continue}!n&&r&&(n=r);return n?extendCMap(e,a,n):e}async function extendCMap(e,t,a){e.useCMap=await createBuiltInCMap(a,t);if(0===e.numCodespaceRanges){const t=e.useCMap.codespaceRanges;for(let a=0;aextendCMap(s,t,e));const r=new Lexer(new Stream(a));return parseCMap(s,r,t,null)}class CMapFactory{static async create({encoding:e,fetchBuiltInCMap:t,useCMap:a}){if(e instanceof Name)return createBuiltInCMap(e.name,t);if(e instanceof BaseStream){if(e.isAsync){const t=await e.asyncGetBytes();t&&(e=new Stream(t,0,t.length,e.dict))}const n=await parseCMap(new CMap,new Lexer(e),t,a);return n.isIdentityCMap?createBuiltInCMap(n.name,t):n}throw new Error("Encoding required.")}}class CSS_FONT_INFO{static strings=["fontFamily","fontWeight","italicAngle"]}class SYSTEM_FONT_INFO{static strings=["css","loadedName","baseFontName","src"]}class FONT_INFO{static bools=["black","bold","disableFontFace","fontExtraProperties","isInvalidPDFjsFont","isType3Font","italic","missingFile","remeasure","vertical"];static numbers=["ascent","defaultWidth","descent"];static strings=["fallbackName","loadedName","mimetype","name"];static OFFSET_NUMBERS=Math.ceil(2*this.bools.length/8);static OFFSET_BBOX=this.OFFSET_NUMBERS+8*this.numbers.length;static OFFSET_FONT_MATRIX=this.OFFSET_BBOX+1+8;static OFFSET_DEFAULT_VMETRICS=this.OFFSET_FONT_MATRIX+1+48;static OFFSET_STRINGS=this.OFFSET_DEFAULT_VMETRICS+1+6}class PATTERN_INFO{static KIND=0;static HAS_BBOX=1;static HAS_BACKGROUND=2;static SHADING_TYPE=3;static N_COORD=4;static N_COLOR=8;static N_STOP=12;static N_FIGURES=16}function compileFontInfo(e){const t=e.systemFontInfo?function compileSystemFontInfo(e){const t=new TextEncoder,a={};let n=0;for(const s of SYSTEM_FONT_INFO.strings){const r=t.encode(e[s]);a[s]=r;n+=4+r.length}n+=4;let s,r,i=1+n;if(e.style){s=t.encode(e.style.style);r=t.encode(e.style.weight);i+=4+s.length+4+r.length}const o=new ArrayBuffer(i),l=new Uint8Array(o),f=new DataView(o);let c=0;f.setUint8(c++,e.guessFallback?1:0);f.setUint32(c,0);c+=4;n=0;for(const e of SYSTEM_FONT_INFO.strings){const t=a[e],s=t.length;n+=4+s;f.setUint32(c,s);l.set(t,c+4);c+=4+s}f.setUint32(c-n-4,n);if(e.style){f.setUint32(c,s.length);l.set(s,c+4);c+=4+s.length;f.setUint32(c,r.length);l.set(r,c+4);c+=4+r.length}assert(c<=o.byteLength,"compileSystemFontInfo: Buffer overflow");return o.transferToFixedLength(c)}(e.systemFontInfo):null,a=e.cssFontInfo?function compileCssFontInfo(e){const t=new TextEncoder,a={};let n=0;for(const s of CSS_FONT_INFO.strings){const r=t.encode(e[s]);a[s]=r;n+=4+r.length}const s=new ArrayBuffer(n),r=new Uint8Array(s),i=new DataView(s);let o=0;for(const e of CSS_FONT_INFO.strings){const t=a[e],n=t.length;i.setUint32(o,n);r.set(t,o+4);o+=4+n}assert(o===s.byteLength,"compileCssFontInfo: Buffer overflow");return s}(e.cssFontInfo):null,n=new TextEncoder,s={};let r=0;for(const t of FONT_INFO.strings){s[t]=n.encode(e[t]);r+=4+s[t].length}const i=FONT_INFO.OFFSET_STRINGS+4+r+4+(t?.byteLength??0)+4+(a?.byteLength??0)+4+(e.data?.length??0),o=new ArrayBuffer(i),l=new Uint8Array(o),f=new DataView(o);let c=0;const h=FONT_INFO.bools.length;let u=0,m=0;for(let t=0;t=5&&t<=7))return-1;n=e.substring(1)}if(n===n.toUpperCase()){a=parseInt(n,16);if(a>=0)return a}}return-1}const gn=[[0,127],[128,255],[256,383],[384,591],[592,687,7424,7551,7552,7615],[688,767,42752,42783],[768,879,7616,7679],[880,1023],[11392,11519],[1024,1279,1280,1327,11744,11775,42560,42655],[1328,1423],[1424,1535],[42240,42559],[1536,1791,1872,1919],[1984,2047],[2304,2431],[2432,2559],[2560,2687],[2688,2815],[2816,2943],[2944,3071],[3072,3199],[3200,3327],[3328,3455],[3584,3711],[3712,3839],[4256,4351,11520,11567],[6912,7039],[4352,4607],[7680,7935,11360,11391,42784,43007],[7936,8191],[8192,8303,11776,11903],[8304,8351],[8352,8399],[8400,8447],[8448,8527],[8528,8591],[8592,8703,10224,10239,10496,10623,11008,11263],[8704,8959,10752,11007,10176,10223,10624,10751],[8960,9215],[9216,9279],[9280,9311],[9312,9471],[9472,9599],[9600,9631],[9632,9727],[9728,9983],[9984,10175],[12288,12351],[12352,12447],[12448,12543,12784,12799],[12544,12591,12704,12735],[12592,12687],[43072,43135],[12800,13055],[13056,13311],[44032,55215],[55296,57343],[67840,67871],[19968,40959,11904,12031,12032,12255,12272,12287,13312,19903,131072,173791,12688,12703],[57344,63743],[12736,12783,63744,64255,194560,195103],[64256,64335],[64336,65023],[65056,65071],[65040,65055],[65104,65135],[65136,65279],[65280,65519],[65520,65535],[3840,4095],[1792,1871],[1920,1983],[3456,3583],[4096,4255],[4608,4991,4992,5023,11648,11743],[5024,5119],[5120,5759],[5760,5791],[5792,5887],[6016,6143],[6144,6319],[10240,10495],[40960,42127],[5888,5919,5920,5951,5952,5983,5984,6015],[66304,66351],[66352,66383],[66560,66639],[118784,119039,119040,119295,119296,119375],[119808,120831],[1044480,1048573],[65024,65039,917760,917999],[917504,917631],[6400,6479],[6480,6527],[6528,6623],[6656,6687],[11264,11359],[11568,11647],[19904,19967],[43008,43055],[65536,65663,65664,65791,65792,65855],[65856,65935],[66432,66463],[66464,66527],[66640,66687],[66688,66735],[67584,67647],[68096,68191],[119552,119647],[73728,74751,74752,74879],[119648,119679],[7040,7103],[7168,7247],[7248,7295],[43136,43231],[43264,43311],[43312,43359],[43520,43615],[65936,65999],[66e3,66047],[66208,66271,66176,66207,67872,67903],[127024,127135,126976,127023]];function getUnicodeRangeFor(e,t=-1){if(-1!==t){const a=gn[t];for(let n=0,s=a.length;n=a[n]&&e<=a[n+1])return t}for(let t=0,a=gn.length;t=a[n]&&e<=a[n+1])return t}return-1}const bn=new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$","u"),wn=new Map;const jn=!0,kn=1,yn=2,qn=4,vn=32,Sn=[".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"];function recoverGlyphName(e,t){if(void 0!==t[e])return e;const a=getUnicodeForGlyph(e,t);if(-1!==a)for(const e in t)if(t[e]===a)return e;info("Unable to recover a standard glyph name for: "+e);return e}function type1FontGlyphMapping(e,t,a){const n=Object.create(null);let s,r,i;const o=!!(e.flags&qn);if(e.isInternalFont){i=t;for(r=0;r=0?s:0}}else if(e.baseEncodingName){i=getEncoding(e.baseEncodingName);for(r=0;r=0?s:0}}else if(o)for(r in t)n[r]=t[r];else{i=fn;for(r=0;r=0?s:0}}const l=e.differences;let f;if(l)for(r in l){const e=l[r];s=a.indexOf(e);if(-1===s){f||(f=mn());const t=recoverGlyphName(e,f);t!==e&&(s=a.indexOf(t))}n[r]=s>=0?s:0}return n}function normalizeFontName(e){return e.replaceAll(/[,_]/g,"-").replaceAll(/\s/g,"")}const An=getLookupTableFactory(e=>{e[8211]=65074;e[8212]=65073;e[8229]=65072;e[8230]=65049;e[12289]=65041;e[12290]=65042;e[12296]=65087;e[12297]=65088;e[12298]=65085;e[12299]=65086;e[12300]=65089;e[12301]=65090;e[12302]=65091;e[12303]=65092;e[12304]=65083;e[12305]=65084;e[12308]=65081;e[12309]=65082;e[12310]=65047;e[12311]=65048;e[65103]=65076;e[65281]=65045;e[65288]=65077;e[65289]=65078;e[65292]=65040;e[65306]=65043;e[65307]=65044;e[65311]=65046;e[65339]=65095;e[65341]=65096;e[65343]=65075;e[65371]=65079;e[65373]=65080});const xn=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron"],Cn=[".notdef","space","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],In=[".notdef","space","dollaroldstyle","dollarsuperior","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","hyphensuperior","colonmonetary","onefitted","rupiah","centoldstyle","figuredash","hypheninferior","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior"],Fn=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall","001.000","001.001","001.002","001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"],Tn=391,On=[null,{id:"hstem",min:2,stackClearing:!0,stem:!0},null,{id:"vstem",min:2,stackClearing:!0,stem:!0},{id:"vmoveto",min:1,stackClearing:!0},{id:"rlineto",min:2,resetStack:!0},{id:"hlineto",min:1,resetStack:!0},{id:"vlineto",min:1,resetStack:!0},{id:"rrcurveto",min:6,resetStack:!0},null,{id:"callsubr",min:1},{id:"return",min:0},null,null,{id:"endchar",min:0,stackClearing:!0},null,null,null,{id:"hstemhm",min:2,stackClearing:!0,stem:!0},{id:"hintmask",min:0,stackClearing:!0},{id:"cntrmask",min:0,stackClearing:!0},{id:"rmoveto",min:2,stackClearing:!0},{id:"hmoveto",min:1,stackClearing:!0},{id:"vstemhm",min:2,stackClearing:!0,stem:!0},{id:"rcurveline",min:8,resetStack:!0},{id:"rlinecurve",min:8,resetStack:!0},{id:"vvcurveto",min:4,resetStack:!0},{id:"hhcurveto",min:4,resetStack:!0},null,{id:"callgsubr",min:1},{id:"vhcurveto",min:4,resetStack:!0},{id:"hvcurveto",min:4,resetStack:!0}],Rn=[null,null,null,{id:"and",min:2,stackDelta:-1},{id:"or",min:2,stackDelta:-1},{id:"not",min:1,stackDelta:0},null,null,null,{id:"abs",min:1,stackDelta:0},{id:"add",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]+e[t-1]}},{id:"sub",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]-e[t-1]}},{id:"div",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]/e[t-1]}},null,{id:"neg",min:1,stackDelta:0,stackFn(e,t){e[t-1]=-e[t-1]}},{id:"eq",min:2,stackDelta:-1},null,null,{id:"drop",min:1,stackDelta:-1},null,{id:"put",min:2,stackDelta:-2},{id:"get",min:1,stackDelta:0},{id:"ifelse",min:4,stackDelta:-3},{id:"random",min:0,stackDelta:1},{id:"mul",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]*e[t-1]}},null,{id:"sqrt",min:1,stackDelta:0},{id:"dup",min:1,stackDelta:1},{id:"exch",min:2,stackDelta:0},{id:"index",min:2,stackDelta:0},{id:"roll",min:3,stackDelta:-2},null,null,null,{id:"hflex",min:7,resetStack:!0},{id:"flex",min:13,resetStack:!0},{id:"hflex1",min:9,resetStack:!0},{id:"flex1",min:11,resetStack:!0}];class CFFParser{constructor(e,t,a){this.bytes=e.getBytes();this.properties=t;this.seacAnalysisEnabled=!!a}parse(){const e=this.properties,t=new CFF(this.bytes.length);this.cff=t;const a=this.parseHeader(),n=this.parseIndex(a.endPos),s=this.parseIndex(n.endPos),r=this.parseIndex(s.endPos),i=this.parseIndex(r.endPos),o=this.parseDict(s.obj.get(0)),l=this.createDict(CFFTopDict,o,t.strings);t.header=a.obj;t.names=this.parseNameIndex(n.obj);t.strings=this.parseStringIndex(r.obj);t.topDict=l;t.globalSubrIndex=i.obj;this.parsePrivateDict(t.topDict);t.isCIDFont=l.hasName("ROS");const f=l.getByName("CharStrings"),c=this.parseIndex(f).obj;t.charStringCount=c.count;const h=l.getByName("FontMatrix");h&&(e.fontMatrix=h);const u=l.getByName("FontBBox");if(u){e.ascent=Math.max(u[3],u[1]);e.descent=Math.min(u[1],u[3]);e.ascentScaled=!0}let m,p;if(t.isCIDFont){const e=this.parseIndex(l.getByName("FDArray")).obj;for(let a=0,n=e.count;a=t)throw new FormatError("Invalid CFF header");if(0!==a){info("cff data is shifted");e=e.subarray(a);this.bytes=e}const n=e[0],s=e[1],r=e[2],i=e[3];return{obj:new CFFHeader(n,s,r,i),endPos:r}}parseDict(e){const t=new DataView(e.buffer,e.byteOffset,e.bytesLength);let a=0;function parseOperand(){let n=e[a++];if(30===n)return function parseFloatOperand(){let t="";const n=15,s=["0","1","2","3","4","5","6","7","8","9",".","E","E-",null,"-"],r=e.length;for(;a>4,o=15&r;if(i===n)break;t+=s[i];if(o===n)break;t+=s[o]}return parseFloat(t)}();if(28===n){n=t.getInt16(a);a+=2;return n}if(29===n){n=t.getInt32(a);a+=4;return n}if(n>=32&&n<=246)return n-139;if(n>=247&&n<=250)return 256*(n-247)+e[a++]+108;if(n>=251&&n<=254)return-256*(n-251)-e[a++]-108;warn(`CFFParser.parseDict: "${n}" is a reserved command.`);return NaN}let n=[];const s=[];a=0;const r=e.length;for(;a10)return!1;const s=new DataView(t.buffer,t.byteOffset,t.bytesLength);let r=e.stackSize;const i=e.stack;let o=t.length;for(let l=0;l=4){r-=4;if(this.seacAnalysisEnabled){e.seac=i.slice(r,r+4);return!1}}c=On[f]}else if(f>=32&&f<=246){i[r]=f-139;r++}else if(f>=247&&f<=254){i[r]=f<251?(f-247<<8)+t[l]+108:-(f-251<<8)-t[l]-108;l++;r++}else if(255===f){i[r]=s.getInt32(l)/65536;l+=4;r++}else if(19===f||20===f){e.hints+=r>>1;if(0===e.hints){t.copyWithin(l-1,l,-1);l-=1;o-=1;continue}l+=e.hints+7>>3;r%=2;c=On[f]}else{if(10===f||29===f){const t=10===f?a:n;if(!t){c=On[f];warn("Missing subrsIndex for "+c.id);return!1}let s=32768;t.count<1240?s=107:t.count<33900&&(s=1131);const o=i[--r]+s;if(o<0||o>=t.count||isNaN(o)){c=On[f];warn("Out of bounds subrIndex for "+c.id);return!1}e.stackSize=r;e.callDepth++;if(!this.parseCharString(e,t.get(o),a,n))return!1;e.callDepth--;r=e.stackSize;continue}if(11===f){e.stackSize=r;return!0}if(0===f&&l===t.length){t[l-1]=14;c=On[14]}else{if(9===f){t.copyWithin(l-1,l,-1);l-=1;o-=1;continue}c=On[f]}}if(c){if(c.stem){e.hints+=r>>1;if(3===f||23===f)e.hasVStems=!0;else if(e.hasVStems&&(1===f||18===f)){warn("CFF stem hints are in wrong order");t[l-1]=1===f?3:23}}if(r=2&&c.stem?r%=2:r>1&&warn("Found too many parameters for stack-clearing command");r>0&&(e.width=i[r-1])}if("stackDelta"in c){"stackFn"in c&&c.stackFn(i,r);r+=c.stackDelta}else(c.stackClearing||c.resetStack)&&(r=0)}}o=s.length){warn("Invalid fd index for glyph index.");h=!1}if(h){m=s[e].privateDict;u=m.subrsIndex}}else t&&(u=t);h&&(h=this.parseCharString(c,l,u,a));if(null!==c.width){const e=m.getByName("nominalWidthX");o[f]=e+c.width}else{const e=m.getByName("defaultWidthX");o[f]=e}null!==c.seac&&(i[f]=c.seac);h||e.set(f,new Uint8Array([14]))}return{charStrings:e,seacs:i,widths:o}}emptyPrivateDictionary(e){const t=this.createDict(CFFPrivateDict,[],e.strings);e.setByKey(18,[0,0]);e.privateDict=t}parsePrivateDict(e){if(!e.hasName("Private")){this.emptyPrivateDictionary(e);return}const t=e.getByName("Private");if(!Array.isArray(t)||2!==t.length){e.removeByName("Private");return}const a=t[0],n=t[1];if(0===a||n>=this.bytes.length){this.emptyPrivateDictionary(e);return}const s=n+a,r=this.bytes.subarray(n,s),i=this.parseDict(r),o=this.createDict(CFFPrivateDict,i,e.strings);e.privateDict=o;0===o.getByName("ExpansionFactor")&&o.setByName("ExpansionFactor",.06);if(!o.getByName("Subrs"))return;const l=o.getByName("Subrs"),f=n+l;if(0===l||f>=this.bytes.length){this.emptyPrivateDictionary(e);return}const c=this.parseIndex(f);o.subrsIndex=c.obj}parseCharsets(e,t,a,n){if(0===e)return new CFFCharset(!0,Dn.ISO_ADOBE,xn);if(1===e)return new CFFCharset(!0,Dn.EXPERT,Cn);if(2===e)return new CFFCharset(!0,Dn.EXPERT_SUBSET,In);const s=this.bytes,r=e,i=s[e++],o=[n?0:".notdef"];let l,f,c;t-=1;switch(i){case 0:for(c=0;c=65535){warn("Not enough space in charstrings to duplicate first glyph.");return}const e=this.charStrings.get(0);this.charStrings.add(e);this.isCIDFont&&this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0])}hasGlyphId(e){if(e<0||e>=this.charStrings.count)return!1;return this.charStrings.get(e).length>0}}class CFFHeader{constructor(e,t,a,n){this.major=e;this.minor=t;this.hdrSize=a;this.offSize=n}}class CFFStrings{strings=[];get(e){return e>=0&&e<=390?Fn[e]:e-Tn<=this.strings.length?this.strings[e-Tn]:Fn[0]}getSID(e){let t=Fn.indexOf(e);if(-1!==t)return t;t=this.strings.indexOf(e);return-1!==t?t+Tn:-1}add(e){this.strings.push(e)}get count(){return this.strings.length}}class CFFIndex{objects=[];length=0;add(e){this.length+=e.length;this.objects.push(e)}set(e,t){this.length+=t.length-this.objects[e].length;this.objects[e]=t}get(e){return this.objects[e]}get count(){return this.objects.length}}class CFFDict{constructor(e,t){this.keyToNameMap=e.keyToNameMap;this.nameToKeyMap=e.nameToKeyMap;this.defaults=e.defaults;this.types=e.types;this.opcodes=e.opcodes;this.order=e.order;this.strings=t;this.values=Object.create(null)}setByKey(e,t){if(!(e in this.keyToNameMap))return!1;if(0===t.length)return!0;for(const a of t)if(isNaN(a)){warn(`Invalid CFFDict value: "${t}" for key "${e}".`);return!0}const a=this.types[e];"num"!==a&&"sid"!==a&&"offset"!==a||(t=t[0]);this.values[e]=t;return!0}setByName(e,t){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name "${e}"`);this.values[this.nameToKeyMap[e]]=t}hasName(e){return this.nameToKeyMap[e]in this.values}getByName(e){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name ${e}"`);const t=this.nameToKeyMap[e];return t in this.values?this.values[t]:this.defaults[t]}removeByName(e){delete this.values[this.nameToKeyMap[e]]}static createTables(e){const t={keyToNameMap:{},nameToKeyMap:{},defaults:{},types:{},opcodes:{},order:[]};for(const a of e){const e=Array.isArray(a[0])?(a[0][0]<<8)+a[0][1]:a[0];t.keyToNameMap[e]=a[1];t.nameToKeyMap[a[1]]=e;t.types[e]=a[2];t.defaults[e]=a[3];t.opcodes[e]=Array.isArray(a[0])?a[0]:[a[0]];t.order.push(e)}return t}}const Hn=[[[12,30],"ROS",["sid","sid","num"],null],[[12,20],"SyntheticBase","num",null],[0,"version","sid",null],[1,"Notice","sid",null],[[12,0],"Copyright","sid",null],[2,"FullName","sid",null],[3,"FamilyName","sid",null],[4,"Weight","sid",null],[[12,1],"isFixedPitch","num",0],[[12,2],"ItalicAngle","num",0],[[12,3],"UnderlinePosition","num",-100],[[12,4],"UnderlineThickness","num",50],[[12,5],"PaintType","num",0],[[12,6],"CharstringType","num",2],[[12,7],"FontMatrix",["num","num","num","num","num","num"],[.001,0,0,.001,0,0]],[13,"UniqueID","num",null],[5,"FontBBox",["num","num","num","num"],[0,0,0,0]],[[12,8],"StrokeWidth","num",0],[14,"XUID","array",null],[15,"charset","offset",0],[16,"Encoding","offset",0],[17,"CharStrings","offset",0],[18,"Private",["offset","offset"],null],[[12,21],"PostScript","sid",null],[[12,22],"BaseFontName","sid",null],[[12,23],"BaseFontBlend","delta",null],[[12,31],"CIDFontVersion","num",0],[[12,32],"CIDFontRevision","num",0],[[12,33],"CIDFontType","num",0],[[12,34],"CIDCount","num",8720],[[12,35],"UIDBase","num",null],[[12,37],"FDSelect","offset",null],[[12,36],"FDArray","offset",null],[[12,38],"FontName","sid",null]];class CFFTopDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Hn))}constructor(e){super(CFFTopDict.tables,e);this.privateDict=null}}const Bn=[[6,"BlueValues","delta",null],[7,"OtherBlues","delta",null],[8,"FamilyBlues","delta",null],[9,"FamilyOtherBlues","delta",null],[[12,9],"BlueScale","num",.039625],[[12,10],"BlueShift","num",7],[[12,11],"BlueFuzz","num",1],[10,"StdHW","num",null],[11,"StdVW","num",null],[[12,12],"StemSnapH","delta",null],[[12,13],"StemSnapV","delta",null],[[12,14],"ForceBold","num",0],[[12,17],"LanguageGroup","num",0],[[12,18],"ExpansionFactor","num",.06],[[12,19],"initialRandomSeed","num",0],[20,"defaultWidthX","num",0],[21,"nominalWidthX","num",0],[19,"Subrs","offset",null]];class CFFPrivateDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Bn))}constructor(e){super(CFFPrivateDict.tables,e);this.subrsIndex=null}}const Dn={ISO_ADOBE:0,EXPERT:1,EXPERT_SUBSET:2};class CFFCharset{constructor(e,t,a,n){this.predefined=e;this.format=t;this.charset=a;this.raw=n}}class CFFEncoding{constructor(e,t,a,n){this.predefined=e;this.format=t;this.encoding=a;this.raw=n}}class CFFFDSelect{constructor(e,t){this.format=e;this.fdSelect=t}getFDIndex(e){return e<0||e>=this.fdSelect.length?-1:this.fdSelect[e]}}class CFFOffsetTracker{offsets=Object.create(null);isTracking(e){return e in this.offsets}track(e,t){if(e in this.offsets)throw new FormatError(`Already tracking location of ${e}`);this.offsets[e]=t}offset(e){for(const t in this.offsets)this.offsets[t]+=e}setEntryLocation(e,t,a){if(!(e in this.offsets))throw new FormatError(`Not tracking location of ${e}`);const n=a.data,s=this.offsets[e];for(let e=0,a=t.length;e>24&255;n[i]=f>>16&255;n[o]=f>>8&255;n[l]=255&f}}}class CompilerOutput{#V;#$=1024;#Y=0;constructor(e){this.#J(e)}#J(e){for(;this.#$this.#$&&this.#J(t);this.#V.set(e,this.#Y);this.#Y=t}}class CFFCompiler{constructor(e){this.cff=e}compile(){const e=this.cff,t=new CompilerOutput(e.rawFileLength),a=this.compileHeader(e.header);t.add(a);const n=this.compileNameIndex(e.names);t.add(n);if(e.isCIDFont&&e.topDict.hasName("FontMatrix")){const t=e.topDict.getByName("FontMatrix");e.topDict.removeByName("FontMatrix");for(const a of e.fdArray){let e=t.slice(0);a.hasName("FontMatrix")&&(e=Util.transform(e,a.getByName("FontMatrix")));a.setByName("FontMatrix",e)}}const s=e.topDict.getByName("XUID");s?.length>16&&e.topDict.removeByName("XUID");e.topDict.setByName("charset",0);let r=this.compileTopDicts([e.topDict],t.length,e.isCIDFont);t.add(r.output);const i=r.trackers[0],o=this.compileStringIndex(e.strings.strings);t.add(o);const l=this.compileIndex(e.globalSubrIndex);t.add(l);if(e.encoding&&e.topDict.hasName("Encoding"))if(e.encoding.predefined)i.setEntryLocation("Encoding",[e.encoding.format],t);else{const a=this.compileEncoding(e.encoding);i.setEntryLocation("Encoding",[t.length],t);t.add(a)}const f=this.compileCharset(e.charset,e.charStrings.count,e.strings,e.isCIDFont);i.setEntryLocation("charset",[t.length],t);t.add(f);const c=this.compileCharStrings(e.charStrings);i.setEntryLocation("CharStrings",[t.length],t);t.add(c);if(e.isCIDFont){i.setEntryLocation("FDSelect",[t.length],t);const a=this.compileFDSelect(e.fdSelect);t.add(a);r=this.compileTopDicts(e.fdArray,t.length,!0);i.setEntryLocation("FDArray",[t.length],t);t.add(r.output);const n=r.trackers;this.compilePrivateDicts(e.fdArray,n,t)}this.compilePrivateDicts([e.topDict],[i],t);t.add([0]);return t.finalData}encodeNumber(e){return Number.isInteger(e)?this.encodeInteger(e):this.encodeFloat(e)}static get EncodeFloatRegExp(){return shadow(this,"EncodeFloatRegExp",/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/)}encodeFloat(e){let t=e.toString();const a=CFFCompiler.EncodeFloatRegExp.exec(t);if(a){const n=parseFloat("1e"+((a[2]?+a[2]:0)+a[1].length));t=(Math.round(e*n)/n).toString()}let n,s,r="";for(n=0,s=t.length;n=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?[28,e>>8&255,255&e]:[29,e>>24&255,e>>16&255,e>>8&255,255&e];return t}compileHeader(e){return[e.major,e.minor,4,e.offSize]}compileNameIndex(e){const t=new CFFIndex;for(const a of e){const e=Math.min(a.length,127);let n=new Array(e);for(let t=0;t"~"||"["===e||"]"===e||"("===e||")"===e||"{"===e||"}"===e||"<"===e||">"===e||"/"===e||"%"===e)&&(e="_");n[t]=e}n=n.join("");""===n&&(n="Bad_Font_Name");t.add(stringToBytes(n))}return this.compileIndex(t)}compileTopDicts(e,t,a){const n=[];let s=new CFFIndex;for(const r of e){if(a){r.removeByName("CIDFontVersion");r.removeByName("CIDFontRevision");r.removeByName("CIDFontType");r.removeByName("CIDCount");r.removeByName("UIDBase")}const e=new CFFOffsetTracker,i=this.compileDict(r,e);n.push(e);s.add(i);e.offset(t)}s=this.compileIndex(s,n);return{trackers:n,output:s}}compilePrivateDicts(e,t,a){for(let n=0,s=e.length;n>8&255,255&e])}else{s=new Uint8Array(1+2*r);let t=0;const n=e.charset.length;let i=!1;for(let r=1;r>8&255;s[r+1]=255&o}}return s}compileEncoding(e){return e.raw}compileFDSelect(e){const t=e.format;let a,n;switch(t){case 0:a=new Uint8Array(1+e.fdSelect.length);a[0]=t;for(n=0;n>8&255,255&s,r];for(n=1;n>8&255,255&n,t);r=t}}const o=(i.length-3)/3;i[1]=o>>8&255;i[2]=255&o;i.push(n>>8&255,255&n);a=new Uint8Array(i)}return a}compileIndex(e,t=[]){const a=e.objects,n=a.length;if(0===n)return new Uint8Array(2);let s,r,i=1;for(s=0;s>8&255;o[l++]=255&n;o[l++]=r;let f=1;for(s=0;s>8&255;o[l++]=255&f}else if(3===r){o[l++]=f>>16&255;o[l++]=f>>8&255;o[l++]=255&f}else{o[l++]=f>>>24&255;o[l++]=f>>16&255;o[l++]=f>>8&255;o[l++]=255&f}a[s]&&(f+=a[s].length)}for(s=0;s=this.firstChar&&e<=this.lastChar?e:-1}amend(e){unreachable("Should not call amend()")}}class CFFFont{constructor(e,t){this.properties=t;const a=new CFFParser(e,t,jn);this.cff=a.parse();this.cff.duplicateFirstGlyph();const n=new CFFCompiler(this.cff);this.seacs=this.cff.seacs;try{this.data=n.compile()}catch{warn("Failed to compile font "+t.loadedName);this.data=e}this._createBuiltInEncoding()}get numGlyphs(){return this.cff.charStrings.count}getCharset(){return this.cff.charset.charset}getGlyphMapping(){const e=this.cff,t=this.properties,{cidToGidMap:a,cMap:n}=t,s=e.charset.charset;let r,i;if(t.composite){let t,o;if(a?.length>0){t=Object.create(null);for(let e=0,n=a.length;e=0){const n=a[t];n&&(s[e]=n)}}s.length>0&&(this.properties.builtInEncoding=s)}}function getFloat214(e,t){return e.getInt16(t)/16384}function getSubroutineBias(e){const t=e.length;let a=32768;t<1240?a=107:t<33900&&(a=1131);return a}function parseCmap(e,t,a){const n=new DataView(e.buffer,e.byteOffset,e.byteLength),s=1===n.getUint16(t+2)?n.getUint32(t+8):n.getUint32(t+16),r=n.getUint16(t+s);let i,o,l;if(4===r){const e=n.getUint16(t+s+6)>>1;o=t+s+14;i=[];for(l=0;l>1;a0;)h.push({flags:o})}for(a=0;a>1;j=!0;break;case 4:i+=s.pop();moveTo(r,i);j=!0;break;case 5:for(;s.length>0;){r+=s.shift();i+=s.shift();lineTo(r,i)}break;case 6:for(;s.length>0;){r+=s.shift();lineTo(r,i);if(0===s.length)break;i+=s.shift();lineTo(r,i)}break;case 7:for(;s.length>0;){i+=s.shift();lineTo(r,i);if(0===s.length)break;r+=s.shift();lineTo(r,i)}break;case 8:for(;s.length>0;){c=r+s.shift();u=i+s.shift();h=c+s.shift();m=u+s.shift();r=h+s.shift();i=m+s.shift();bezierCurveTo(c,u,h,m,r,i)}break;case 10:b=s.pop();w=null;if(a.isCFFCIDFont){const e=a.fdSelect.getFDIndex(n);if(e>=0&&eMath.abs(i-t)?r+=s.shift():i+=s.shift();bezierCurveTo(c,u,h,m,r,i);break;default:throw new FormatError(`unknown operator: 12 ${k}`)}break;case 14:if(s.length>=4){const e=s.pop(),n=s.pop();i=s.pop();r=s.pop();t.save();t.translate(r,i);let o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[fn[e]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId);t.restore();o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[fn[n]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId)}return;case 19:case 20:o+=s.length>>1;f+=o+7>>3;j=!0;break;case 21:i+=s.pop();r+=s.pop();moveTo(r,i);j=!0;break;case 22:r+=s.pop();moveTo(r,i);j=!0;break;case 24:for(;s.length>2;){c=r+s.shift();u=i+s.shift();h=c+s.shift();m=u+s.shift();r=h+s.shift();i=m+s.shift();bezierCurveTo(c,u,h,m,r,i)}r+=s.shift();i+=s.shift();lineTo(r,i);break;case 25:for(;s.length>6;){r+=s.shift();i+=s.shift();lineTo(r,i)}c=r+s.shift();u=i+s.shift();h=c+s.shift();m=u+s.shift();r=h+s.shift();i=m+s.shift();bezierCurveTo(c,u,h,m,r,i);break;case 26:s.length%2&&(r+=s.shift());for(;s.length>0;){c=r;u=i+s.shift();h=c+s.shift();m=u+s.shift();r=h;i=m+s.shift();bezierCurveTo(c,u,h,m,r,i)}break;case 27:s.length%2&&(i+=s.shift());for(;s.length>0;){c=r+s.shift();u=i;h=c+s.shift();m=u+s.shift();r=h+s.shift();i=m;bezierCurveTo(c,u,h,m,r,i)}break;case 28:s.push(l.getInt16(f));f+=2;break;case 29:b=s.pop()+a.gsubrsBias;w=a.gsubrs[b];w&&parse(w);break;case 30:for(;s.length>0;){c=r;u=i+s.shift();h=c+s.shift();m=u+s.shift();r=h+s.shift();i=m+(1===s.length?s.shift():0);bezierCurveTo(c,u,h,m,r,i);if(0===s.length)break;c=r+s.shift();u=i;h=c+s.shift();m=u+s.shift();i=m+s.shift();r=h+(1===s.length?s.shift():0);bezierCurveTo(c,u,h,m,r,i)}break;case 31:for(;s.length>0;){c=r+s.shift();u=i;h=c+s.shift();m=u+s.shift();i=m+s.shift();r=h+(1===s.length?s.shift():0);bezierCurveTo(c,u,h,m,r,i);if(0===s.length)break;c=r;u=i+s.shift();h=c+s.shift();m=u+s.shift();r=h+s.shift();i=m+(1===s.length?s.shift():0);bezierCurveTo(c,u,h,m,r,i)}break;default:if(k<32)throw new FormatError(`unknown operator: ${k}`);if(k<247)s.push(k-139);else if(k<251)s.push(256*(k-247)+e[f++]+108);else if(k<255)s.push(256*-(k-251)-e[f++]-108);else{s.push(l.getInt32(f)/65536);f+=4}}j&&(s.length=0)}}(e)}class Commands{cmds=[];transformStack=[];currentTransform=[1,0,0,1,0,0];add(e,t){if(t){const{currentTransform:a}=this;for(let e=0,n=t.length;e=0&&ee.getUint32(t)}else{s=2;r=(e,t)=>2*e.getUint16(t)}const i=[];let o=r(n,0);for(let a=s;ae.getSize()+3&-4))}write(){const e=this.getSize(),t=new DataView(new ArrayBuffer(e)),a=e>131070,n=a?4:2,s=new DataView(new ArrayBuffer((this.glyphs.length+1)*n));a?s.setUint32(0,0):s.setUint16(0,0);let r=0,i=0;for(const e of this.glyphs){r+=e.write(r,t);r=r+3&-4;i+=n;a?s.setUint32(i,r):s.setUint16(i,r>>1)}return{isLocationLong:a,loca:new Uint8Array(s.buffer),glyf:new Uint8Array(t.buffer)}}scale(e){for(let t=0,a=this.glyphs.length;te.getSize()));return this.header.getSize()+e}write(e,t){if(!this.header)return 0;const a=e;e+=this.header.write(e,t);if(this.simple)e+=this.simple.write(e,t);else for(const a of this.composites)e+=a.write(e,t);return e-a}scale(e){if(!this.header)return;const t=(this.header.xMin+this.header.xMax)/2;this.header.scale(t,e);if(this.simple)this.simple.scale(t,e);else for(const a of this.composites)a.scale(t,e)}}class GlyphHeader{constructor({numberOfContours:e,xMin:t,yMin:a,xMax:n,yMax:s}){this.numberOfContours=e;this.xMin=t;this.yMin=a;this.xMax=n;this.yMax=s}static parse(e,t){return[10,new GlyphHeader({numberOfContours:t.getInt16(e),xMin:t.getInt16(e+2),yMin:t.getInt16(e+4),xMax:t.getInt16(e+6),yMax:t.getInt16(e+8)})]}getSize(){return 10}write(e,t){t.setInt16(e,this.numberOfContours);t.setInt16(e+2,this.xMin);t.setInt16(e+4,this.yMin);t.setInt16(e+6,this.xMax);t.setInt16(e+8,this.yMax);return 10}scale(e,t){this.xMin=Math.round(e+(this.xMin-e)*t);this.xMax=Math.round(e+(this.xMax-e)*t)}}class Contour{constructor({flags:e,xCoordinates:t,yCoordinates:a}){this.xCoordinates=t;this.yCoordinates=a;this.flags=e}}class SimpleGlyph{constructor({contours:e,instructions:t}){this.contours=e;this.instructions=t}static parse(e,t,a){const n=[];for(let s=0;s255?e+=2:o>0&&(e+=1);t=r;o=Math.abs(i-a);o>255?e+=2:o>0&&(e+=1);a=i}}return e}write(e,t){const a=e,n=[],s=[],r=[];let i=0,o=0;for(const a of this.contours){for(let e=0,t=a.xCoordinates.length;e=0?18:2;n.push(e)}else n.push(f)}i=l;const c=a.yCoordinates[e];f=c-o;if(0===f){t|=32;s.push(0)}else{const e=Math.abs(f);if(e<=255){t|=f>=0?36:4;s.push(e)}else s.push(f)}o=c;r.push(t)}t.setUint16(e,n.length-1);e+=2}t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}for(const a of r)t.setUint8(e++,a);for(let a=0,s=n.length;a=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(e+=2):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(e+=2);return e}write(e,t){const a=e;2&this.flags?this.argument1>=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(this.flags|=1):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(this.flags|=1);t.setUint16(e,this.flags);t.setUint16(e+2,this.glyphIndex);e+=4;if(1&this.flags){if(2&this.flags){t.setInt16(e,this.argument1);t.setInt16(e+2,this.argument2)}else{t.setUint16(e,this.argument1);t.setUint16(e+2,this.argument2)}e+=4}else{t.setUint8(e,this.argument1);t.setUint8(e+1,this.argument2);e+=2}if(256&this.flags){t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}}return e-a}scale(e,t){}}class OpenTypeFileBuilder{#Q=new Map;constructor(e){this.sfnt=e}static getSearchParams(e,t){let a=1,n=0;for(;(a^e)>a;){a<<=1;n++}const s=a*t;return{range:s,entry:n,rangeShift:t*e-s}}toArray(){let e=this.sfnt;const t=this.#Q,a=[...t.keys()].sort(),n=a.length;let s=12+16*n;const r=[s];for(let e=0;e>>0;r.push(s)}const i=new Uint8Array(s),o=new DataView(i.buffer);for(let e=0;e>>0}o.setInt32(s+4,l);o.setInt32(s+8,r[e]);o.setInt32(s+12,t.get(n).length);s+=16}this.#Q.clear();return i}addTable(e,t){if(this.#Q.has(e))throw new Error(`Table ${e} already exists`);this.#Q.set(e,t)}}const Kn=[4],Gn=[5],Vn=[6],$n=[7],Yn=[8],Jn=[12,35],Qn=[14],Zn=[21],es=[22],ts=[30],as=[31];class Type1CharString{width=0;lsb=0;flexing=!1;output=[];stack=[];convert(e,t,a){const n=e.length;let s,r,i,o=!1;for(let l=0;ln)return!0;const s=n-e;for(let e=s;e>8&255,255&t);else{t=65536*t|0;this.output.push(255,t>>24&255,t>>16&255,t>>8&255,255&t)}}this.output.push(...t);a?this.stack.splice(s,e):this.stack.length=0;return!1}}function isHexDigit(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function decrypt(e,t,a){if(a>=e.length)return new Uint8Array(0);let n,s,r=0|t;for(n=0;n>8;r=52845*(t+r)+22719&65535}return o}function isSpecial(e){return 47===e||91===e||93===e||123===e||125===e||40===e||41===e}class Type1Parser{constructor(e,t,a){if(t){const t=e.getBytes(),a=!((isHexDigit(t[0])||isWhiteSpace(t[0]))&&isHexDigit(t[1])&&isHexDigit(t[2])&&isHexDigit(t[3])&&isHexDigit(t[4])&&isHexDigit(t[5])&&isHexDigit(t[6])&&isHexDigit(t[7]));e=new Stream(a?decrypt(t,55665,4):function decryptAscii(e,t,a){let n=0|t;const s=e.length,r=new Uint8Array(s>>>1);let i,o;for(i=0,o=0;i>8;n=52845*(e+n)+22719&65535}}return r.slice(a,o)}(t,55665,4))}this.seacAnalysisEnabled=!!a;this.stream=e;this.nextChar()}readNumberArray(){this.getToken();const e=[];for(;;){const t=this.getToken();if(null===t||"]"===t||"}"===t)break;e.push(parseFloat(t||0))}return e}readNumber(){const e=this.getToken();return parseFloat(e||0)}readInt(){const e=this.getToken();return 0|parseInt(e||0,10)}readBoolean(){return"true"===this.getToken()?1:0}nextChar(){return this.currentChar=this.stream.getByte()}prevChar(){this.stream.skip(-2);return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(-1===t)return null;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}if(isSpecial(t)){this.nextChar();return String.fromCharCode(t)}let a="";do{a+=String.fromCharCode(t);t=this.nextChar()}while(t>=0&&!isWhiteSpace(t)&&!isSpecial(t));return a}readCharStrings(e,t){return-1===t?e:decrypt(e,4330,t)}extractFontProgram(e){const t=this.stream,a=[],n=[],s=new Map([["lenIV",4]]),r={subrs:[],charstrings:[],properties:{privateData:s}};let i,o,l,f;for(;null!==(i=this.getToken());)if("/"===i){i=this.getToken();switch(i){case"CharStrings":this.getToken();this.getToken();this.getToken();this.getToken();for(;;){i=this.getToken();if(null===i||"end"===i)break;if("/"!==i)continue;const e=this.getToken();o=this.readInt();this.getToken();l=o>0?t.getBytes(o):new Uint8Array(0);f=s.get("lenIV");const a=this.readCharStrings(l,f);this.nextChar();i=this.getToken();"noaccess"===i?this.getToken():"/"===i&&this.prevChar();n.push({glyph:e,encoded:a})}break;case"Subrs":this.readInt();this.getToken();for(;"dup"===this.getToken();){const e=this.readInt();o=this.readInt();this.getToken();l=o>0?t.getBytes(o):new Uint8Array(0);f=s.get("lenIV");const n=this.readCharStrings(l,f);this.nextChar();i=this.getToken();"noaccess"===i&&this.getToken();a[e]=n}break;case"BlueValues":case"OtherBlues":case"FamilyBlues":case"FamilyOtherBlues":const e=this.readNumberArray();e.length>0&&e.length,0;break;case"StemSnapH":case"StemSnapV":s.set(i,this.readNumberArray());break;case"StdHW":case"StdVW":s.set(i,this.readNumberArray()[0]);break;case"BlueShift":case"lenIV":case"BlueFuzz":case"BlueScale":case"LanguageGroup":s.set(i,this.readNumber());break;case"ExpansionFactor":s.set(i,this.readNumber()||.06);break;case"ForceBold":s.set(i,this.readBoolean())}}for(const{encoded:t,glyph:s}of n){const n=new Type1CharString,i=n.convert(t,a,this.seacAnalysisEnabled);let o=n.output;i&&(o=[14]);const l={glyphName:s,charstring:o,width:n.width,lsb:n.lsb,seac:n.seac};".notdef"===s?r.charstrings.unshift(l):r.charstrings.push(l);if(e.builtInEncoding){const t=e.builtInEncoding.indexOf(s);t>-1&&void 0===e.widths[t]&&t>=e.firstChar&&t<=e.lastChar&&(e.widths[t]=n.width)}}return r}extractFontHeader(e){let t;for(;null!==(t=this.getToken());)if("/"===t){t=this.getToken();switch(t){case"FontMatrix":const a=this.readNumberArray();e.fontMatrix=a;break;case"Encoding":const n=this.getToken();let s;if(/^\d+$/.test(n)){s=[];const e=0|parseInt(n,10);this.getToken();for(let a=0;a=s){i+=a;for(;i=0&&(n[e]=s)}}return type1FontGlyphMapping(e,n,a)}hasGlyphId(e){if(e<0||e>=this.numGlyphs)return!1;if(0===e)return!0;return this.charstrings[e-1].charstring.length>0}getSeacs(e){const t=[];for(let a=0,n=e.length;a0;e--)t[e]-=t[e-1];h.setByName(e,t)}r.topDict.privateDict=h;const m=new CFFIndex;for(const e of n)m.add(e);h.subrsIndex=m;return new CFFCompiler(r).compile()}}const ns=[[57344,63743],[1048576,1114109]],ss=1e3,rs=["ascent","bbox","black","bold","cssFontInfo","data","defaultVMetrics","defaultWidth","descent","disableFontFace","fallbackName","fontExtraProperties","fontMatrix","isInvalidPDFjsFont","isType3Font","italic","loadedName","mimetype","missingFile","name","remeasure","systemFontInfo","vertical"],is=["cMap","composite","defaultEncoding","differences","isMonospace","isSerifFont","isSymbolicFont","seacMap","subtype","toFontChar","toUnicode","type","vmetrics","widths"];function adjustWidths(e){if(!e.fontMatrix)return;if(e.fontMatrix[0]===n[0])return;const t=.001/e.fontMatrix[0],a=e.widths;for(const e in a)a[e]*=t;e.defaultWidth*=t}function amendFallbackToUnicode(e){if(!e.fallbackToUnicode)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const t=[];for(const a in e.fallbackToUnicode)e.toUnicode.has(a)||(t[a]=e.fallbackToUnicode[a]);t.length>0&&e.toUnicode.amend(t)}class fonts_Glyph{constructor(e,t,a,n,s,r,i,o,l){this.originalCharCode=e;this.fontChar=t;this.unicode=a;this.accent=n;this.width=s;this.vmetric=r;this.operatorListId=i;this.isSpace=o;this.isInFont=l}get category(){return shadow(this,"category",function getCharUnicodeCategory(e){const t=wn.get(e);if(t)return t;const a=e.match(bn),n={isWhitespace:!!a?.[1],isZeroWidthDiacritic:!!a?.[2],isInvisibleFormatMark:!!a?.[3]};wn.set(e,n);return n}(this.unicode),!0)}}function int16(e,t){return(e<<8)+t}function writeSignedInt16(e,t,a){e[t+1]=a;e[t]=a>>>8}function signedInt16(e,t){const a=(e<<8)+t;return 32768&a?a-65536:a}class TrueTypeTableBuilder{#V;#$=1024;#ee=!1;#Y=0;#te;constructor({exactLength:e,minLength:t}){this.#ee=!!e;this.#J(e||t)}#J(e){if(this.#ee)this.#$=e;else for(;this.#$this.#$&&this.#J(t);this.#V.set(e,this.#Y);this.#Y=t}setInt16(e){const t=this.#Y+2;!this.#ee&&t>this.#$&&this.#J(t);this.#te.setInt16(this.#Y,e);this.#Y=t}setSafeInt16(e){const t=this.#Y+2;!this.#ee&&t>this.#$&&this.#J(t);this.#te.setInt16(this.#Y,MathClamp(e,-32768,32767));this.#Y=t}setInt32(e){const t=this.#Y+4;!this.#ee&&t>this.#$&&this.#J(t);this.#te.setInt32(this.#Y,e);this.#Y=t}}function isTrueTypeCollectionFile(e){return"ttcf"===bytesToString(e.peekBytes(4))}function getFontFileType(e,{type:t,subtype:a,composite:n}){let s,r;if(function isTrueTypeFile(e){const t=bytesToString(e.peekBytes(4));return"\0\0\0"===t||"true"===t}(e)||isTrueTypeCollectionFile(e))s=n?"CIDFontType2":"TrueType";else if(function isOpenTypeFile(e){return"OTTO"===bytesToString(e.peekBytes(4))}(e))s=n?"CIDFontType2":"OpenType";else if(function isType1File(e){const t=e.peekBytes(2);return 37===t[0]&&33===t[1]||128===t[0]&&1===t[1]}(e))s=n?"CIDFontType0":"MMType1"===t?"MMType1":"Type1";else if(function isCFFFile(e){const t=e.peekBytes(4);return t[0]>=1&&t[3]>=1&&t[3]<=4}(e))if(n){s="CIDFontType0";r="CIDFontType0C"}else{s="MMType1"===t?"MMType1":"Type1";r="Type1C"}else{warn("getFontFileType: Unable to detect correct font file Type/Subtype.");s=t;r=a}return[s,r]}function applyStandardFontGlyphMap(e,t){for(const a in t)e[+a]=t[a]}function buildToFontChar(e,t,a){const n=[];let s;for(let a=0,r=e.length;ans[0][0]<=e&&e<=ns[0][1]||ns[1][0]<=e&&e<=ns[1][1];let h=null;for(const u in e){let m=e[u];if(!t(m))continue;if(f>c){l++;if(l>=ns.length){warn("Ran out of space in font private use area.");break}f=ns[l][0];c=ns[l][1]}const p=f++;0===m&&(m=a);let d=n.get(u);if("string"==typeof d)if(1===d.length)d=d.codePointAt(0);else{if(!h){h=new Map;for(let e=64256;e<=64335;e++){const t=String.fromCharCode(e).normalize("NFKD");t.length>1&&h.set(t,e)}}d=h.get(d)||d.codePointAt(0)}if(d&&!isInPrivateArea(d)&&!o.has(m)){r.set(d,m);o.add(m)}s[p]=m;i[u]=p}return{toFontChar:i,charCodeToGlyphId:s,toUnicodeExtraMap:r,nextAvailableFontCharCode:f}}function createCmapTable(e,t,a){const n=function getRanges(e,t,a){const n=[];for(const t in e)e[t]>=a||n.push({fontCharCode:0|t,glyphId:e[t]});if(t)for(const[e,s]of t)s>=a||n.push({fontCharCode:e,glyphId:s});0===n.length&&n.push({fontCharCode:0,glyphId:0});n.sort((e,t)=>e.fontCharCode-t.fontCharCode);const s=[],r=n.length;for(let e=0;e65535?2:1,r=new TrueTypeTableBuilder({exactLength:12});r.skip(2);r.setInt16(s);r.setArray([0,3]);r.setArray([0,1]);r.setInt32(4+8*s);let i,o,l,f;for(i=n.length-1;i>=0&&!(n[i][0]<=65535);--i);const c=i+1;n[i][0]<65535&&65535===n[i][1]&&(n[i][1]=65534);const h=n[i][1]<65535?1:0,u=c+h,m=OpenTypeFileBuilder.getSearchParams(u,2),p=2*c+2*h,d=new TrueTypeTableBuilder({exactLength:p}),g=new TrueTypeTableBuilder({exactLength:p}),b=new TrueTypeTableBuilder({exactLength:p}),w=new TrueTypeTableBuilder({exactLength:p}),j=new TrueTypeTableBuilder({});let k=0;for(i=0,o=c;i0){g.setArray([255,255]);d.setArray([255,255]);b.setArray([0,1]);w.skip(2)}const y=new TrueTypeTableBuilder({exactLength:12+d.length+g.length+b.length+w.length+j.length});y.skip(2);y.setInt16(2*u);y.setInt16(m.range);y.setInt16(m.entry);y.setInt16(m.rangeShift);y.setArray(g.data);y.skip(2);y.setArray(d.data);y.setArray(b.data);y.setArray(w.data);y.setArray(j.data);let q=null,v=null,S=null;if(s>1){q=new TrueTypeTableBuilder({exactLength:8});q.setArray([0,3]);q.setArray([0,10]);q.setInt32(4+8*s+4+y.length);v=new TrueTypeTableBuilder({});for(const e of n){let t=e[0];const a=e[2];let n=a[0];for(l=1,f=a.length;le||!o)&&(o=e);l 123 are reserved for internal usage");i|=1<65535&&(l=65535)}else{o=0;l=255}const c=e.bbox||[0,0,0,0],h=a.unitsPerEm||(e.fontMatrix?1/Math.max(...e.fontMatrix.slice(0,4).map(Math.abs)):1e3),u=e.ascentScaled?1:h/ss,m=a.ascent||Math.round(u*(e.ascent||c[3]));let p=a.descent||Math.round(u*(e.descent||c[1]));p>0&&e.descent>0&&c[1]<0&&(p=-p);const d=a.yMax||m,g=-a.yMin||-p,b=new TrueTypeTableBuilder({exactLength:96});b.setArray([0,3]);b.setArray([2,36]);b.setArray([1,244]);b.setArray([0,5]);b.skip(2);b.setArray([2,138]);b.setArray([2,187]);b.skip(2);b.setArray([0,140]);b.setArray([2,138]);b.setArray([2,187]);b.skip(2);b.setArray([1,223]);b.setArray([0,49]);b.setArray([1,2]);b.skip(2);b.setArray([0,0,6,e.fixedPitch?9:0,0,0,0,0,0,0]);b.setInt32(n);b.setInt32(s);b.setInt32(r);b.setInt32(i);b.setArray([42,50,49,42]);b.setInt16(e.italicAngle?1:0);b.setInt16(o||e.firstChar);b.setInt16(l||e.lastChar);b.setInt16(m);b.setInt16(p);b.setArray([0,100]);b.setInt16(d);b.setInt16(g);b.skip(8);b.setInt16(e.xHeight);b.setInt16(e.capHeight);b.skip(2);b.setInt16(o||e.firstChar);b.setArray([0,3]);return b.data}function createPostTable(e){const t=new TrueTypeTableBuilder({exactLength:32});t.setArray([0,3,0,0]);t.setInt32(Math.floor(65536*e.italicAngle));t.skip(4);t.setInt32(e.fixedPitch?1:0);t.skip(16);return t.data}function createPostscriptName(e){return e.replaceAll(/[^\x21-\x7E]|[[\](){}<>/%]/g,"").slice(0,63)}function createNameTable(e,t){t||=[[],[]];const a=[t[0][0]||"Original licence",t[0][1]||e,t[0][2]||"Unknown",t[0][3]||"uniqueID",t[0][4]||e,t[0][5]||"Version 0.11",t[0][6]||createPostscriptName(e),t[0][7]||"Unknown",t[0][8]||"Unknown",t[0][9]||"Unknown"],n=a.map(e=>stringToBytes(e)),s=new Array(a.length);let r,i,o,l,f;for(r=0,i=a.length;re.length))+Math.sumPrecise(n.map(e=>e.length))+Math.sumPrecise(s.map(e=>e.length))});b.skip(2);b.setInt16(g);b.setInt16(12*g+6);for(const e of p)b.setArray(e);for(const e of n)b.setArray(e);for(const e of s)b.setArray(e);return b.data}class Font{#ae=new Map;#ne=new Map;charProcOperatorList;constructor(e,t,a,n){this.name=e;this.psName=null;this.mimetype=null;this.disableFontFace=n.disableFontFace;this.fontExtraProperties=n.fontExtraProperties;this.loadedName=a.loadedName;this.isType3Font=a.isType3Font;this.missingFile=!1;this.cssFontInfo=a.cssFontInfo;let s=!!(a.flags&yn);if(!s&&!a.isSimulatedFlags){const t=Mn(),a=Pn(),n=En();for(const r of e.split("+")){let e=normalizeFontName(r);e=t[e]||a[e]||e;e=e.split("-",1)[0];if(n[e]){s=!0;break}}}this.isSerifFont=s;this.isSymbolicFont=!!(a.flags&qn);this.isMonospace=!!(a.flags&kn);let{type:r,subtype:i}=a;this.type=r;this.subtype=i;this.systemFontInfo=a.systemFontInfo;const o=e.match(/^InvalidPDFjsFont_(.*)_\d+$/);this.isInvalidPDFjsFont=!!o;this.isInvalidPDFjsFont?this.fallbackName=o[1]:this.isMonospace?this.fallbackName="monospace":this.isSerifFont?this.fallbackName="serif":this.fallbackName="sans-serif";if(this.systemFontInfo?.guessFallback){this.systemFontInfo.guessFallback=!1;this.systemFontInfo.css+=`,${this.fallbackName}`}this.differences=a.differences;this.widths=a.widths;this.defaultWidth=a.defaultWidth;this.composite=a.composite;this.cMap=a.cMap;this.capHeight=a.capHeight/ss;this.ascent=a.ascent/ss;this.descent=a.descent/ss;this.lineHeight=this.ascent-this.descent;this.fontMatrix=a.fontMatrix;this.bbox=a.bbox;this.defaultEncoding=a.defaultEncoding;this.toUnicode=a.toUnicode;this.toFontChar=[];if("Type3"===a.type){for(let e=0;e<256;e++)this.toFontChar[e]=this.differences[e]||a.defaultEncoding[e];return}this.cidEncoding=a.cidEncoding||"";this.vertical=!!a.vertical;if(this.vertical){this.vmetrics=a.vmetrics;this.defaultVMetrics=a.defaultVMetrics}if(!t||t.isEmpty){t&&warn('Font file is empty in "'+e+'" ('+this.loadedName+")");this.fallbackToSystemFont(a);return}[r,i]=getFontFileType(t,a);r===this.type&&i===this.subtype||info(`Inconsistent font file Type/SubType, expected: ${this.type}/${this.subtype} but found: ${r}/${i}.`);let l;try{switch(r){case"MMType1":info("MMType1 font ("+e+"), falling back to Type1.");case"Type1":case"CIDFontType0":this.mimetype="font/opentype";const n="Type1C"===i||"CIDFontType0C"===i?new CFFFont(t,a):new Type1Font(e,t,a);adjustWidths(a);l=this.convert(e,n,a);break;case"OpenType":case"TrueType":case"CIDFontType2":this.mimetype="font/opentype";l=this.checkAndRepair(e,t,a);adjustWidths(a);this.isOpenType&&(r="OpenType");break;default:throw new FormatError(`Font ${r} is not supported`)}}catch(e){warn(e);this.fallbackToSystemFont(a);return}amendFallbackToUnicode(a);this.data=l;this.type=r;this.subtype=i;this.fontMatrix=a.fontMatrix;this.widths=a.widths;this.defaultWidth=a.defaultWidth;this.toUnicode=a.toUnicode;this.seacMap=a.seacMap}get renderer(){return shadow(this,"renderer",FontRendererFactory.create(this,jn))}#se(e){const t=Object.create(null);for(const a of e){const e=this[a];void 0!==e&&(t[a]=e)}return t}exportData(){return{buffer:compileFontInfo(this.#se(rs)),charProcOperatorList:this.charProcOperatorList,extra:this.fontExtraProperties?this.#se(is):void 0}}fallbackToSystemFont(e){this.missingFile=!0;const{name:t,type:a}=this;let n=normalizeFontName(t);const s=Mn(),r=Pn(),i=!!s[n],o=!(!r[n]||!s[r[n]]);n=s[n]||r[n]||n;const l=Wn()[n];if(l){isNaN(this.ascent)&&(this.ascent=l.ascent/ss);isNaN(this.descent)&&(this.descent=l.descent/ss);isNaN(this.capHeight)&&(this.capHeight=l.capHeight/ss)}this.bold=/bold/gi.test(n);this.italic=/oblique|italic/gi.test(n);this.black=/Black/g.test(t);const f=/Narrow/g.test(t);this.remeasure=(!i||f)&&Object.keys(this.widths).length>0;if((i||o)&&"CIDFontType2"===a&&this.cidEncoding.startsWith("Identity-")){const a=e.cidToGidMap,n=[];applyStandardFontGlyphMap(n,zn());/Arial-?Black/i.test(t)?applyStandardFontGlyphMap(n,Ln()):/Calibri/i.test(t)&&applyStandardFontGlyphMap(n,Un());if(a){for(const e in n){const t=n[e];void 0!==a[t]&&(n[+e]=a[t])}a.length!==this.toUnicode.length&&e.hasIncludedToUnicodeMap&&this.toUnicode instanceof IdentityToUnicodeMap&&this.toUnicode.forEach(function(e,t){const s=n[e];void 0===a[s]&&(n[+e]=t)})}this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach(function(e,t){n[+e]=t});this.toFontChar=n;this.toUnicode=new ToUnicodeMap(n)}else if(/Symbol/i.test(n))this.toFontChar=buildToFontChar(hn,mn(),this.differences);else if(/Dingbats/i.test(n))this.toFontChar=buildToFontChar(un,pn(),this.differences);else if(i||o){const e=buildToFontChar(this.defaultEncoding,mn(),this.differences);"CIDFontType2"!==a||this.cidEncoding.startsWith("Identity-")||this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach(function(t,a){e[+t]=a});this.toFontChar=e}else{const e=mn(),a=[];this.toUnicode.forEach((t,n)=>{if(!this.composite){const a=getUnicodeForGlyph(this.differences[t]||this.defaultEncoding[t],e);-1!==a&&(n=a)}a[+t]=n});this.composite&&this.toUnicode instanceof IdentityToUnicodeMap&&/Tahoma|Verdana/i.test(t)&&applyStandardFontGlyphMap(a,zn());this.toFontChar=a}amendFallbackToUnicode(e);this.loadedName=n.split("-",1)[0]}checkAndRepair(e,t,a){const n=["OS/2","cmap","head","hhea","hmtx","maxp","name","post","loca","glyf","fpgm","prep","cvt ","CFF "];function readTables(e,t){const a=Object.create(null);a["OS/2"]=null;a.cmap=null;a.head=null;a.hhea=null;a.hmtx=null;a.maxp=null;a.name=null;a.post=null;for(let s=0;s>>0,n=e.getInt32()>>>0,s=e.getInt32()>>>0,r=e.pos;e.pos=e.start||0;e.skip(n);const i=e.getBytes(s);e.pos=r;if("head"===t){i[8]=i[9]=i[10]=i[11]=0;i[17]|=32}return{tag:t,checksum:a,length:s,offset:n,data:i,view:new DataView(i.buffer,i.byteOffset,i.byteLength)}}function readOpenTypeHeader(e){return{version:e.getString(4),numTables:e.getUint16(),searchRange:e.getUint16(),entrySelector:e.getUint16(),rangeShift:e.getUint16()}}function sanitizeGlyph(e,t,a,n,s,r){const i={length:0,sizeOfInstructions:0};if(t<0||t>=e.length||a>e.length||a-t<=12)return i;const o=e.subarray(t,a),l=signedInt16(o[2],o[3]),f=signedInt16(o[4],o[5]),c=signedInt16(o[6],o[7]),h=signedInt16(o[8],o[9]);if(l>c){writeSignedInt16(o,2,c);writeSignedInt16(o,6,l)}if(f>h){writeSignedInt16(o,4,h);writeSignedInt16(o,8,f)}const u=signedInt16(o[0],o[1]);if(u<0){if(u<-1)return i;n.set(o,s);i.length=o.length;return i}let m,p=10,d=0;for(m=0;mo.length)return i;if(!r&&b>0){n.set(o.subarray(0,g),s);n.set([0,0],s+g);n.set(o.subarray(w,k),s+g+2);k-=b;o.length-k>3&&(k=k+3&-4);i.length=k;return i}if(o.length-k>3){k=k+3&-4;n.set(o.subarray(0,k),s);i.length=k;return i}n.set(o,s);i.length=o.length;return i}function readNameTable(e){const a=(t.start||0)+e.offset;t.pos=a;const n=[[],[]],s=[],r=e.length,i=a+r;if(0!==t.getUint16()||r<6)return[n,s];const o=t.getUint16(),l=t.getUint16();let f,c;for(f=0;fi)continue;t.pos=r;const o=e.name;if(e.encoding){let a="";for(let n=0,s=e.length;n0&&(f+=e-1)}}else{if(g||w){warn("TT: nested FDEFs not allowed");d=!0}g=!0;h=f;i=u.pop();t.functionsDefined[i]={data:l,i:f}}else if(!g&&!w){i=u.at(-1);if(isNaN(i))info("TT: CALL empty stack (or invalid entry).");else{t.functionsUsed[i]=!0;if(i in t.functionsStackDeltas){const e=u.length+t.functionsStackDeltas[i];if(e<0){warn("TT: CALL invalid functions stack delta.");t.hintsValid=!1;return}u.length=e}else if(i in t.functionsDefined&&!p.includes(i)){m.push({data:l,i:f,stackTop:u.length-1});p.push(i);o=t.functionsDefined[i];if(!o){warn("TT: CALL non-existent function");t.hintsValid=!1;return}l=o.data;f=o.i}}}if(!g&&!w){let t=0;e<=142?t=s[e]:e>=192&&e<=223?t=-1:e>=224&&(t=-2);if(e>=113&&e<=117){n=u.pop();isNaN(n)||(t=2*-n)}for(;t<0&&u.length>0;){u.pop();t++}for(;t>0;){u.push(NaN);t--}}}t.tooComplexToFollowFunctions=d;const j=[l];f>l.length&&j.push(new Uint8Array(f-l.length));if(h>c){warn("TT: complementing a missing function tail");j.push(new Uint8Array([34,45]))}!function foldTTTable(e,t){if(t.length>1){let a,n,s=0;for(a=0,n=t.length;a>>0,r=[];for(let t=0;t>>0);const i={ttcTag:t,majorVersion:a,minorVersion:n,numFonts:s,offsetTable:r};switch(a){case 1:return i;case 2:i.dsigTag=e.getInt32()>>>0;i.dsigLength=e.getInt32()>>>0;i.dsigOffset=e.getInt32()>>>0;return i}throw new FormatError(`Invalid TrueType Collection majorVersion: ${a}.`)}(e),s=t.split("+");let r;for(let i=0;i=32))throw new FormatError('"maxp" table has a wrong version number');f=65536}!function writeUint32(e,t,a){e[t+3]=255&a;e[t+2]=a>>>8;e[t+1]=a>>>16;e[t]=a>>>24}(i.maxp.data,0,f)}let h=int16(i.head.data[50],i.head.data[51]);if(i.loca){const e=h?4*(c+1):2*(c+1);if(i.loca.length!==e){warn("Incorrect 'loca' table length -- attempting to fix it.");const a=Object.values(i).filter(Boolean).sort((e,t)=>e.offset-t.offset),n=a.indexOf(i.loca),s=a[n+1]||null;if(s&&i.loca.offset+e>8&255;o[a+1]=255&n;writeSignedInt16(o,a+2,Math.round(e[t]*signedInt16(o[a+2],o[a+3])))}}let u=c+1,m=!0;if(u>65535){m=!1;u=c;warn("Not enough space in glyfs to duplicate first glyph.")}let p=0,d=0;if(f>=65536&&i.maxp.length>=32){t.pos+=8;if(t.getUint16()>2){i.maxp.data[14]=0;i.maxp.data[15]=2}t.pos+=4;p=t.getUint16();t.pos+=4;d=t.getUint16()}i.maxp.data[4]=u>>8;i.maxp.data[5]=255&u;const g=function sanitizeTTPrograms(e,t,a,n){const s={functionsDefined:[],functionsUsed:[],functionsStackDeltas:[],tooComplexToFollowFunctions:!1,hintsValid:!0};e&&sanitizeTTProgram(e,s);t&&sanitizeTTProgram(t,s);e&&function checkInvalidFunctions(e,t){if(!e.tooComplexToFollowFunctions)if(e.functionsDefined.length>t){warn("TT: more functions defined than expected");e.hintsValid=!1}else for(let a=0,n=e.functionsUsed.length;at){warn("TT: invalid function id: "+a);e.hintsValid=!1;return}if(e.functionsUsed[a]&&!e.functionsDefined[a]){warn("TT: undefined function: "+a);e.hintsValid=!1;return}}}(s,n);if(a&&1&a.length){const e=new Uint8Array(a.length+1);e.set(a.data);a.data=e}return s.hintsValid}(i.fpgm,i.prep,i["cvt "],p);if(!g){delete i.fpgm;delete i.prep;delete i["cvt "]}!function sanitizeMetrics(e,t,a,n,s,r){if(!t){a&&(a.data=null);return}e.pos=(e.start||0)+t.offset;e.pos+=4;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;const i=e.getUint16();e.pos+=8;e.pos+=2;let o=e.getUint16();if(0!==i){if(!(2&int16(n.data[44],n.data[45]))){t.data[22]=0;t.data[23]=0}}if(o>s){info(`The numOfMetrics (${o}) should not be greater than the numGlyphs (${s}).`);o=s;t.data[34]=(65280&o)>>8;t.data[35]=255&o}const l=s-o-(a.length-4*o>>1);if(l>0){const e=new Uint8Array(a.length+2*l);e.set(a.data);if(r){e[a.length]=a.data[2];e[a.length+1]=a.data[3]}a.data=e}}(t,i.hhea,i.hmtx,i.head,u,m);if(!i.head)throw new FormatError('Required "head" table is not found');!function sanitizeHead(e,t,a){const{data:n,view:s}=e,r=s.getInt32(0);if(r>>16!=1){info("Attempting to fix invalid version in head table: "+r);s.setInt32(0,65536)}const i=signedInt16(n[50],n[51]);if(i<0||i>1){info("Attempting to fix invalid indexToLocFormat in head table: "+i);const e=t+1;if(a===e<<1){n[50]=0;n[51]=0}else{if(a!==e<<2)throw new FormatError("Could not fix indexToLocFormat: "+i);n[50]=0;n[51]=1}}}(i.head,c,o?i.loca.length:0);let b=Object.create(null);if(o){const e=function sanitizeGlyphLocations(e,t,a,n,s,r,i){let o,l,f;if(n){o=4;l=function fontItemDecodeLong(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};f=function fontItemEncodeLong(e,t,a){e[t]=a>>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}}else{o=2;l=function fontItemDecode(e,t){return e[t]<<9|e[t+1]<<1};f=function fontItemEncode(e,t,a){e[t]=a>>9&255;e[t+1]=a>>1&255}}const c=r?a+1:a,h=o*(1+c),u=new Uint8Array(h);u.set(e.data.subarray(0,h));e.data=u;const m=t.data,p=m.length,d=new Uint8Array(p);let g,b;const w=[];for(g=0,b=0;gp&&(e=p);w.push({index:g,offset:e,endOffset:0})}w.sort((e,t)=>e.offset-t.offset);for(g=0;ge.index-t.index);for(g=0;gi&&(i=e.sizeOfInstructions);y+=t;f(u,b,y)}if(0===y){const e=new Uint8Array([0,1,0,0,0,0,0,0,0,0,0,0,0,0,49,0]);for(g=0,b=o;ga+y)t.data=d.subarray(0,a+y);else{t.data=new Uint8Array(a+y);t.data.set(d.subarray(0,y))}t.data.set(d.subarray(0,a),y);f(e.data,u.length-o,y+a)}else t.data=d.subarray(0,y);return{missingGlyphs:k,maxSizeOfInstructions:i}}(i.loca,i.glyf,c,h,g,m,d);b=e.missingGlyphs;if(f>=65536&&i.maxp.length>=32){i.maxp.data[26]=e.maxSizeOfInstructions>>8;i.maxp.data[27]=255&e.maxSizeOfInstructions}}if(!i.hhea)throw new FormatError('Required "hhea" table is not found');if(0===i.hhea.data[10]&&0===i.hhea.data[11]){i.hhea.data[10]=255;i.hhea.data[11]=255}const w={unitsPerEm:int16(i.head.data[18],i.head.data[19]),yMax:signedInt16(i.head.data[42],i.head.data[43]),yMin:signedInt16(i.head.data[38],i.head.data[39]),ascent:signedInt16(i.hhea.data[4],i.hhea.data[5]),descent:signedInt16(i.hhea.data[6],i.hhea.data[7]),lineGap:signedInt16(i.hhea.data[8],i.hhea.data[9])};this.ascent=w.ascent/w.unitsPerEm;this.descent=w.descent/w.unitsPerEm;this.lineGap=w.lineGap/w.unitsPerEm;if(this.cssFontInfo?.lineHeight){this.lineHeight=this.cssFontInfo.metrics.lineHeight;this.lineGap=this.cssFontInfo.metrics.lineGap}else this.lineHeight=this.ascent-this.descent+this.lineGap;i.post&&function readPostScriptTable(e,a,n){const s=(t.start||0)+e.offset;t.pos=s;const r=s+e.length,i=t.getInt32();t.skip(28);let o,l,f=!0;switch(i){case 65536:o=Sn;break;case 131072:const e=t.getUint16();if(e!==n){f=!1;break}const s=[];for(l=0;l=32768){f=!1;break}s.push(e)}if(!f)break;const c=[];for(;t.pos65535)throw new FormatError("Max size of CID is 65,535");let s=-1;t?s=n:void 0!==e[n]&&(s=e[n]);s>=0&&s>>0;let c=!1;if(o?.platformId!==s||o?.encodingId!==r){if(0!==s||0!==r&&1!==r&&3!==r)if(1===s&&0===r)c=!0;else if(3!==s||1!==r||!n&&o){if(a&&3===s&&0===r){c=!0;let a=!0;if(e>3;e.push(n);a=Math.max(n,a)}const n=[];for(let e=0;e<=a;e++)n.push({firstCode:t.getUint16(),entryCount:t.getUint16(),idDelta:signedInt16(t.getByte(),t.getByte()),idRangePos:t.pos+t.getUint16()});for(let a=0;a<256;a++)if(0===e[a]){t.pos=n[0].idRangePos+2*a;m=t.getUint16();h.push({charCode:a,glyphId:m})}else{const s=n[e[a]];for(u=0;u>1;t.skip(6);const a=[];let n;for(n=0;n>1)-(e-n);s.offsetIndex=i;o=Math.max(o,i+s.end-s.start+1)}else s.offsetIndex=-1}const l=[];for(u=0;u>>0;for(u=0;u>>0,a=t.getInt32()>>>0;let n=t.getInt32()>>>0;for(let t=e;t<=a;t++)h.push({charCode:t,glyphId:n++})}}}h.sort((e,t)=>e.charCode-t.charCode);const p=[],d=new Set;for(const e of h){const{charCode:t}=e;if(!d.has(t)){d.add(t);p.push(e)}}return{platformId:o.platformId,encodingId:o.encodingId,mappings:p,hasShortCmap:c}}(i.cmap,t,this.isSymbolicFont,a.hasEncoding),n=e.platformId,s=e.encodingId,r=e.mappings;let o=[],l=!1;!a.hasEncoding||"MacRomanEncoding"!==a.baseEncodingName&&"WinAnsiEncoding"!==a.baseEncodingName||(o=getEncoding(a.baseEncodingName));if(a.hasEncoding&&!this.isSymbolicFont&&(3===n&&1===s||1===n&&0===s)){const e=mn();for(let t=0;t<256;t++){let i;i=void 0!==this.differences[t]?this.differences[t]:o.length&&""!==o[t]?o[t]:fn[t];if(!i)continue;const l=recoverGlyphName(i,e);let f;3===n&&1===s?f=e[l]:1===n&&0===s&&(f=ln.indexOf(l));if(void 0===f){if(!a.glyphNames&&a.hasIncludedToUnicodeMap&&!(this.toUnicode instanceof IdentityToUnicodeMap)){const e=this.toUnicode.get(t);e&&(f=e.codePointAt(0))}if(void 0===f)continue}for(const e of r)if(e.charCode===f){j[t]=e.glyphId;break}}}else if(0===n){for(const e of r)j[e.charCode]=e.glyphId;l=!0}else if(3===n&&0===s)for(const e of r){let t=e.charCode;t>=61440&&t<=61695&&(t&=255);j[t]=e.glyphId}else for(const e of r)j[e.charCode]=e.glyphId;if(a.glyphNames&&(o.length||this.differences.length))for(let e=0;e<256;++e){if(!l&&void 0!==j[e])continue;const t=this.differences[e]||o[e];if(!t)continue;const n=a.glyphNames.indexOf(t);n>0&&hasGlyph(n)&&(j[e]=n)}}0===j.length&&(j[0]=0);let k=u-1;m||(k=0);if(!a.cssFontInfo){const e=adjustMapping(j,hasGlyph,k,this.toUnicode);this.toFontChar=e.toFontChar;i.cmap={tag:"cmap",data:createCmapTable(e.charCodeToGlyphId,e.toUnicodeExtraMap,u)};i["OS/2"]&&function validateOS2Table(e,t){t.pos=(t.start||0)+e.offset;const a=t.getUint16();t.skip(60);const n=t.getUint16();if(a<4&&768&n)return!1;if(t.getUint16()>t.getUint16())return!1;t.skip(6);if(0===t.getUint16())return!1;e.data[8]=e.data[9]=0;return!0}(i["OS/2"],t)||(i["OS/2"]={tag:"OS/2",data:createOS2Table(a,e.charCodeToGlyphId,w)})}if(i.name){const[t,n]=readNameTable(i.name);i.name.data=createNameTable(e,t);this.psName=t[0][6]||null;a.composite||function adjustTrueTypeToUnicode(e,t,a){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(e.hasEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;if(!t)return;if(0===a.length)return;if(e.defaultEncoding===cn)return;for(const e of a)if(!isWinNameRecord(e))return;const n=cn,s=[],r=mn();for(const e in n){const t=n[e];if(""===t)continue;const a=r[t];void 0!==a&&(s[e]=String.fromCharCode(a))}s.length>0&&e.toUnicode.amend(s)}(a,this.isSymbolicFont,n)}else i.name={tag:"name",data:createNameTable(this.name)};const y=new OpenTypeFileBuilder(r.version);for(const e in i)y.addTable(e,i[e].data);return y.toArray()}convert(e,t,a){a.fixedPitch=!1;a.builtInEncoding&&function adjustType1ToUnicode(e,t){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(t===e.defaultEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const a=[],n=mn();for(const s in t){if(e.hasEncoding&&(e.baseEncodingName||void 0!==e.differences[s]))continue;const r=getUnicodeForGlyph(t[s],n);-1!==r&&(a[s]=String.fromCharCode(r))}a.length>0&&e.toUnicode.amend(a)}(a,a.builtInEncoding);let s=1;t instanceof CFFFont&&(s=t.numGlyphs-1);const r=t.getGlyphMapping(a);let i=null,o=r,l=null;if(!a.cssFontInfo){i=adjustMapping(r,t.hasGlyphId.bind(t),s,this.toUnicode);this.toFontChar=i.toFontChar;o=i.charCodeToGlyphId;l=i.toUnicodeExtraMap}const f=t.numGlyphs;function getCharCodes(e,t){let a=null;for(const n in e)t===e[n]&&(a||=[]).push(0|n);return a}function createCharCode(e,t){for(const a in e)if(t===e[a])return 0|a;i.charCodeToGlyphId[i.nextAvailableFontCharCode]=t;return i.nextAvailableFontCharCode++}const c=t.seacs;if(i&&c?.length){const e=a.fontMatrix||n,s=t.getCharset(),o=Object.create(null);for(let t in c){t|=0;const a=c[t],n=fn[a[2]],l=fn[a[3]],f=s.indexOf(n),h=s.indexOf(l);if(f<0||h<0)continue;const u={x:a[0]*e[0]+a[1]*e[2]+e[4],y:a[0]*e[1]+a[1]*e[3]+e[5]},m=getCharCodes(r,t);if(m)for(const e of m){const t=i.charCodeToGlyphId,a=createCharCode(t,f),n=createCharCode(t,h);o[e]={baseFontCharCode:a,accentFontCharCode:n,accentOffset:u}}}a.seacMap=o}const h=a.fontMatrix?1/Math.max(...a.fontMatrix.slice(0,4).map(Math.abs)):1e3,u=new OpenTypeFileBuilder("OTTO");u.addTable("CFF ",t.data);u.addTable("OS/2",createOS2Table(a,o));u.addTable("cmap",createCmapTable(o,l,f));u.addTable("head",function fontTableHead(){const e=[0,0,0,0,158,11,126,39],t=new TrueTypeTableBuilder({exactLength:54});t.setArray([0,1,0,0]);t.setArray([0,0,16,0]);t.skip(4);t.setArray([95,15,60,245]);t.skip(2);t.setSafeInt16(h);t.setArray(e);t.setArray(e);t.skip(2);t.setSafeInt16(a.descent);t.setArray([15,255]);t.setSafeInt16(a.ascent);t.setInt16(a.italicAngle?2:0);t.setArray([0,17]);t.skip(6);return t.data}());u.addTable("hhea",function fontTableHhea(){const e=new TrueTypeTableBuilder({exactLength:36});e.setArray([0,1,0,0]);e.setSafeInt16(a.ascent);e.setSafeInt16(a.descent);e.skip(2);e.setArray([255,255]);e.skip(6);e.setSafeInt16(a.capHeight);e.setSafeInt16(Math.tan(a.italicAngle)*a.xHeight);e.skip(12);e.setInt16(f);return e.data}());u.addTable("hmtx",function fontTableHmtx(){const e=t.charstrings,a=t.cff?.widths??null,n=new TrueTypeTableBuilder({exactLength:4*f});n.skip(4);for(let t=1,s=f;t=65520&&e<=65535?0:e>=62976&&e<=63743?dn()[e]||e:173===e?45:e}(a)}this.isType3Font&&(s=a);let c=null;if(this.seacMap?.[e]){f=!0;const t=this.seacMap[e];a=t.baseFontCharCode;c={fontChar:String.fromCodePoint(t.accentFontCharCode),offset:t.accentOffset}}let h="";"number"==typeof a&&(a<=1114111?h=String.fromCodePoint(a):warn(`charToGlyph - invalid fontCharCode: ${a}`));if(this.missingFile&&this.vertical&&1===h.length){const e=An()[h.charCodeAt(0)];e&&(h=l=String.fromCharCode(e))}r=new fonts_Glyph(e,h,l,c,n,o,s,t,f);this.#ne.set(e,r);return r}charsToGlyphs(e){let t=this.#ae.get(e);if(t)return t;t=[];if(this.cMap){const a=Object.create(null),n=e.length;let s=0;for(;st.length%2==1,n=this.toUnicode instanceof IdentityToUnicodeMap?e=>this.toUnicode.charCodeOf(e):e=>this.toUnicode.charCodeOf(String.fromCodePoint(e));for(let s=0,r=e.length;s55295&&(r<57344||r>65533)&&s++;if(this.toUnicode){const e=n(r);if(-1!==e){if(hasCurrentBufErrors()){t.push(a.join(""));a.length=0}for(let t=(this.cMap?this.cMap.getCharCodeLength(e):1)-1;t>=0;t--)a.push(String.fromCharCode(e>>8*t&255));continue}}if(!hasCurrentBufErrors()){t.push(a.join(""));a.length=0}a.push(String.fromCodePoint(r))}t.push(a.join(""));return t}}class ErrorFont{constructor(e){this.error=e;this.loadedName="g_font_error";this.missingFile=!0}charsToGlyphs(){return[]}encodeString(e){return[e]}exportData(){return{error:this.error}}}const os=[1.3877,1,1,1,.97801,.92482,.89552,.91133,.81988,.97566,.98152,.93548,.93548,1.2798,.85284,.92794,1,.96134,1.54657,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.82845,.82845,.85284,.85284,.85284,.75859,.92138,.83908,.7762,.73293,.87289,.73133,.7514,.81921,.87356,.95958,.59526,.75727,.69225,1.04924,.9121,.86943,.79795,.88198,.77958,.70864,.81055,.90399,.88653,.96017,.82577,.77892,.78257,.97507,1.54657,.97507,.85284,.89552,.90176,.88762,.8785,.75241,.8785,.90518,.95015,.77618,.8785,.88401,.91916,.86304,.88401,.91488,.8785,.8801,.8785,.8785,.91343,.7173,1.04106,.8785,.85075,.95794,.82616,.85162,.79492,.88331,1.69808,.88331,.85284,.97801,.89552,.91133,.89552,.91133,1.7801,.89552,1.24487,1.13254,1.12401,.96839,.85284,.68787,.70645,.85592,.90747,1.01466,1.0088,.90323,1,1.07463,1,.91056,.75806,1.19118,.96839,.78864,.82845,.84133,.75859,.83908,.83908,.83908,.83908,.83908,.83908,.77539,.73293,.73133,.73133,.73133,.73133,.95958,.95958,.95958,.95958,.88506,.9121,.86943,.86943,.86943,.86943,.86943,.85284,.87508,.90399,.90399,.90399,.90399,.77892,.79795,.90807,.88762,.88762,.88762,.88762,.88762,.88762,.8715,.75241,.90518,.90518,.90518,.90518,.88401,.88401,.88401,.88401,.8785,.8785,.8801,.8801,.8801,.8801,.8801,.90747,.89049,.8785,.8785,.8785,.8785,.85162,.8785,.85162,.83908,.88762,.83908,.88762,.83908,.88762,.73293,.75241,.73293,.75241,.73293,.75241,.73293,.75241,.87289,.83016,.88506,.93125,.73133,.90518,.73133,.90518,.73133,.90518,.73133,.90518,.73133,.90518,.81921,.77618,.81921,.77618,.81921,.77618,1,1,.87356,.8785,.91075,.89608,.95958,.88401,.95958,.88401,.95958,.88401,.95958,.88401,.95958,.88401,.76229,.90167,.59526,.91916,1,1,.86304,.69225,.88401,1,1,.70424,.79468,.91926,.88175,.70823,.94903,.9121,.8785,1,1,.9121,.8785,.87802,.88656,.8785,.86943,.8801,.86943,.8801,.86943,.8801,.87402,.89291,.77958,.91343,1,1,.77958,.91343,.70864,.7173,.70864,.7173,.70864,.7173,.70864,.7173,1,1,.81055,.75841,.81055,1.06452,.90399,.8785,.90399,.8785,.90399,.8785,.90399,.8785,.90399,.8785,.90399,.8785,.96017,.95794,.77892,.85162,.77892,.78257,.79492,.78257,.79492,.78257,.79492,.9297,.56892,.83908,.88762,.77539,.8715,.87508,.89049,1,1,.81055,1.04106,1.20528,1.20528,1,1.15543,.70674,.98387,.94721,1.33431,1.45894,.95161,1.06303,.83908,.80352,.57184,.6965,.56289,.82001,.56029,.81235,1.02988,.83908,.7762,.68156,.80367,.73133,.78257,.87356,.86943,.95958,.75727,.89019,1.04924,.9121,.7648,.86943,.87356,.79795,.78275,.81055,.77892,.9762,.82577,.99819,.84896,.95958,.77892,.96108,1.01407,.89049,1.02988,.94211,.96108,.8936,.84021,.87842,.96399,.79109,.89049,1.00813,1.02988,.86077,.87445,.92099,.84723,.86513,.8801,.75638,.85714,.78216,.79586,.87965,.94211,.97747,.78287,.97926,.84971,1.02988,.94211,.8801,.94211,.84971,.73133,1,1,1,1,1,1,1,1,1,1,1,1,.90264,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.90518,1,1,1,1,1,1,1,1,1,1,1,1,.90548,1,1,1,1,1,1,.96017,.95794,.96017,.95794,.96017,.95794,.77892,.85162,1,1,.89552,.90527,1,.90363,.92794,.92794,.92794,.92794,.87012,.87012,.87012,.89552,.89552,1.42259,.71143,1.06152,1,1,1.03372,1.03372,.97171,1.4956,2.2807,.93835,.83406,.91133,.84107,.91133,1,1,1,.72021,1,1.23108,.83489,.88525,.88525,.81499,.90527,1.81055,.90527,1.81055,1.31006,1.53711,.94434,1.08696,1,.95018,.77192,.85284,.90747,1.17534,.69825,.9716,1.37077,.90747,.90747,.85356,.90747,.90747,1.44947,.85284,.8941,.8941,.70572,.8,.70572,.70572,.70572,.70572,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.99862,.99862,1,1,1,1,1,1.08004,.91027,1,1,1,.99862,1,1,1,1,1,1,1,1,1,1,1,1,.90727,.90727,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],ls={lineHeight:1.2207,lineGap:.2207},fs=[1.3877,1,1,1,.97801,.92482,.89552,.91133,.81988,.97566,.98152,.93548,.93548,1.2798,.85284,.92794,1,.96134,1.56239,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.82845,.82845,.85284,.85284,.85284,.75859,.92138,.83908,.7762,.71805,.87289,.73133,.7514,.81921,.87356,.95958,.59526,.75727,.69225,1.04924,.90872,.85938,.79795,.87068,.77958,.69766,.81055,.90399,.88653,.96068,.82577,.77892,.78257,.97507,1.529,.97507,.85284,.89552,.90176,.94908,.86411,.74012,.86411,.88323,.95015,.86411,.86331,.88401,.91916,.86304,.88401,.9039,.86331,.86331,.86411,.86411,.90464,.70852,1.04106,.86331,.84372,.95794,.82616,.84548,.79492,.88331,1.69808,.88331,.85284,.97801,.89552,.91133,.89552,.91133,1.7801,.89552,1.24487,1.13254,1.19129,.96839,.85284,.68787,.70645,.85592,.90747,1.01466,1.0088,.90323,1,1.07463,1,.91056,.75806,1.19118,.96839,.78864,.82845,.84133,.75859,.83908,.83908,.83908,.83908,.83908,.83908,.77539,.71805,.73133,.73133,.73133,.73133,.95958,.95958,.95958,.95958,.88506,.90872,.85938,.85938,.85938,.85938,.85938,.85284,.87068,.90399,.90399,.90399,.90399,.77892,.79795,.90807,.94908,.94908,.94908,.94908,.94908,.94908,.85887,.74012,.88323,.88323,.88323,.88323,.88401,.88401,.88401,.88401,.8785,.86331,.86331,.86331,.86331,.86331,.86331,.90747,.89049,.86331,.86331,.86331,.86331,.84548,.86411,.84548,.83908,.94908,.83908,.94908,.83908,.94908,.71805,.74012,.71805,.74012,.71805,.74012,.71805,.74012,.87289,.79538,.88506,.92726,.73133,.88323,.73133,.88323,.73133,.88323,.73133,.88323,.73133,.88323,.81921,.86411,.81921,.86411,.81921,.86411,1,1,.87356,.86331,.91075,.8777,.95958,.88401,.95958,.88401,.95958,.88401,.95958,.88401,.95958,.88401,.76467,.90167,.59526,.91916,1,1,.86304,.69225,.88401,1,1,.70424,.77312,.91926,.88175,.70823,.94903,.90872,.86331,1,1,.90872,.86331,.86906,.88116,.86331,.85938,.86331,.85938,.86331,.85938,.86331,.87402,.86549,.77958,.90464,1,1,.77958,.90464,.69766,.70852,.69766,.70852,.69766,.70852,.69766,.70852,1,1,.81055,.75841,.81055,1.06452,.90399,.86331,.90399,.86331,.90399,.86331,.90399,.86331,.90399,.86331,.90399,.86331,.96068,.95794,.77892,.84548,.77892,.78257,.79492,.78257,.79492,.78257,.79492,.9297,.56892,.83908,.94908,.77539,.85887,.87068,.89049,1,1,.81055,1.04106,1.20528,1.20528,1,1.15543,.70088,.98387,.94721,1.33431,1.45894,.95161,1.48387,.83908,.80352,.57118,.6965,.56347,.79179,.55853,.80346,1.02988,.83908,.7762,.67174,.86036,.73133,.78257,.87356,.86441,.95958,.75727,.89019,1.04924,.90872,.74889,.85938,.87891,.79795,.7957,.81055,.77892,.97447,.82577,.97466,.87179,.95958,.77892,.94252,.95612,.8753,1.02988,.92733,.94252,.87411,.84021,.8728,.95612,.74081,.8753,1.02189,1.02988,.84814,.87445,.91822,.84723,.85668,.86331,.81344,.87581,.76422,.82046,.96057,.92733,.99375,.78022,.95452,.86015,1.02988,.92733,.86331,.92733,.86015,.73133,1,1,1,1,1,1,1,1,1,1,1,1,.90631,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.88323,1,1,1,1,1,1,1,1,1,1,1,1,.85174,1,1,1,1,1,1,.96068,.95794,.96068,.95794,.96068,.95794,.77892,.84548,1,1,.89552,.90527,1,.90363,.92794,.92794,.92794,.89807,.87012,.87012,.87012,.89552,.89552,1.42259,.71094,1.06152,1,1,1.03372,1.03372,.97171,1.4956,2.2807,.92972,.83406,.91133,.83326,.91133,1,1,1,.72021,1,1.23108,.83489,.88525,.88525,.81499,.90616,1.81055,.90527,1.81055,1.3107,1.53711,.94434,1.08696,1,.95018,.77192,.85284,.90747,1.17534,.69825,.9716,1.37077,.90747,.90747,.85356,.90747,.90747,1.44947,.85284,.8941,.8941,.70572,.8,.70572,.70572,.70572,.70572,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.99862,.99862,1,1,1,1,1,1.08004,.91027,1,1,1,.99862,1,1,1,1,1,1,1,1,1,1,1,1,.90727,.90727,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],cs={lineHeight:1.2207,lineGap:.2207},hs=[1.3877,1,1,1,1.17223,1.1293,.89552,.91133,.80395,1.02269,1.15601,.91056,.91056,1.2798,.85284,.89807,1,.90861,1.39543,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.96309,.96309,.85284,.85284,.85284,.83319,.88071,.8675,.81552,.72346,.85193,.73206,.7522,.81105,.86275,.90685,.6377,.77892,.75593,1.02638,.89249,.84118,.77452,.85374,.75186,.67789,.79776,.88844,.85066,.94309,.77818,.7306,.76659,1.10369,1.38313,1.10369,1.06139,.89552,.8739,.9245,.9245,.83203,.9245,.85865,1.09842,.9245,.9245,1.03297,1.07692,.90918,1.03297,.94959,.9245,.92274,.9245,.9245,1.02933,.77832,1.20562,.9245,.8916,.98986,.86621,.89453,.79004,.94152,1.77256,.94152,.85284,.97801,.89552,.91133,.89552,.91133,1.91729,.89552,1.17889,1.13254,1.16359,.92098,.85284,.68787,.71353,.84737,.90747,1.0088,1.0044,.87683,1,1.09091,1,.92229,.739,1.15642,.92098,.76288,.80504,.80972,.75859,.8675,.8675,.8675,.8675,.8675,.8675,.76318,.72346,.73206,.73206,.73206,.73206,.90685,.90685,.90685,.90685,.86477,.89249,.84118,.84118,.84118,.84118,.84118,.85284,.84557,.88844,.88844,.88844,.88844,.7306,.77452,.86331,.9245,.9245,.9245,.9245,.9245,.9245,.84843,.83203,.85865,.85865,.85865,.85865,.82601,.82601,.82601,.82601,.94469,.9245,.92274,.92274,.92274,.92274,.92274,.90747,.86651,.9245,.9245,.9245,.9245,.89453,.9245,.89453,.8675,.9245,.8675,.9245,.8675,.9245,.72346,.83203,.72346,.83203,.72346,.83203,.72346,.83203,.85193,.8875,.86477,.99034,.73206,.85865,.73206,.85865,.73206,.85865,.73206,.85865,.73206,.85865,.81105,.9245,.81105,.9245,.81105,.9245,1,1,.86275,.9245,.90872,.93591,.90685,.82601,.90685,.82601,.90685,.82601,.90685,1.03297,.90685,.82601,.77896,1.05611,.6377,1.07692,1,1,.90918,.75593,1.03297,1,1,.76032,.9375,.98156,.93407,.77261,1.11429,.89249,.9245,1,1,.89249,.9245,.92534,.86698,.9245,.84118,.92274,.84118,.92274,.84118,.92274,.8667,.86291,.75186,1.02933,1,1,.75186,1.02933,.67789,.77832,.67789,.77832,.67789,.77832,.67789,.77832,1,1,.79776,.97655,.79776,1.23023,.88844,.9245,.88844,.9245,.88844,.9245,.88844,.9245,.88844,.9245,.88844,.9245,.94309,.98986,.7306,.89453,.7306,.76659,.79004,.76659,.79004,.76659,.79004,1.09231,.54873,.8675,.9245,.76318,.84843,.84557,.86651,1,1,.79776,1.20562,1.18622,1.18622,1,1.1437,.67009,.96334,.93695,1.35191,1.40909,.95161,1.48387,.8675,.90861,.6192,.7363,.64824,.82411,.56321,.85696,1.23516,.8675,.81552,.7286,.84134,.73206,.76659,.86275,.84369,.90685,.77892,.85871,1.02638,.89249,.75828,.84118,.85984,.77452,.76466,.79776,.7306,.90782,.77818,.903,.87291,.90685,.7306,.99058,1.03667,.94635,1.23516,.9849,.99058,.92393,.8916,.942,1.03667,.75026,.94635,1.0297,1.23516,.90918,.94048,.98217,.89746,.84153,.92274,.82507,.88832,.84438,.88178,1.03525,.9849,1.00225,.78086,.97248,.89404,1.23516,.9849,.92274,.9849,.89404,.73206,1,1,1,1,1,1,1,1,1,1,1,1,.89693,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.85865,1,1,1,1,1,1,1,1,1,1,1,1,.90933,1,1,1,1,1,1,.94309,.98986,.94309,.98986,.94309,.98986,.7306,.89453,1,1,.89552,.90527,1,.90186,1.12308,1.12308,1.12308,1.12308,1.2566,1.2566,1.2566,.89552,.89552,1.42259,.68994,1.03809,1,1,1.0176,1.0176,1.11523,1.4956,2.01462,.97858,.82616,.91133,.83437,.91133,1,1,1,.70508,1,1.23108,.79801,.84426,.84426,.774,.90572,1.81055,.90749,1.81055,1.28809,1.55469,.94434,1.07806,1,.97094,.7589,.85284,.90747,1.19658,.69825,.97622,1.33512,.90747,.90747,.85284,.90747,.90747,1.44947,.85284,.8941,.8941,.70572,.8,.70572,.70572,.70572,.70572,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.99862,.99862,1,1,1,1,1,1.0336,.91027,1,1,1,.99862,1,1,1,1,1,1,1,1,1,1,1,1,1.05859,1.05859,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],us={lineHeight:1.2207,lineGap:.2207},ms=[1.3877,1,1,1,1.17223,1.1293,.89552,.91133,.80395,1.02269,1.15601,.91056,.91056,1.2798,.85284,.89807,1,.90861,1.39016,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.91133,.96309,.96309,.85284,.85284,.85284,.83319,.88071,.8675,.81552,.73834,.85193,.73206,.7522,.81105,.86275,.90685,.6377,.77892,.75593,1.02638,.89385,.85122,.77452,.86503,.75186,.68887,.79776,.88844,.85066,.94258,.77818,.7306,.76659,1.10369,1.39016,1.10369,1.06139,.89552,.8739,.86128,.94469,.8457,.94469,.89464,1.09842,.84636,.94469,1.03297,1.07692,.90918,1.03297,.95897,.94469,.9482,.94469,.94469,1.04692,.78223,1.20562,.94469,.90332,.98986,.86621,.90527,.79004,.94152,1.77256,.94152,.85284,.97801,.89552,.91133,.89552,.91133,1.91729,.89552,1.17889,1.13254,1.08707,.92098,.85284,.68787,.71353,.84737,.90747,1.0088,1.0044,.87683,1,1.09091,1,.92229,.739,1.15642,.92098,.76288,.80504,.80972,.75859,.8675,.8675,.8675,.8675,.8675,.8675,.76318,.73834,.73206,.73206,.73206,.73206,.90685,.90685,.90685,.90685,.86477,.89385,.85122,.85122,.85122,.85122,.85122,.85284,.85311,.88844,.88844,.88844,.88844,.7306,.77452,.86331,.86128,.86128,.86128,.86128,.86128,.86128,.8693,.8457,.89464,.89464,.89464,.89464,.82601,.82601,.82601,.82601,.94469,.94469,.9482,.9482,.9482,.9482,.9482,.90747,.86651,.94469,.94469,.94469,.94469,.90527,.94469,.90527,.8675,.86128,.8675,.86128,.8675,.86128,.73834,.8457,.73834,.8457,.73834,.8457,.73834,.8457,.85193,.92454,.86477,.9921,.73206,.89464,.73206,.89464,.73206,.89464,.73206,.89464,.73206,.89464,.81105,.84636,.81105,.84636,.81105,.84636,1,1,.86275,.94469,.90872,.95786,.90685,.82601,.90685,.82601,.90685,.82601,.90685,1.03297,.90685,.82601,.77741,1.05611,.6377,1.07692,1,1,.90918,.75593,1.03297,1,1,.76032,.90452,.98156,1.11842,.77261,1.11429,.89385,.94469,1,1,.89385,.94469,.95877,.86901,.94469,.85122,.9482,.85122,.9482,.85122,.9482,.8667,.90016,.75186,1.04692,1,1,.75186,1.04692,.68887,.78223,.68887,.78223,.68887,.78223,.68887,.78223,1,1,.79776,.92188,.79776,1.23023,.88844,.94469,.88844,.94469,.88844,.94469,.88844,.94469,.88844,.94469,.88844,.94469,.94258,.98986,.7306,.90527,.7306,.76659,.79004,.76659,.79004,.76659,.79004,1.09231,.54873,.8675,.86128,.76318,.8693,.85311,.86651,1,1,.79776,1.20562,1.18622,1.18622,1,1.1437,.67742,.96334,.93695,1.35191,1.40909,.95161,1.48387,.86686,.90861,.62267,.74359,.65649,.85498,.56963,.88254,1.23516,.8675,.81552,.75443,.84503,.73206,.76659,.86275,.85122,.90685,.77892,.85746,1.02638,.89385,.75657,.85122,.86275,.77452,.74171,.79776,.7306,.95165,.77818,.89772,.88831,.90685,.7306,.98142,1.02191,.96576,1.23516,.99018,.98142,.9236,.89258,.94035,1.02191,.78848,.96576,.9561,1.23516,.90918,.92578,.95424,.89746,.83969,.9482,.80113,.89442,.85208,.86155,.98022,.99018,1.00452,.81209,.99247,.89181,1.23516,.99018,.9482,.99018,.89181,.73206,1,1,1,1,1,1,1,1,1,1,1,1,.88844,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.89464,1,1,1,1,1,1,1,1,1,1,1,1,.96766,1,1,1,1,1,1,.94258,.98986,.94258,.98986,.94258,.98986,.7306,.90527,1,1,.89552,.90527,1,.90186,1.12308,1.12308,1.12308,1.12308,1.2566,1.2566,1.2566,.89552,.89552,1.42259,.69043,1.03809,1,1,1.0176,1.0176,1.11523,1.4956,2.01462,.99331,.82616,.91133,.84286,.91133,1,1,1,.70508,1,1.23108,.79801,.84426,.84426,.774,.90527,1.81055,.90527,1.81055,1.28809,1.55469,.94434,1.07806,1,.97094,.7589,.85284,.90747,1.19658,.69825,.97622,1.33512,.90747,.90747,.85356,.90747,.90747,1.44947,.85284,.8941,.8941,.70572,.8,.70572,.70572,.70572,.70572,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.99862,.99862,1,1,1,1,1,1.0336,.91027,1,1,1,.99862,1,1,1,1,1,1,1,1,1,1,1,1,1.05859,1.05859,1,1,1,1.07185,.99413,.96334,1.08065,1,1,1,1,1,1,1,1,1,1,1],ps={lineHeight:1.2207,lineGap:.2207},ds=[.76116,1,1,1.0006,.99998,.99974,.99973,.99973,.99982,.99977,1.00087,.99998,.99998,.99959,1.00003,1.0006,.99998,1.0006,1.0006,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99998,1,1.00003,1.00003,1.00003,1.00026,.9999,.99977,.99977,.99977,.99977,1.00001,1.00026,1.00022,.99977,1.0006,.99973,.99977,1.00026,.99999,.99977,1.00022,1.00001,1.00022,.99977,1.00001,1.00026,.99977,1.00001,1.00016,1.00001,1.00001,1.00026,.99998,1.0006,.99998,1.00003,.99973,.99998,.99973,1.00026,.99973,1.00026,.99973,.99998,1.00026,1.00026,1.0006,1.0006,.99973,1.0006,.99982,1.00026,1.00026,1.00026,1.00026,.99959,.99973,.99998,1.00026,.99973,1.00022,.99973,.99973,1,.99959,1.00077,.99959,1.00003,.99998,.99973,.99973,.99973,.99973,1.00077,.99973,.99998,1.00025,.99968,.99973,1.00003,1.00025,.60299,1.00024,1.06409,1,1,.99998,1,.99973,1.0006,.99998,1,.99936,.99973,1.00002,1.00002,1.00002,1.00026,.99977,.99977,.99977,.99977,.99977,.99977,1,.99977,1.00001,1.00001,1.00001,1.00001,1.0006,1.0006,1.0006,1.0006,.99977,.99977,1.00022,1.00022,1.00022,1.00022,1.00022,1.00003,1.00022,.99977,.99977,.99977,.99977,1.00001,1.00001,1.00026,.99973,.99973,.99973,.99973,.99973,.99973,.99982,.99973,.99973,.99973,.99973,.99973,1.0006,1.0006,1.0006,1.0006,1.00026,1.00026,1.00026,1.00026,1.00026,1.00026,1.00026,1.06409,1.00026,1.00026,1.00026,1.00026,1.00026,.99973,1.00026,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,1.03374,.99977,1.00026,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00022,1.00026,1.00022,1.00026,1.00022,1.00026,1.00022,1.00026,.99977,1.00026,.99977,1.00026,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.00042,.99973,.99973,1.0006,.99977,.99973,.99973,1.00026,1.0006,1.00026,1.0006,1.00026,1.03828,1.00026,.99999,1.00026,1.0006,.99977,1.00026,.99977,1.00026,.99977,1.00026,.9993,.9998,1.00026,1.00022,1.00026,1.00022,1.00026,1.00022,1.00026,1,1.00016,.99977,.99959,.99977,.99959,.99977,.99959,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00026,.99998,1.00026,.8121,1.00026,.99998,.99977,1.00026,.99977,1.00026,.99977,1.00026,.99977,1.00026,.99977,1.00026,.99977,1.00026,1.00016,1.00022,1.00001,.99973,1.00001,1.00026,1,1.00026,1,1.00026,1,1.0006,.99973,.99977,.99973,1,.99982,1.00022,1.00026,1.00001,.99973,1.00026,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,1.00034,.99977,1,.99997,1.00026,1.00078,1.00036,.99973,1.00013,1.0006,.99977,.99977,.99988,.85148,1.00001,1.00026,.99977,1.00022,1.0006,.99977,1.00001,.99999,.99977,1.00069,1.00022,.99977,1.00001,.99984,1.00026,1.00001,1.00024,1.00001,.9999,1,1.0006,1.00001,1.00041,.99962,1.00026,1.0006,.99995,1.00041,.99942,.99973,.99927,1.00082,.99902,1.00026,1.00087,1.0006,1.00069,.99973,.99867,.99973,.9993,1.00026,1.00049,1.00056,1,.99988,.99935,.99995,.99954,1.00055,.99945,1.00032,1.0006,.99995,1.00026,.99995,1.00032,1.00001,1.00008,.99971,1.00019,.9994,1.00001,1.0006,1.00044,.99973,1.00023,1.00047,1,.99942,.99561,.99989,1.00035,.99977,1.00035,.99977,1.00019,.99944,1.00001,1.00021,.99926,1.00035,1.00035,.99942,1.00048,.99999,.99977,1.00022,1.00035,1.00001,.99977,1.00026,.99989,1.00057,1.00001,.99936,1.00052,1.00012,.99996,1.00043,1,1.00035,.9994,.99976,1.00035,.99973,1.00052,1.00041,1.00119,1.00037,.99973,1.00002,.99986,1.00041,1.00041,.99902,.9996,1.00034,.99999,1.00026,.99999,1.00026,.99973,1.00052,.99973,1,.99973,1.00041,1.00075,.9994,1.0003,.99999,1,1.00041,.99955,1,.99915,.99973,.99973,1.00026,1.00119,.99955,.99973,1.0006,.99911,1.0006,1.00026,.99972,1.00026,.99902,1.00041,.99973,.99999,1,1,1.00038,1.0005,1.00016,1.00022,1.00016,1.00022,1.00016,1.00022,1.00001,.99973,1,1,.99973,1,1,.99955,1.0006,1.0006,1.0006,1.0006,1,1,1,.99973,.99973,.99972,1,1,1.00106,.99999,.99998,.99998,.99999,.99998,1.66475,1,.99973,.99973,1.00023,.99973,.99971,1.00047,1.00023,1,.99991,.99984,1.00002,1.00002,1.00002,1.00002,1,1,1,1,1,1,1,.99972,1,1.20985,1.39713,1.00003,1.00031,1.00015,1,.99561,1.00027,1.00031,1.00031,.99915,1.00031,1.00031,.99999,1.00003,.99999,.99999,1.41144,1.6,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.40579,1.40579,1.36625,.99999,1,.99861,.99861,1,1.00026,1.00026,1.00026,1.00026,.99972,.99999,.99999,.99999,.99999,1.40483,1,.99977,1.00054,1,1,.99953,.99962,1.00042,.9995,1,1,1,1,1,1,1,1,.99998,.99998,.99998,.99998,1,1,1,1,1,1,1,1,1,1,1],gs={lineHeight:1.2,lineGap:.2},bs=[.76116,1,1,1.0006,.99998,.99974,.99973,.99973,.99982,.99977,1.00087,.99998,.99998,.99959,1.00003,1.0006,.99998,1.0006,1.0006,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99998,1,1.00003,1.00003,1.00003,1.00026,.9999,.99977,.99977,.99977,.99977,1.00001,1.00026,1.00022,.99977,1.0006,.99973,.99977,1.00026,.99999,.99977,1.00022,1.00001,1.00022,.99977,1.00001,1.00026,.99977,1.00001,1.00016,1.00001,1.00001,1.00026,.99998,1.0006,.99998,1.00003,.99973,.99998,.99973,1.00026,.99973,1.00026,.99973,.99998,1.00026,1.00026,1.0006,1.0006,.99973,1.0006,.99982,1.00026,1.00026,1.00026,1.00026,.99959,.99973,.99998,1.00026,.99973,1.00022,.99973,.99973,1,.99959,1.00077,.99959,1.00003,.99998,.99973,.99973,.99973,.99973,1.00077,.99973,.99998,1.00025,.99968,.99973,1.00003,1.00025,.60299,1.00024,1.06409,1,1,.99998,1,.99973,1.0006,.99998,1,.99936,.99973,1.00002,1.00002,1.00002,1.00026,.99977,.99977,.99977,.99977,.99977,.99977,1,.99977,1.00001,1.00001,1.00001,1.00001,1.0006,1.0006,1.0006,1.0006,.99977,.99977,1.00022,1.00022,1.00022,1.00022,1.00022,1.00003,1.00022,.99977,.99977,.99977,.99977,1.00001,1.00001,1.00026,.99973,.99973,.99973,.99973,.99973,.99973,.99982,.99973,.99973,.99973,.99973,.99973,1.0006,1.0006,1.0006,1.0006,1.00026,1.00026,1.00026,1.00026,1.00026,1.00026,1.00026,1.06409,1.00026,1.00026,1.00026,1.00026,1.00026,.99973,1.00026,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,1.0044,.99977,1.00026,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00022,1.00026,1.00022,1.00026,1.00022,1.00026,1.00022,1.00026,.99977,1.00026,.99977,1.00026,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,.99971,.99973,.99973,1.0006,.99977,.99973,.99973,1.00026,1.0006,1.00026,1.0006,1.00026,1.01011,1.00026,.99999,1.00026,1.0006,.99977,1.00026,.99977,1.00026,.99977,1.00026,.9993,.9998,1.00026,1.00022,1.00026,1.00022,1.00026,1.00022,1.00026,1,1.00016,.99977,.99959,.99977,.99959,.99977,.99959,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00026,.99998,1.00026,.8121,1.00026,.99998,.99977,1.00026,.99977,1.00026,.99977,1.00026,.99977,1.00026,.99977,1.00026,.99977,1.00026,1.00016,1.00022,1.00001,.99973,1.00001,1.00026,1,1.00026,1,1.00026,1,1.0006,.99973,.99977,.99973,1,.99982,1.00022,1.00026,1.00001,.99973,1.00026,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99977,1,1,1.00026,.99969,.99972,.99981,.9998,1.0006,.99977,.99977,1.00022,.91155,1.00001,1.00026,.99977,1.00022,1.0006,.99977,1.00001,.99999,.99977,.99966,1.00022,1.00032,1.00001,.99944,1.00026,1.00001,.99968,1.00001,1.00047,1,1.0006,1.00001,.99981,1.00101,1.00026,1.0006,.99948,.99981,1.00064,.99973,.99942,1.00101,1.00061,1.00026,1.00069,1.0006,1.00014,.99973,1.01322,.99973,1.00065,1.00026,1.00012,.99923,1,1.00064,1.00076,.99948,1.00055,1.00063,1.00007,.99943,1.0006,.99948,1.00026,.99948,.99943,1.00001,1.00001,1.00029,1.00038,1.00035,1.00001,1.0006,1.0006,.99973,.99978,1.00001,1.00057,.99989,.99967,.99964,.99967,.99977,.99999,.99977,1.00038,.99977,1.00001,.99973,1.00066,.99967,.99967,1.00041,.99998,.99999,.99977,1.00022,.99967,1.00001,.99977,1.00026,.99964,1.00031,1.00001,.99999,.99999,1,1.00023,1,1,.99999,1.00035,1.00001,.99999,.99973,.99977,.99999,1.00058,.99973,.99973,.99955,.9995,1.00026,1.00026,1.00032,.99989,1.00034,.99999,1.00026,1.00026,1.00026,.99973,.45998,.99973,1.00026,.99973,1.00001,.99999,.99982,.99994,.99996,1,1.00042,1.00044,1.00029,1.00023,.99973,.99973,1.00026,.99949,1.00002,.99973,1.0006,1.0006,1.0006,.99975,1.00026,1.00026,1.00032,.98685,.99973,1.00026,1,1,.99966,1.00044,1.00016,1.00022,1.00016,1.00022,1.00016,1.00022,1.00001,.99973,1,1,.99973,1,1,.99955,1.0006,1.0006,1.0006,1.0006,1,1,1,.99973,.99973,.99972,1,1,1.00106,.99999,.99998,.99998,.99999,.99998,1.66475,1,.99973,.99973,1,.99973,.99971,.99978,1,1,.99991,.99984,1.00002,1.00002,1.00002,1.00002,1.00098,1,1,1,1.00049,1,1,.99972,1,1.20985,1.39713,1.00003,1.00031,1.00015,1,.99561,1.00027,1.00031,1.00031,.99915,1.00031,1.00031,.99999,1.00003,.99999,.99999,1.41144,1.6,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.40579,1.40579,1.36625,.99999,1,.99861,.99861,1,1.00026,1.00026,1.00026,1.00026,.99972,.99999,.99999,.99999,.99999,1.40483,1,.99977,1.00054,1,1,.99953,.99962,1.00042,.9995,1,1,1,1,1,1,1,1,.99998,.99998,.99998,.99998,1,1,1,1,1,1,1,1,1,1,1],ws={lineHeight:1.35,lineGap:.2},js=[.76116,1,1,1.0006,1.0006,1.00006,.99973,.99973,.99982,1.00001,1.00043,.99998,.99998,.99959,1.00003,1.0006,.99998,1.0006,1.0006,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,1.0006,1,1.00003,1.00003,1.00003,.99973,.99987,1.00001,1.00001,.99977,.99977,1.00001,1.00026,1.00022,.99977,1.0006,1,1.00001,.99973,.99999,.99977,1.00022,1.00001,1.00022,.99977,1.00001,1.00026,.99977,1.00001,1.00016,1.00001,1.00001,1.00026,1.0006,1.0006,1.0006,.99949,.99973,.99998,.99973,.99973,1,.99973,.99973,1.0006,.99973,.99973,.99924,.99924,1,.99924,.99999,.99973,.99973,.99973,.99973,.99998,1,1.0006,.99973,1,.99977,1,1,1,1.00005,1.0009,1.00005,1.00003,.99998,.99973,.99973,.99973,.99973,1.0009,.99973,.99998,1.00025,.99968,.99973,1.00003,1.00025,.60299,1.00024,1.06409,1,1,.99998,1,.9998,1.0006,.99998,1,.99936,.99973,1.00002,1.00002,1.00002,1.00026,1.00001,1.00001,1.00001,1.00001,1.00001,1.00001,1,.99977,1.00001,1.00001,1.00001,1.00001,1.0006,1.0006,1.0006,1.0006,.99977,.99977,1.00022,1.00022,1.00022,1.00022,1.00022,1.00003,1.00022,.99977,.99977,.99977,.99977,1.00001,1.00001,1.00026,.99973,.99973,.99973,.99973,.99973,.99973,.99982,1,.99973,.99973,.99973,.99973,1.0006,1.0006,1.0006,1.0006,.99973,.99973,.99973,.99973,.99973,.99973,.99973,1.06409,1.00026,.99973,.99973,.99973,.99973,1,.99973,1,1.00001,.99973,1.00001,.99973,1.00001,.99973,.99977,1,.99977,1,.99977,1,.99977,1,.99977,1.0288,.99977,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00022,.99973,1.00022,.99973,1.00022,.99973,1.00022,.99973,.99977,.99973,.99977,.99973,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,.99924,1.0006,1.0006,.99946,1.00034,1,.99924,1.00001,1,1,.99973,.99924,.99973,.99924,.99973,1.06311,.99973,1.00024,.99973,.99924,.99977,.99973,.99977,.99973,.99977,.99973,1.00041,.9998,.99973,1.00022,.99973,1.00022,.99973,1.00022,.99973,1,1.00016,.99977,.99998,.99977,.99998,.99977,.99998,1.00001,1,1.00001,1,1.00001,1,1.00001,1,1.00026,1.0006,1.00026,.89547,1.00026,1.0006,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,1.00016,.99977,1.00001,1,1.00001,1.00026,1,1.00026,1,1.00026,1,.99924,.99973,1.00001,.99973,1,.99982,1.00022,1.00026,1.00001,1,1.00026,1.0006,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,1.00001,1,1.00054,.99977,1.00084,1.00007,.99973,1.00013,.99924,1.00001,1.00001,.99945,.91221,1.00001,1.00026,.99977,1.00022,1.0006,1.00001,1.00001,.99999,.99977,.99933,1.00022,1.00054,1.00001,1.00065,1.00026,1.00001,1.0001,1.00001,1.00052,1,1.0006,1.00001,.99945,.99897,.99968,.99924,1.00036,.99945,.99949,1,1.0006,.99897,.99918,.99968,.99911,.99924,1,.99962,1.01487,1,1.0005,.99973,1.00012,1.00043,1,.99995,.99994,1.00036,.99947,1.00019,1.00063,1.00025,.99924,1.00036,.99973,1.00036,1.00025,1.00001,1.00001,1.00027,1.0001,1.00068,1.00001,1.0006,1.0006,1,1.00008,.99957,.99972,.9994,.99954,.99975,1.00051,1.00001,1.00019,1.00001,1.0001,.99986,1.00001,1.00001,1.00038,.99954,.99954,.9994,1.00066,.99999,.99977,1.00022,1.00054,1.00001,.99977,1.00026,.99975,1.0001,1.00001,.99993,.9995,.99955,1.00016,.99978,.99974,1.00019,1.00022,.99955,1.00053,.99973,1.00089,1.00005,.99967,1.00048,.99973,1.00002,1.00034,.99973,.99973,.99964,1.00006,1.00066,.99947,.99973,.98894,.99973,1,.44898,1,.99946,1,1.00039,1.00082,.99991,.99991,.99985,1.00022,1.00023,1.00061,1.00006,.99966,.99973,.99973,.99973,1.00019,1.0008,1,.99924,.99924,.99924,.99983,1.00044,.99973,.99964,.98332,1,.99973,1,1,.99962,.99895,1.00016,.99977,1.00016,.99977,1.00016,.99977,1.00001,1,1,1,.99973,1,1,.99955,.99924,.99924,.99924,.99924,.99998,.99998,.99998,.99973,.99973,.99972,1,1,1.00267,.99999,.99998,.99998,1,.99998,1.66475,1,.99973,.99973,1.00023,.99973,1.00423,.99925,.99999,1,.99991,.99984,1.00002,1.00002,1.00002,1.00002,1.00049,1,1.00245,1,1,1,1,.96329,1,1.20985,1.39713,1.00003,.8254,1.00015,1,1.00035,1.00027,1.00031,1.00031,1.00003,1.00031,1.00031,.99999,1.00003,.99999,.99999,1.41144,1.6,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.40579,1.40579,1.36625,.99999,1,.99861,.99861,1,1.00026,1.00026,1.00026,1.00026,.95317,.99999,.99999,.99999,.99999,1.40483,1,.99977,1.00054,1,1,.99953,.99962,1.00042,.9995,1,1,1,1,1,1,1,1,.99998,.99998,.99998,.99998,1,1,1,1,1,1,1,1,1,1,1],ks={lineHeight:1.35,lineGap:.2},ys=[.76116,1,1,1.0006,1.0006,1.00006,.99973,.99973,.99982,1.00001,1.00043,.99998,.99998,.99959,1.00003,1.0006,.99998,1.0006,1.0006,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,.99973,1.0006,1,1.00003,1.00003,1.00003,.99973,.99987,1.00001,1.00001,.99977,.99977,1.00001,1.00026,1.00022,.99977,1.0006,1,1.00001,.99973,.99999,.99977,1.00022,1.00001,1.00022,.99977,1.00001,1.00026,.99977,1.00001,1.00016,1.00001,1.00001,1.00026,1.0006,1.0006,1.0006,.99949,.99973,.99998,.99973,.99973,1,.99973,.99973,1.0006,.99973,.99973,.99924,.99924,1,.99924,.99999,.99973,.99973,.99973,.99973,.99998,1,1.0006,.99973,1,.99977,1,1,1,1.00005,1.0009,1.00005,1.00003,.99998,.99973,.99973,.99973,.99973,1.0009,.99973,.99998,1.00025,.99968,.99973,1.00003,1.00025,.60299,1.00024,1.06409,1,1,.99998,1,.9998,1.0006,.99998,1,.99936,.99973,1.00002,1.00002,1.00002,1.00026,1.00001,1.00001,1.00001,1.00001,1.00001,1.00001,1,.99977,1.00001,1.00001,1.00001,1.00001,1.0006,1.0006,1.0006,1.0006,.99977,.99977,1.00022,1.00022,1.00022,1.00022,1.00022,1.00003,1.00022,.99977,.99977,.99977,.99977,1.00001,1.00001,1.00026,.99973,.99973,.99973,.99973,.99973,.99973,.99982,1,.99973,.99973,.99973,.99973,1.0006,1.0006,1.0006,1.0006,.99973,.99973,.99973,.99973,.99973,.99973,.99973,1.06409,1.00026,.99973,.99973,.99973,.99973,1,.99973,1,1.00001,.99973,1.00001,.99973,1.00001,.99973,.99977,1,.99977,1,.99977,1,.99977,1,.99977,1.04596,.99977,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00001,.99973,1.00022,.99973,1.00022,.99973,1.00022,.99973,1.00022,.99973,.99977,.99973,.99977,.99973,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,1.0006,.99924,1.0006,1.0006,1.00019,1.00034,1,.99924,1.00001,1,1,.99973,.99924,.99973,.99924,.99973,1.02572,.99973,1.00005,.99973,.99924,.99977,.99973,.99977,.99973,.99977,.99973,.99999,.9998,.99973,1.00022,.99973,1.00022,.99973,1.00022,.99973,1,1.00016,.99977,.99998,.99977,.99998,.99977,.99998,1.00001,1,1.00001,1,1.00001,1,1.00001,1,1.00026,1.0006,1.00026,.84533,1.00026,1.0006,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,.99977,.99973,1.00016,.99977,1.00001,1,1.00001,1.00026,1,1.00026,1,1.00026,1,.99924,.99973,1.00001,.99973,1,.99982,1.00022,1.00026,1.00001,1,1.00026,1.0006,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99998,.99928,1,.99977,1.00013,1.00055,.99947,.99945,.99941,.99924,1.00001,1.00001,1.0004,.91621,1.00001,1.00026,.99977,1.00022,1.0006,1.00001,1.00005,.99999,.99977,1.00015,1.00022,.99977,1.00001,.99973,1.00026,1.00001,1.00019,1.00001,.99946,1,1.0006,1.00001,.99978,1.00045,.99973,.99924,1.00023,.99978,.99966,1,1.00065,1.00045,1.00019,.99973,.99973,.99924,1,1,.96499,1,1.00055,.99973,1.00008,1.00027,1,.9997,.99995,1.00023,.99933,1.00019,1.00015,1.00031,.99924,1.00023,.99973,1.00023,1.00031,1.00001,.99928,1.00029,1.00092,1.00035,1.00001,1.0006,1.0006,1,.99988,.99975,1,1.00082,.99561,.9996,1.00035,1.00001,.99962,1.00001,1.00092,.99964,1.00001,.99963,.99999,1.00035,1.00035,1.00082,.99962,.99999,.99977,1.00022,1.00035,1.00001,.99977,1.00026,.9996,.99967,1.00001,1.00034,1.00074,1.00054,1.00053,1.00063,.99971,.99962,1.00035,.99975,.99977,.99973,1.00043,.99953,1.0007,.99915,.99973,1.00008,.99892,1.00073,1.00073,1.00114,.99915,1.00073,.99955,.99973,1.00092,.99973,1,.99998,1,1.0003,1,1.00043,1.00001,.99969,1.0003,1,1.00035,1.00001,.9995,1,1.00092,.99973,.99973,.99973,1.0007,.9995,1,.99924,1.0006,.99924,.99972,1.00062,.99973,1.00114,1.00073,1,.99955,1,1,1.00047,.99968,1.00016,.99977,1.00016,.99977,1.00016,.99977,1.00001,1,1,1,.99973,1,1,.99955,.99924,.99924,.99924,.99924,.99998,.99998,.99998,.99973,.99973,.99972,1,1,1.00267,.99999,.99998,.99998,1,.99998,1.66475,1,.99973,.99973,1.00023,.99973,.99971,.99925,1.00023,1,.99991,.99984,1.00002,1.00002,1.00002,1.00002,1,1,1,1,1,1,1,.96329,1,1.20985,1.39713,1.00003,.8254,1.00015,1,1.00035,1.00027,1.00031,1.00031,.99915,1.00031,1.00031,.99999,1.00003,.99999,.99999,1.41144,1.6,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.41144,1.40579,1.40579,1.36625,.99999,1,.99861,.99861,1,1.00026,1.00026,1.00026,1.00026,.95317,.99999,.99999,.99999,.99999,1.40483,1,.99977,1.00054,1,1,.99953,.99962,1.00042,.9995,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],qs={lineHeight:1.2,lineGap:.2},vs=[365,0,333,278,333,474,556,556,889,722,238,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,333,333,584,584,584,611,975,722,722,722,722,667,611,778,722,278,556,722,611,833,722,778,667,778,722,667,611,722,667,944,667,667,611,333,278,333,584,556,333,556,611,556,611,556,333,611,611,278,278,556,278,889,611,611,611,611,389,556,333,611,556,778,556,556,500,389,280,389,584,333,556,556,556,556,280,556,333,737,370,556,584,737,552,400,549,333,333,333,576,556,278,333,333,365,556,834,834,834,611,722,722,722,722,722,722,1e3,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,556,556,556,556,556,278,278,278,278,611,611,611,611,611,611,611,549,611,611,611,611,611,556,611,556,722,556,722,556,722,556,722,556,722,556,722,556,722,556,722,719,722,611,667,556,667,556,667,556,667,556,667,556,778,611,778,611,778,611,778,611,722,611,722,611,278,278,278,278,278,278,278,278,278,278,785,556,556,278,722,556,556,611,278,611,278,611,385,611,479,611,278,722,611,722,611,722,611,708,723,611,778,611,778,611,778,611,1e3,944,722,389,722,389,722,389,667,556,667,556,667,556,667,556,611,333,611,479,611,333,722,611,722,611,722,611,722,611,722,611,722,611,944,778,667,556,667,611,500,611,500,611,500,278,556,722,556,1e3,889,778,611,667,556,611,333,333,333,333,333,333,333,333,333,333,333,465,722,333,853,906,474,825,927,838,278,722,722,601,719,667,611,722,778,278,722,667,833,722,644,778,722,667,600,611,667,821,667,809,802,278,667,615,451,611,278,582,615,610,556,606,475,460,611,541,278,558,556,612,556,445,611,766,619,520,684,446,582,715,576,753,845,278,582,611,582,845,667,669,885,567,711,667,278,276,556,1094,1062,875,610,722,622,719,722,719,722,567,712,667,904,626,719,719,610,702,833,722,778,719,667,722,611,622,854,667,730,703,1005,1019,870,979,719,711,1031,719,556,618,615,417,635,556,709,497,615,615,500,635,740,604,611,604,611,556,490,556,875,556,615,581,833,844,729,854,615,552,854,583,556,556,611,417,552,556,278,281,278,969,906,611,500,615,556,604,778,611,487,447,944,778,944,778,944,778,667,556,333,333,556,1e3,1e3,552,278,278,278,278,500,500,500,556,556,350,1e3,1e3,240,479,333,333,604,333,167,396,556,556,1094,556,885,489,1115,1e3,768,600,834,834,834,834,1e3,500,1e3,500,1e3,500,500,494,612,823,713,584,549,713,979,722,274,549,549,583,549,549,604,584,604,604,708,625,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,729,604,604,354,354,1e3,990,990,990,990,494,604,604,604,604,354,1021,1052,917,750,750,531,656,594,510,500,750,750,611,611,333,333,333,333,333,333,333,333,222,222,333,333,333,333,333,333,333,333],Ss=[-1,-1,-1,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,161,162,163,164,165,166,167,168,169,170,171,172,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,402,506,507,508,509,510,511,536,537,538,539,710,711,713,728,729,730,731,732,733,900,901,902,903,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1138,1139,1168,1169,7808,7809,7810,7811,7812,7813,7922,7923,8208,8209,8211,8212,8213,8215,8216,8217,8218,8219,8220,8221,8222,8224,8225,8226,8230,8240,8242,8243,8249,8250,8252,8254,8260,8319,8355,8356,8359,8364,8453,8467,8470,8482,8486,8494,8539,8540,8541,8542,8592,8593,8594,8595,8596,8597,8616,8706,8710,8719,8721,8722,8730,8734,8735,8745,8747,8776,8800,8801,8804,8805,8962,8976,8992,8993,9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9552,9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9600,9604,9608,9612,9616,9617,9618,9619,9632,9633,9642,9643,9644,9650,9658,9660,9668,9674,9675,9679,9688,9689,9702,9786,9787,9788,9792,9794,9824,9827,9829,9830,9834,9835,9836,61441,61442,61445,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],As=[365,0,333,278,333,474,556,556,889,722,238,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,333,333,584,584,584,611,975,722,722,722,722,667,611,778,722,278,556,722,611,833,722,778,667,778,722,667,611,722,667,944,667,667,611,333,278,333,584,556,333,556,611,556,611,556,333,611,611,278,278,556,278,889,611,611,611,611,389,556,333,611,556,778,556,556,500,389,280,389,584,333,556,556,556,556,280,556,333,737,370,556,584,737,552,400,549,333,333,333,576,556,278,333,333,365,556,834,834,834,611,722,722,722,722,722,722,1e3,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,556,556,556,556,556,278,278,278,278,611,611,611,611,611,611,611,549,611,611,611,611,611,556,611,556,722,556,722,556,722,556,722,556,722,556,722,556,722,556,722,740,722,611,667,556,667,556,667,556,667,556,667,556,778,611,778,611,778,611,778,611,722,611,722,611,278,278,278,278,278,278,278,278,278,278,782,556,556,278,722,556,556,611,278,611,278,611,396,611,479,611,278,722,611,722,611,722,611,708,723,611,778,611,778,611,778,611,1e3,944,722,389,722,389,722,389,667,556,667,556,667,556,667,556,611,333,611,479,611,333,722,611,722,611,722,611,722,611,722,611,722,611,944,778,667,556,667,611,500,611,500,611,500,278,556,722,556,1e3,889,778,611,667,556,611,333,333,333,333,333,333,333,333,333,333,333,333,722,333,854,906,473,844,930,847,278,722,722,610,671,667,611,722,778,278,722,667,833,722,657,778,718,667,590,611,667,822,667,829,781,278,667,620,479,611,278,591,620,621,556,610,479,492,611,558,278,566,556,603,556,450,611,712,605,532,664,409,591,704,578,773,834,278,591,611,591,834,667,667,886,614,719,667,278,278,556,1094,1042,854,622,719,677,719,722,708,722,614,722,667,927,643,719,719,615,687,833,722,778,719,667,722,611,677,781,667,729,708,979,989,854,1e3,708,719,1042,729,556,619,604,534,618,556,736,510,611,611,507,622,740,604,611,611,611,556,889,556,885,556,646,583,889,935,707,854,594,552,865,589,556,556,611,469,563,556,278,278,278,969,906,611,507,619,556,611,778,611,575,467,944,778,944,778,944,778,667,556,333,333,556,1e3,1e3,552,278,278,278,278,500,500,500,556,556,350,1e3,1e3,240,479,333,333,604,333,167,396,556,556,1104,556,885,516,1146,1e3,768,600,834,834,834,834,999,500,1e3,500,1e3,500,500,494,612,823,713,584,549,713,979,722,274,549,549,583,549,549,604,584,604,604,708,625,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,729,604,604,354,354,1e3,990,990,990,990,494,604,604,604,604,354,1021,1052,917,750,750,531,656,594,510,500,750,750,611,611,333,333,333,333,333,333,333,333,222,222,333,333,333,333,333,333,333,333],xs=[-1,-1,-1,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,161,162,163,164,165,166,167,168,169,170,171,172,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,402,506,507,508,509,510,511,536,537,538,539,710,711,713,728,729,730,731,732,733,900,901,902,903,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1138,1139,1168,1169,7808,7809,7810,7811,7812,7813,7922,7923,8208,8209,8211,8212,8213,8215,8216,8217,8218,8219,8220,8221,8222,8224,8225,8226,8230,8240,8242,8243,8249,8250,8252,8254,8260,8319,8355,8356,8359,8364,8453,8467,8470,8482,8486,8494,8539,8540,8541,8542,8592,8593,8594,8595,8596,8597,8616,8706,8710,8719,8721,8722,8730,8734,8735,8745,8747,8776,8800,8801,8804,8805,8962,8976,8992,8993,9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9552,9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9600,9604,9608,9612,9616,9617,9618,9619,9632,9633,9642,9643,9644,9650,9658,9660,9668,9674,9675,9679,9688,9689,9702,9786,9787,9788,9792,9794,9824,9827,9829,9830,9834,9835,9836,61441,61442,61445,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],Cs=[365,0,333,278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,1015,667,667,722,722,667,611,778,722,278,500,667,556,833,722,778,667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,333,556,556,556,556,260,556,333,737,370,556,584,737,552,400,549,333,333,333,576,537,278,333,333,365,556,834,834,834,611,667,667,667,667,667,667,1e3,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,500,556,556,556,556,278,278,278,278,556,556,556,556,556,556,556,549,611,556,556,556,556,500,556,500,667,556,667,556,667,556,722,500,722,500,722,500,722,500,722,625,722,556,667,556,667,556,667,556,667,556,667,556,778,556,778,556,778,556,778,556,722,556,722,556,278,278,278,278,278,278,278,222,278,278,733,444,500,222,667,500,500,556,222,556,222,556,281,556,400,556,222,722,556,722,556,722,556,615,723,556,778,556,778,556,778,556,1e3,944,722,333,722,333,722,333,667,500,667,500,667,500,667,500,611,278,611,354,611,278,722,556,722,556,722,556,722,556,722,556,722,556,944,722,667,500,667,611,500,611,500,611,500,222,556,667,556,1e3,889,778,611,667,500,611,278,333,333,333,333,333,333,333,333,333,333,333,667,278,789,846,389,794,865,775,222,667,667,570,671,667,611,722,778,278,667,667,833,722,648,778,725,667,600,611,667,837,667,831,761,278,667,570,439,555,222,550,570,571,500,556,439,463,555,542,222,500,492,548,500,447,556,670,573,486,603,374,550,652,546,728,779,222,550,556,550,779,667,667,843,544,708,667,278,278,500,1066,982,844,589,715,639,724,667,651,667,544,704,667,917,614,715,715,589,686,833,722,778,725,667,722,611,639,795,667,727,673,920,923,805,886,651,694,1022,682,556,562,522,493,553,556,688,465,556,556,472,564,686,550,556,556,556,500,833,500,835,500,572,518,830,851,621,736,526,492,752,534,556,556,556,378,496,500,222,222,222,910,828,556,472,565,500,556,778,556,492,339,944,722,944,722,944,722,667,500,333,333,556,1e3,1e3,552,222,222,222,222,333,333,333,556,556,350,1e3,1e3,188,354,333,333,500,333,167,365,556,556,1094,556,885,323,1083,1e3,768,600,834,834,834,834,1e3,500,998,500,1e3,500,500,494,612,823,713,584,549,713,979,719,274,549,549,584,549,549,604,584,604,604,708,625,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,729,604,604,354,354,1e3,990,990,990,990,494,604,604,604,604,354,1021,1052,917,750,750,531,656,594,510,500,750,750,500,500,333,333,333,333,333,333,333,333,222,222,294,294,324,324,316,328,398,285],Is=[-1,-1,-1,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,161,162,163,164,165,166,167,168,169,170,171,172,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,402,506,507,508,509,510,511,536,537,538,539,710,711,713,728,729,730,731,732,733,900,901,902,903,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1138,1139,1168,1169,7808,7809,7810,7811,7812,7813,7922,7923,8208,8209,8211,8212,8213,8215,8216,8217,8218,8219,8220,8221,8222,8224,8225,8226,8230,8240,8242,8243,8249,8250,8252,8254,8260,8319,8355,8356,8359,8364,8453,8467,8470,8482,8486,8494,8539,8540,8541,8542,8592,8593,8594,8595,8596,8597,8616,8706,8710,8719,8721,8722,8730,8734,8735,8745,8747,8776,8800,8801,8804,8805,8962,8976,8992,8993,9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9552,9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9600,9604,9608,9612,9616,9617,9618,9619,9632,9633,9642,9643,9644,9650,9658,9660,9668,9674,9675,9679,9688,9689,9702,9786,9787,9788,9792,9794,9824,9827,9829,9830,9834,9835,9836,61441,61442,61445,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],Fs=[365,0,333,278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,1015,667,667,722,722,667,611,778,722,278,500,667,556,833,722,778,667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,333,556,556,556,556,260,556,333,737,370,556,584,737,552,400,549,333,333,333,576,537,278,333,333,365,556,834,834,834,611,667,667,667,667,667,667,1e3,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,500,556,556,556,556,278,278,278,278,556,556,556,556,556,556,556,549,611,556,556,556,556,500,556,500,667,556,667,556,667,556,722,500,722,500,722,500,722,500,722,615,722,556,667,556,667,556,667,556,667,556,667,556,778,556,778,556,778,556,778,556,722,556,722,556,278,278,278,278,278,278,278,222,278,278,735,444,500,222,667,500,500,556,222,556,222,556,292,556,334,556,222,722,556,722,556,722,556,604,723,556,778,556,778,556,778,556,1e3,944,722,333,722,333,722,333,667,500,667,500,667,500,667,500,611,278,611,375,611,278,722,556,722,556,722,556,722,556,722,556,722,556,944,722,667,500,667,611,500,611,500,611,500,222,556,667,556,1e3,889,778,611,667,500,611,278,333,333,333,333,333,333,333,333,333,333,333,667,278,784,838,384,774,855,752,222,667,667,551,668,667,611,722,778,278,667,668,833,722,650,778,722,667,618,611,667,798,667,835,748,278,667,578,446,556,222,547,578,575,500,557,446,441,556,556,222,500,500,576,500,448,556,690,569,482,617,395,547,648,525,713,781,222,547,556,547,781,667,667,865,542,719,667,278,278,500,1057,1010,854,583,722,635,719,667,656,667,542,677,667,923,604,719,719,583,656,833,722,778,719,667,722,611,635,760,667,740,667,917,938,792,885,656,719,1010,722,556,573,531,365,583,556,669,458,559,559,438,583,688,552,556,542,556,500,458,500,823,500,573,521,802,823,625,719,521,510,750,542,556,556,556,365,510,500,222,278,222,906,812,556,438,559,500,552,778,556,489,411,944,722,944,722,944,722,667,500,333,333,556,1e3,1e3,552,222,222,222,222,333,333,333,556,556,350,1e3,1e3,188,354,333,333,500,333,167,365,556,556,1094,556,885,323,1073,1e3,768,600,834,834,834,834,1e3,500,1e3,500,1e3,500,500,494,612,823,713,584,549,713,979,719,274,549,549,583,549,549,604,584,604,604,708,625,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,708,729,604,604,354,354,1e3,990,990,990,990,494,604,604,604,604,354,1021,1052,917,750,750,531,656,594,510,500,750,750,500,500,333,333,333,333,333,333,333,333,222,222,294,294,324,324,316,328,398,285],Ts=[-1,-1,-1,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,161,162,163,164,165,166,167,168,169,170,171,172,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,402,506,507,508,509,510,511,536,537,538,539,710,711,713,728,729,730,731,732,733,900,901,902,903,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1138,1139,1168,1169,7808,7809,7810,7811,7812,7813,7922,7923,8208,8209,8211,8212,8213,8215,8216,8217,8218,8219,8220,8221,8222,8224,8225,8226,8230,8240,8242,8243,8249,8250,8252,8254,8260,8319,8355,8356,8359,8364,8453,8467,8470,8482,8486,8494,8539,8540,8541,8542,8592,8593,8594,8595,8596,8597,8616,8706,8710,8719,8721,8722,8730,8734,8735,8745,8747,8776,8800,8801,8804,8805,8962,8976,8992,8993,9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9552,9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9600,9604,9608,9612,9616,9617,9618,9619,9632,9633,9642,9643,9644,9650,9658,9660,9668,9674,9675,9679,9688,9689,9702,9786,9787,9788,9792,9794,9824,9827,9829,9830,9834,9835,9836,61441,61442,61445,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],Os=[1.36898,1,1,.72706,.80479,.83734,.98894,.99793,.9897,.93884,.86209,.94292,.94292,1.16661,1.02058,.93582,.96694,.93582,1.19137,.99793,.99793,.99793,.99793,.99793,.99793,.99793,.99793,.99793,.99793,.78076,.78076,1.02058,1.02058,1.02058,.72851,.78966,.90838,.83637,.82391,.96376,.80061,.86275,.8768,.95407,1.0258,.73901,.85022,.83655,1.0156,.95546,.92179,.87107,.92179,.82114,.8096,.89713,.94438,.95353,.94083,.91905,.90406,.9446,.94292,1.18777,.94292,1.02058,.89903,.90088,.94938,.97898,.81093,.97571,.94938,1.024,.9577,.95933,.98621,1.0474,.97455,.98981,.9672,.95933,.9446,.97898,.97407,.97646,.78036,1.10208,.95442,.95298,.97579,.9332,.94039,.938,.80687,1.01149,.80687,1.02058,.80479,.99793,.99793,.99793,.99793,1.01149,1.00872,.90088,.91882,1.0213,.8361,1.02058,.62295,.54324,.89022,1.08595,1,1,.90088,1,.97455,.93582,.90088,1,1.05686,.8361,.99642,.99642,.99642,.72851,.90838,.90838,.90838,.90838,.90838,.90838,.868,.82391,.80061,.80061,.80061,.80061,1.0258,1.0258,1.0258,1.0258,.97484,.95546,.92179,.92179,.92179,.92179,.92179,1.02058,.92179,.94438,.94438,.94438,.94438,.90406,.86958,.98225,.94938,.94938,.94938,.94938,.94938,.94938,.9031,.81093,.94938,.94938,.94938,.94938,.98621,.98621,.98621,.98621,.93969,.95933,.9446,.9446,.9446,.9446,.9446,1.08595,.9446,.95442,.95442,.95442,.95442,.94039,.97898,.94039,.90838,.94938,.90838,.94938,.90838,.94938,.82391,.81093,.82391,.81093,.82391,.81093,.82391,.81093,.96376,.84313,.97484,.97571,.80061,.94938,.80061,.94938,.80061,.94938,.80061,.94938,.80061,.94938,.8768,.9577,.8768,.9577,.8768,.9577,1,1,.95407,.95933,.97069,.95933,1.0258,.98621,1.0258,.98621,1.0258,.98621,1.0258,.98621,1.0258,.98621,.887,1.01591,.73901,1.0474,1,1,.97455,.83655,.98981,1,1,.83655,.73977,.83655,.73903,.84638,1.033,.95546,.95933,1,1,.95546,.95933,.8271,.95417,.95933,.92179,.9446,.92179,.9446,.92179,.9446,.936,.91964,.82114,.97646,1,1,.82114,.97646,.8096,.78036,.8096,.78036,1,1,.8096,.78036,1,1,.89713,.77452,.89713,1.10208,.94438,.95442,.94438,.95442,.94438,.95442,.94438,.95442,.94438,.95442,.94438,.95442,.94083,.97579,.90406,.94039,.90406,.9446,.938,.9446,.938,.9446,.938,1,.99793,.90838,.94938,.868,.9031,.92179,.9446,1,1,.89713,1.10208,.90088,.90088,.90088,.90088,.90088,.90088,.90088,.90088,.90088,.90989,.9358,.91945,.83181,.75261,.87992,.82976,.96034,.83689,.97268,1.0078,.90838,.83637,.8019,.90157,.80061,.9446,.95407,.92436,1.0258,.85022,.97153,1.0156,.95546,.89192,.92179,.92361,.87107,.96318,.89713,.93704,.95638,.91905,.91709,.92796,1.0258,.93704,.94836,1.0373,.95933,1.0078,.95871,.94836,.96174,.92601,.9498,.98607,.95776,.95933,1.05453,1.0078,.98275,.9314,.95617,.91701,1.05993,.9446,.78367,.9553,1,.86832,1.0128,.95871,.99394,.87548,.96361,.86774,1.0078,.95871,.9446,.95871,.86774,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.94083,.97579,.94083,.97579,.94083,.97579,.90406,.94039,.96694,1,.89903,1,1,1,.93582,.93582,.93582,1,.908,.908,.918,.94219,.94219,.96544,1,1.285,1,1,.81079,.81079,1,1,.74854,1,1,1,1,.99793,1,1,1,.65,1,1.36145,1,1,1,1,1,1,1,1,1,1,1,1.17173,1,.80535,.76169,1.02058,1.0732,1.05486,1,1,1.30692,1.08595,1.08595,1,1.08595,1.08595,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.16161,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],Rs={lineHeight:1.2,lineGap:.2},Hs=[1.36898,1,1,.66227,.80779,.81625,.97276,.97276,.97733,.92222,.83266,.94292,.94292,1.16148,1.02058,.93582,.96694,.93582,1.17337,.97276,.97276,.97276,.97276,.97276,.97276,.97276,.97276,.97276,.97276,.78076,.78076,1.02058,1.02058,1.02058,.71541,.76813,.85576,.80591,.80729,.94299,.77512,.83655,.86523,.92222,.98621,.71743,.81698,.79726,.98558,.92222,.90637,.83809,.90637,.80729,.76463,.86275,.90699,.91605,.9154,.85308,.85458,.90531,.94292,1.21296,.94292,1.02058,.89903,1.18616,.99613,.91677,.78216,.91677,.90083,.98796,.9135,.92168,.95381,.98981,.95298,.95381,.93459,.92168,.91513,.92004,.91677,.95077,.748,1.04502,.91677,.92061,.94236,.89544,.89364,.9,.80687,.8578,.80687,1.02058,.80779,.97276,.97276,.97276,.97276,.8578,.99973,1.18616,.91339,1.08074,.82891,1.02058,.55509,.71526,.89022,1.08595,1,1,1.18616,1,.96736,.93582,1.18616,1,1.04864,.82711,.99043,.99043,.99043,.71541,.85576,.85576,.85576,.85576,.85576,.85576,.845,.80729,.77512,.77512,.77512,.77512,.98621,.98621,.98621,.98621,.95961,.92222,.90637,.90637,.90637,.90637,.90637,1.02058,.90251,.90699,.90699,.90699,.90699,.85458,.83659,.94951,.99613,.99613,.99613,.99613,.99613,.99613,.85811,.78216,.90083,.90083,.90083,.90083,.95381,.95381,.95381,.95381,.9135,.92168,.91513,.91513,.91513,.91513,.91513,1.08595,.91677,.91677,.91677,.91677,.91677,.89364,.92332,.89364,.85576,.99613,.85576,.99613,.85576,.99613,.80729,.78216,.80729,.78216,.80729,.78216,.80729,.78216,.94299,.76783,.95961,.91677,.77512,.90083,.77512,.90083,.77512,.90083,.77512,.90083,.77512,.90083,.86523,.9135,.86523,.9135,.86523,.9135,1,1,.92222,.92168,.92222,.92168,.98621,.95381,.98621,.95381,.98621,.95381,.98621,.95381,.98621,.95381,.86036,.97096,.71743,.98981,1,1,.95298,.79726,.95381,1,1,.79726,.6894,.79726,.74321,.81691,1.0006,.92222,.92168,1,1,.92222,.92168,.79464,.92098,.92168,.90637,.91513,.90637,.91513,.90637,.91513,.909,.87514,.80729,.95077,1,1,.80729,.95077,.76463,.748,.76463,.748,1,1,.76463,.748,1,1,.86275,.72651,.86275,1.04502,.90699,.91677,.90699,.91677,.90699,.91677,.90699,.91677,.90699,.91677,.90699,.91677,.9154,.94236,.85458,.89364,.85458,.90531,.9,.90531,.9,.90531,.9,1,.97276,.85576,.99613,.845,.85811,.90251,.91677,1,1,.86275,1.04502,1.18616,1.18616,1.18616,1.18616,1.18616,1.18616,1.18616,1.18616,1.18616,1.00899,1.30628,.85576,.80178,.66862,.7927,.69323,.88127,.72459,.89711,.95381,.85576,.80591,.7805,.94729,.77512,.90531,.92222,.90637,.98621,.81698,.92655,.98558,.92222,.85359,.90637,.90976,.83809,.94523,.86275,.83509,.93157,.85308,.83392,.92346,.98621,.83509,.92886,.91324,.92168,.95381,.90646,.92886,.90557,.86847,.90276,.91324,.86842,.92168,.99531,.95381,.9224,.85408,.92699,.86847,1.0051,.91513,.80487,.93481,1,.88159,1.05214,.90646,.97355,.81539,.89398,.85923,.95381,.90646,.91513,.90646,.85923,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.9154,.94236,.9154,.94236,.9154,.94236,.85458,.89364,.96694,1,.89903,1,1,1,.91782,.91782,.91782,1,.896,.896,.896,.9332,.9332,.95973,1,1.26,1,1,.80479,.80178,1,1,.85633,1,1,1,1,.97276,1,1,1,.698,1,1.36145,1,1,1,1,1,1,1,1,1,1,1,1.14542,1,.79199,.78694,1.02058,1.03493,1.05486,1,1,1.23026,1.08595,1.08595,1,1.08595,1.08595,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.20006,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],Bs={lineHeight:1.2,lineGap:.2},Ds=[1.36898,1,1,.65507,.84943,.85639,.88465,.88465,.86936,.88307,.86948,.85283,.85283,1.06383,1.02058,.75945,.9219,.75945,1.17337,.88465,.88465,.88465,.88465,.88465,.88465,.88465,.88465,.88465,.88465,.75945,.75945,1.02058,1.02058,1.02058,.69046,.70926,.85158,.77812,.76852,.89591,.70466,.76125,.80094,.86822,.83864,.728,.77212,.79475,.93637,.87514,.8588,.76013,.8588,.72421,.69866,.77598,.85991,.80811,.87832,.78112,.77512,.8562,1.0222,1.18417,1.0222,1.27014,.89903,1.15012,.93859,.94399,.846,.94399,.81453,1.0186,.94219,.96017,1.03075,1.02175,.912,1.03075,.96998,.96017,.93859,.94399,.94399,.95493,.746,1.12658,.94578,.91,.979,.882,.882,.83,.85034,.83537,.85034,1.02058,.70869,.88465,.88465,.88465,.88465,.83537,.90083,1.15012,.9161,.94565,.73541,1.02058,.53609,.69353,.79519,1.08595,1,1,1.15012,1,.91974,.75945,1.15012,1,.9446,.73361,.9005,.9005,.9005,.62864,.85158,.85158,.85158,.85158,.85158,.85158,.773,.76852,.70466,.70466,.70466,.70466,.83864,.83864,.83864,.83864,.90561,.87514,.8588,.8588,.8588,.8588,.8588,1.02058,.85751,.85991,.85991,.85991,.85991,.77512,.76013,.88075,.93859,.93859,.93859,.93859,.93859,.93859,.8075,.846,.81453,.81453,.81453,.81453,.82424,.82424,.82424,.82424,.9278,.96017,.93859,.93859,.93859,.93859,.93859,1.08595,.8562,.94578,.94578,.94578,.94578,.882,.94578,.882,.85158,.93859,.85158,.93859,.85158,.93859,.76852,.846,.76852,.846,.76852,.846,.76852,.846,.89591,.8544,.90561,.94399,.70466,.81453,.70466,.81453,.70466,.81453,.70466,.81453,.70466,.81453,.80094,.94219,.80094,.94219,.80094,.94219,1,1,.86822,.96017,.86822,.96017,.83864,.82424,.83864,.82424,.83864,.82424,.83864,1.03075,.83864,.82424,.81402,1.02738,.728,1.02175,1,1,.912,.79475,1.03075,1,1,.79475,.83911,.79475,.66266,.80553,1.06676,.87514,.96017,1,1,.87514,.96017,.86865,.87396,.96017,.8588,.93859,.8588,.93859,.8588,.93859,.867,.84759,.72421,.95493,1,1,.72421,.95493,.69866,.746,.69866,.746,1,1,.69866,.746,1,1,.77598,.88417,.77598,1.12658,.85991,.94578,.85991,.94578,.85991,.94578,.85991,.94578,.85991,.94578,.85991,.94578,.87832,.979,.77512,.882,.77512,.8562,.83,.8562,.83,.8562,.83,1,.88465,.85158,.93859,.773,.8075,.85751,.8562,1,1,.77598,1.12658,1.15012,1.15012,1.15012,1.15012,1.15012,1.15313,1.15012,1.15012,1.15012,1.08106,1.03901,.85158,.77025,.62264,.7646,.65351,.86026,.69461,.89947,1.03075,.85158,.77812,.76449,.88836,.70466,.8562,.86822,.8588,.83864,.77212,.85308,.93637,.87514,.82352,.8588,.85701,.76013,.89058,.77598,.8156,.82565,.78112,.77899,.89386,.83864,.8156,.9486,.92388,.96186,1.03075,.91123,.9486,.93298,.878,.93942,.92388,.84596,.96186,.95119,1.03075,.922,.88787,.95829,.88,.93559,.93859,.78815,.93758,1,.89217,1.03737,.91123,.93969,.77487,.85769,.86799,1.03075,.91123,.93859,.91123,.86799,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.87832,.979,.87832,.979,.87832,.979,.77512,.882,.9219,1,.89903,1,1,1,.87321,.87321,.87321,1,1.027,1.027,1.027,.86847,.86847,.79121,1,1.124,1,1,.73572,.73572,1,1,.85034,1,1,1,1,.88465,1,1,1,.669,1,1.36145,1,1,1,1,1,1,1,1,1,1,1,1.04828,1,.74948,.75187,1.02058,.98391,1.02119,1,1,1.06233,1.08595,1.08595,1,1.08595,1.08595,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.05233,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],Ms={lineHeight:1.2,lineGap:.2},Ns=[1.36898,1,1,.76305,.82784,.94935,.89364,.92241,.89073,.90706,.98472,.85283,.85283,1.0664,1.02058,.74505,.9219,.74505,1.23456,.92241,.92241,.92241,.92241,.92241,.92241,.92241,.92241,.92241,.92241,.74505,.74505,1.02058,1.02058,1.02058,.73002,.72601,.91755,.8126,.80314,.92222,.73764,.79726,.83051,.90284,.86023,.74,.8126,.84869,.96518,.91115,.8858,.79761,.8858,.74498,.73914,.81363,.89591,.83659,.89633,.85608,.8111,.90531,1.0222,1.22736,1.0222,1.27014,.89903,.90088,.86667,1.0231,.896,1.01411,.90083,1.05099,1.00512,.99793,1.05326,1.09377,.938,1.06226,1.00119,.99793,.98714,1.0231,1.01231,.98196,.792,1.19137,.99074,.962,1.01915,.926,.942,.856,.85034,.92006,.85034,1.02058,.69067,.92241,.92241,.92241,.92241,.92006,.9332,.90088,.91882,.93484,.75339,1.02058,.56866,.54324,.79519,1.08595,1,1,.90088,1,.95325,.74505,.90088,1,.97198,.75339,.91009,.91009,.91009,.66466,.91755,.91755,.91755,.91755,.91755,.91755,.788,.80314,.73764,.73764,.73764,.73764,.86023,.86023,.86023,.86023,.92915,.91115,.8858,.8858,.8858,.8858,.8858,1.02058,.8858,.89591,.89591,.89591,.89591,.8111,.79611,.89713,.86667,.86667,.86667,.86667,.86667,.86667,.86936,.896,.90083,.90083,.90083,.90083,.84224,.84224,.84224,.84224,.97276,.99793,.98714,.98714,.98714,.98714,.98714,1.08595,.89876,.99074,.99074,.99074,.99074,.942,1.0231,.942,.91755,.86667,.91755,.86667,.91755,.86667,.80314,.896,.80314,.896,.80314,.896,.80314,.896,.92222,.93372,.92915,1.01411,.73764,.90083,.73764,.90083,.73764,.90083,.73764,.90083,.73764,.90083,.83051,1.00512,.83051,1.00512,.83051,1.00512,1,1,.90284,.99793,.90976,.99793,.86023,.84224,.86023,.84224,.86023,.84224,.86023,1.05326,.86023,.84224,.82873,1.07469,.74,1.09377,1,1,.938,.84869,1.06226,1,1,.84869,.83704,.84869,.81441,.85588,1.08927,.91115,.99793,1,1,.91115,.99793,.91887,.90991,.99793,.8858,.98714,.8858,.98714,.8858,.98714,.894,.91434,.74498,.98196,1,1,.74498,.98196,.73914,.792,.73914,.792,1,1,.73914,.792,1,1,.81363,.904,.81363,1.19137,.89591,.99074,.89591,.99074,.89591,.99074,.89591,.99074,.89591,.99074,.89591,.99074,.89633,1.01915,.8111,.942,.8111,.90531,.856,.90531,.856,.90531,.856,1,.92241,.91755,.86667,.788,.86936,.8858,.89876,1,1,.81363,1.19137,.90088,.90088,.90088,.90088,.90088,.90088,.90088,.90088,.90088,.90388,1.03901,.92138,.78105,.7154,.86169,.80513,.94007,.82528,.98612,1.06226,.91755,.8126,.81884,.92819,.73764,.90531,.90284,.8858,.86023,.8126,.91172,.96518,.91115,.83089,.8858,.87791,.79761,.89297,.81363,.88157,.89992,.85608,.81992,.94307,.86023,.88157,.95308,.98699,.99793,1.06226,.95817,.95308,.97358,.928,.98088,.98699,.92761,.99793,.96017,1.06226,.986,.944,.95978,.938,.96705,.98714,.80442,.98972,1,.89762,1.04552,.95817,.99007,.87064,.91879,.88888,1.06226,.95817,.98714,.95817,.88888,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.89633,1.01915,.89633,1.01915,.89633,1.01915,.8111,.942,.9219,1,.89903,1,1,1,.93173,.93173,.93173,1,1.06304,1.06304,1.06904,.89903,.89903,.80549,1,1.156,1,1,.76575,.76575,1,1,.72458,1,1,1,1,.92241,1,1,1,.619,1,1.36145,1,1,1,1,1,1,1,1,1,1,1,1.07257,1,.74705,.71119,1.02058,1.024,1.02119,1,1,1.1536,1.08595,1.08595,1,1.08595,1.08595,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.05638,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],Es={lineHeight:1.2,lineGap:.2},_s=[1.76738,1,1,.99297,.9824,1.04016,1.06497,1.03424,.97529,1.17647,1.23203,1.1085,1.1085,1.16939,1.2107,.9754,1.21408,.9754,1.59578,1.03424,1.03424,1.03424,1.03424,1.03424,1.03424,1.03424,1.03424,1.03424,1.03424,.81378,.81378,1.2107,1.2107,1.2107,.71703,.97847,.97363,.88776,.8641,1.02096,.79795,.85132,.914,1.06085,1.1406,.8007,.89858,.83693,1.14889,1.09398,.97489,.92094,.97489,.90399,.84041,.95923,1.00135,1,1.06467,.98243,.90996,.99361,1.1085,1.56942,1.1085,1.2107,.74627,.94282,.96752,1.01519,.86304,1.01359,.97278,1.15103,1.01359,.98561,1.02285,1.02285,1.00527,1.02285,1.0302,.99041,1.0008,1.01519,1.01359,1.02258,.79104,1.16862,.99041,.97454,1.02511,.99298,.96752,.95801,.94856,1.16579,.94856,1.2107,.9824,1.03424,1.03424,1,1.03424,1.16579,.8727,1.3871,1.18622,1.10818,1.04478,1.2107,1.18622,.75155,.94994,1.28826,1.21408,1.21408,.91056,1,.91572,.9754,.64663,1.18328,1.24866,1.04478,1.14169,1.15749,1.17389,.71703,.97363,.97363,.97363,.97363,.97363,.97363,.93506,.8641,.79795,.79795,.79795,.79795,1.1406,1.1406,1.1406,1.1406,1.02096,1.09398,.97426,.97426,.97426,.97426,.97426,1.2107,.97489,1.00135,1.00135,1.00135,1.00135,.90996,.92094,1.02798,.96752,.96752,.96752,.96752,.96752,.96752,.93136,.86304,.97278,.97278,.97278,.97278,1.02285,1.02285,1.02285,1.02285,.97122,.99041,1,1,1,1,1,1.28826,1.0008,.99041,.99041,.99041,.99041,.96752,1.01519,.96752,.97363,.96752,.97363,.96752,.97363,.96752,.8641,.86304,.8641,.86304,.8641,.86304,.8641,.86304,1.02096,1.03057,1.02096,1.03517,.79795,.97278,.79795,.97278,.79795,.97278,.79795,.97278,.79795,.97278,.914,1.01359,.914,1.01359,.914,1.01359,1,1,1.06085,.98561,1.06085,1.00879,1.1406,1.02285,1.1406,1.02285,1.1406,1.02285,1.1406,1.02285,1.1406,1.02285,.97138,1.08692,.8007,1.02285,1,1,1.00527,.83693,1.02285,1,1,.83693,.9455,.83693,.90418,.83693,1.13005,1.09398,.99041,1,1,1.09398,.99041,.96692,1.09251,.99041,.97489,1.0008,.97489,1.0008,.97489,1.0008,.93994,.97931,.90399,1.02258,1,1,.90399,1.02258,.84041,.79104,.84041,.79104,.84041,.79104,.84041,.79104,1,1,.95923,1.07034,.95923,1.16862,1.00135,.99041,1.00135,.99041,1.00135,.99041,1.00135,.99041,1.00135,.99041,1.00135,.99041,1.06467,1.02511,.90996,.96752,.90996,.99361,.95801,.99361,.95801,.99361,.95801,1.07733,1.03424,.97363,.96752,.93506,.93136,.97489,1.0008,1,1,.95923,1.16862,1.15103,1.15103,1.01173,1.03959,.75953,.81378,.79912,1.15103,1.21994,.95161,.87815,1.01149,.81525,.7676,.98167,1.01134,1.02546,.84097,1.03089,1.18102,.97363,.88776,.85134,.97826,.79795,.99361,1.06085,.97489,1.1406,.89858,1.0388,1.14889,1.09398,.86039,.97489,1.0595,.92094,.94793,.95923,.90996,.99346,.98243,1.02112,.95493,1.1406,.90996,1.03574,1.02597,1.0008,1.18102,1.06628,1.03574,1.0192,1.01932,1.00886,.97531,1.0106,1.0008,1.13189,1.18102,1.02277,.98683,1.0016,.99561,1.07237,1.0008,.90434,.99921,.93803,.8965,1.23085,1.06628,1.04983,.96268,1.0499,.98439,1.18102,1.06628,1.0008,1.06628,.98439,.79795,1,1,1,1,1,1,1,1,1,1,1,1,1.09466,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.97278,1,1,1,1,1,1,1,1,1,1,1,1,1.02065,1,1,1,1,1,1,1.06467,1.02511,1.06467,1.02511,1.06467,1.02511,.90996,.96752,1,1.21408,.89903,1,1,.75155,1.04394,1.04394,1.04394,1.04394,.98633,.98633,.98633,.73047,.73047,1.20642,.91211,1.25635,1.222,1.02956,1.03372,1.03372,.96039,1.24633,1,1.12454,.93503,1.03424,1.19687,1.03424,1,1,1,.771,1,1,1.15749,1.15749,1.15749,1.10948,.86279,.94434,.86279,.94434,.86182,1,1,1.16897,1,.96085,.90137,1.2107,1.18416,1.13973,.69825,.9716,2.10339,1.29004,1.29004,1.21172,1.29004,1.29004,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.42603,1,.99862,.99862,1,.87025,.87025,.87025,.87025,1.18874,1.42603,1,1.42603,1.42603,.99862,1,1,1,1,1,1.2886,1.04315,1.15296,1.34163,1,1,1,1.09193,1.09193,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],zs={lineHeight:1.33008,lineGap:0},Ls=[1.76738,1,1,.98946,1.03959,1.04016,1.02809,1.036,.97639,1.10953,1.23203,1.11144,1.11144,1.16939,1.21237,.9754,1.21261,.9754,1.59754,1.036,1.036,1.036,1.036,1.036,1.036,1.036,1.036,1.036,1.036,.81378,.81378,1.21237,1.21237,1.21237,.73541,.97847,.97363,.89723,.87897,1.0426,.79429,.85292,.91149,1.05815,1.1406,.79631,.90128,.83853,1.04396,1.10615,.97552,.94436,.97552,.88641,.80527,.96083,1.00135,1,1.06777,.9817,.91142,.99361,1.11144,1.57293,1.11144,1.21237,.74627,1.31818,1.06585,.97042,.83055,.97042,.93503,1.1261,.97042,.97922,1.14236,.94552,1.01054,1.14236,1.02471,.97922,.94165,.97042,.97042,1.0276,.78929,1.1261,.97922,.95874,1.02197,.98507,.96752,.97168,.95107,1.16579,.95107,1.21237,1.03959,1.036,1.036,1,1.036,1.16579,.87357,1.31818,1.18754,1.26781,1.05356,1.21237,1.18622,.79487,.94994,1.29004,1.24047,1.24047,1.31818,1,.91484,.9754,1.31818,1.1349,1.24866,1.05356,1.13934,1.15574,1.17389,.73541,.97363,.97363,.97363,.97363,.97363,.97363,.94385,.87897,.79429,.79429,.79429,.79429,1.1406,1.1406,1.1406,1.1406,1.0426,1.10615,.97552,.97552,.97552,.97552,.97552,1.21237,.97552,1.00135,1.00135,1.00135,1.00135,.91142,.94436,.98721,1.06585,1.06585,1.06585,1.06585,1.06585,1.06585,.96705,.83055,.93503,.93503,.93503,.93503,1.14236,1.14236,1.14236,1.14236,.93125,.97922,.94165,.94165,.94165,.94165,.94165,1.29004,.94165,.97922,.97922,.97922,.97922,.96752,.97042,.96752,.97363,1.06585,.97363,1.06585,.97363,1.06585,.87897,.83055,.87897,.83055,.87897,.83055,.87897,.83055,1.0426,1.0033,1.0426,.97042,.79429,.93503,.79429,.93503,.79429,.93503,.79429,.93503,.79429,.93503,.91149,.97042,.91149,.97042,.91149,.97042,1,1,1.05815,.97922,1.05815,.97922,1.1406,1.14236,1.1406,1.14236,1.1406,1.14236,1.1406,1.14236,1.1406,1.14236,.97441,1.04302,.79631,1.01582,1,1,1.01054,.83853,1.14236,1,1,.83853,1.09125,.83853,.90418,.83853,1.19508,1.10615,.97922,1,1,1.10615,.97922,1.01034,1.10466,.97922,.97552,.94165,.97552,.94165,.97552,.94165,.91602,.91981,.88641,1.0276,1,1,.88641,1.0276,.80527,.78929,.80527,.78929,.80527,.78929,.80527,.78929,1,1,.96083,1.05403,.95923,1.16862,1.00135,.97922,1.00135,.97922,1.00135,.97922,1.00135,.97922,1.00135,.97922,1.00135,.97922,1.06777,1.02197,.91142,.96752,.91142,.99361,.97168,.99361,.97168,.99361,.97168,1.23199,1.036,.97363,1.06585,.94385,.96705,.97552,.94165,1,1,.96083,1.1261,1.31818,1.31818,1.31818,1.31818,1.31818,1.31818,1.31818,1.31818,1.31818,.95161,1.27126,1.00811,.83284,.77702,.99137,.95253,1.0347,.86142,1.07205,1.14236,.97363,.89723,.86869,1.09818,.79429,.99361,1.05815,.97552,1.1406,.90128,1.06662,1.04396,1.10615,.84918,.97552,1.04694,.94436,.98015,.96083,.91142,1.00356,.9817,1.01945,.98999,1.1406,.91142,1.04961,.9898,1.00639,1.14236,1.07514,1.04961,.99607,1.02897,1.008,.9898,.95134,1.00639,1.11121,1.14236,1.00518,.97981,1.02186,1,1.08578,.94165,.99314,.98387,.93028,.93377,1.35125,1.07514,1.10687,.93491,1.04232,1.00351,1.14236,1.07514,.94165,1.07514,1.00351,.79429,1,1,1,1,1,1,1,1,1,1,1,1,1.09097,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.93503,1,1,1,1,1,1,1,1,1,1,1,1,.96609,1,1,1,1,1,1,1.06777,1.02197,1.06777,1.02197,1.06777,1.02197,.91142,.96752,1,1.21261,.89903,1,1,.75155,1.04745,1.04745,1.04745,1.04394,.98633,.98633,.98633,.72959,.72959,1.20502,.91406,1.26514,1.222,1.02956,1.03372,1.03372,.96039,1.24633,1,1.09125,.93327,1.03336,1.16541,1.036,1,1,1,.771,1,1,1.15574,1.15574,1.15574,1.15574,.86364,.94434,.86279,.94434,.86224,1,1,1.16798,1,.96085,.90068,1.21237,1.18416,1.13904,.69825,.9716,2.10339,1.29004,1.29004,1.21339,1.29004,1.29004,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.42603,1,.99862,.99862,1,.87025,.87025,.87025,.87025,1.18775,1.42603,1,1.42603,1.42603,.99862,1,1,1,1,1,1.2886,1.04315,1.15296,1.34163,1,1,1,1.13269,1.13269,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],Us={lineHeight:1.33008,lineGap:0},Xs=[1.76738,1,1,.98946,1.14763,1.05365,1.06234,.96927,.92586,1.15373,1.18414,.91349,.91349,1.07403,1.17308,.78383,1.20088,.78383,1.42531,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.78383,.78383,1.17308,1.17308,1.17308,.77349,.94565,.94729,.85944,.88506,.9858,.74817,.80016,.88449,.98039,.95782,.69238,.89898,.83231,.98183,1.03989,.96924,.86237,.96924,.80595,.74524,.86091,.95402,.94143,.98448,.8858,.83089,.93285,1.0949,1.39016,1.0949,1.45994,.74627,1.04839,.97454,.97454,.87207,.97454,.87533,1.06151,.97454,1.00176,1.16484,1.08132,.98047,1.16484,1.02989,1.01054,.96225,.97454,.97454,1.06598,.79004,1.16344,1.00351,.94629,.9973,.91016,.96777,.9043,.91082,.92481,.91082,1.17308,.95748,.96927,.96927,1,.96927,.92481,.80597,1.04839,1.23393,1.1781,.9245,1.17308,1.20808,.63218,.94261,1.24822,1.09971,1.09971,1.04839,1,.85273,.78032,1.04839,1.09971,1.22326,.9245,1.09836,1.13525,1.15222,.70424,.94729,.94729,.94729,.94729,.94729,.94729,.85498,.88506,.74817,.74817,.74817,.74817,.95782,.95782,.95782,.95782,.9858,1.03989,.96924,.96924,.96924,.96924,.96924,1.17308,.96924,.95402,.95402,.95402,.95402,.83089,.86237,.88409,.97454,.97454,.97454,.97454,.97454,.97454,.92916,.87207,.87533,.87533,.87533,.87533,.93146,.93146,.93146,.93146,.93854,1.01054,.96225,.96225,.96225,.96225,.96225,1.24822,.8761,1.00351,1.00351,1.00351,1.00351,.96777,.97454,.96777,.94729,.97454,.94729,.97454,.94729,.97454,.88506,.87207,.88506,.87207,.88506,.87207,.88506,.87207,.9858,.95391,.9858,.97454,.74817,.87533,.74817,.87533,.74817,.87533,.74817,.87533,.74817,.87533,.88449,.97454,.88449,.97454,.88449,.97454,1,1,.98039,1.00176,.98039,1.00176,.95782,.93146,.95782,.93146,.95782,.93146,.95782,1.16484,.95782,.93146,.84421,1.12761,.69238,1.08132,1,1,.98047,.83231,1.16484,1,1,.84723,1.04861,.84723,.78755,.83231,1.23736,1.03989,1.01054,1,1,1.03989,1.01054,.9857,1.03849,1.01054,.96924,.96225,.96924,.96225,.96924,.96225,.92383,.90171,.80595,1.06598,1,1,.80595,1.06598,.74524,.79004,.74524,.79004,.74524,.79004,.74524,.79004,1,1,.86091,1.02759,.85771,1.16344,.95402,1.00351,.95402,1.00351,.95402,1.00351,.95402,1.00351,.95402,1.00351,.95402,1.00351,.98448,.9973,.83089,.96777,.83089,.93285,.9043,.93285,.9043,.93285,.9043,1.31868,.96927,.94729,.97454,.85498,.92916,.96924,.8761,1,1,.86091,1.16344,1.04839,1.04839,1.04839,1.04839,1.04839,1.04839,1.04839,1.04839,1.04839,.81965,.81965,.94729,.78032,.71022,.90883,.84171,.99877,.77596,1.05734,1.2,.94729,.85944,.82791,.9607,.74817,.93285,.98039,.96924,.95782,.89898,.98316,.98183,1.03989,.78614,.96924,.97642,.86237,.86075,.86091,.83089,.90082,.8858,.97296,1.01284,.95782,.83089,1.0976,1.04,1.03342,1.2,1.0675,1.0976,.98205,1.03809,1.05097,1.04,.95364,1.03342,1.05401,1.2,1.02148,1.0119,1.04724,1.0127,1.02732,.96225,.8965,.97783,.93574,.94818,1.30679,1.0675,1.11826,.99821,1.0557,1.0326,1.2,1.0675,.96225,1.0675,1.0326,.74817,1,1,1,1,1,1,1,1,1,1,1,1,1.03754,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.87533,1,1,1,1,1,1,1,1,1,1,1,1,.98705,1,1,1,1,1,1,.98448,.9973,.98448,.9973,.98448,.9973,.83089,.96777,1,1.20088,.89903,1,1,.75155,.94945,.94945,.94945,.94945,1.12317,1.12317,1.12317,.67603,.67603,1.15621,.73584,1.21191,1.22135,1.06483,.94868,.94868,.95996,1.24633,1,1.07497,.87709,.96927,1.01473,.96927,1,1,1,.77295,1,1,1.09836,1.09836,1.09836,1.01522,.86321,.94434,.8649,.94434,.86182,1,1,1.083,1,.91578,.86438,1.17308,1.18416,1.14589,.69825,.97622,1.96791,1.24822,1.24822,1.17308,1.24822,1.24822,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.42603,1,.99862,.99862,1,.87025,.87025,.87025,.87025,1.17984,1.42603,1,1.42603,1.42603,.99862,1,1,1,1,1,1.2886,1.04315,1.15296,1.34163,1,1,1,1.10742,1.10742,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],Ws={lineHeight:1.33008,lineGap:0},Ks=[1.76738,1,1,.98594,1.02285,1.10454,1.06234,.96927,.92037,1.19985,1.2046,.90616,.90616,1.07152,1.1714,.78032,1.20088,.78032,1.40246,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.96927,.78032,.78032,1.1714,1.1714,1.1714,.80597,.94084,.96706,.85944,.85734,.97093,.75842,.79936,.88198,.9831,.95782,.71387,.86969,.84636,1.07796,1.03584,.96924,.83968,.96924,.82826,.79649,.85771,.95132,.93119,.98965,.88433,.8287,.93365,1.08612,1.3638,1.08612,1.45786,.74627,.80499,.91484,1.05707,.92383,1.05882,.9403,1.12654,1.05882,1.01756,1.09011,1.09011,.99414,1.09011,1.034,1.01756,1.05356,1.05707,1.05882,1.04399,.84863,1.21968,1.01756,.95801,1.00068,.91797,.96777,.9043,.90351,.92105,.90351,1.1714,.85337,.96927,.96927,.99912,.96927,.92105,.80597,1.2434,1.20808,1.05937,.90957,1.1714,1.20808,.75155,.94261,1.24644,1.09971,1.09971,.84751,1,.85273,.78032,.61584,1.05425,1.17914,.90957,1.08665,1.11593,1.14169,.73381,.96706,.96706,.96706,.96706,.96706,.96706,.86035,.85734,.75842,.75842,.75842,.75842,.95782,.95782,.95782,.95782,.97093,1.03584,.96924,.96924,.96924,.96924,.96924,1.1714,.96924,.95132,.95132,.95132,.95132,.8287,.83968,.89049,.91484,.91484,.91484,.91484,.91484,.91484,.93575,.92383,.9403,.9403,.9403,.9403,.8717,.8717,.8717,.8717,1.00527,1.01756,1.05356,1.05356,1.05356,1.05356,1.05356,1.24644,.95923,1.01756,1.01756,1.01756,1.01756,.96777,1.05707,.96777,.96706,.91484,.96706,.91484,.96706,.91484,.85734,.92383,.85734,.92383,.85734,.92383,.85734,.92383,.97093,1.0969,.97093,1.05882,.75842,.9403,.75842,.9403,.75842,.9403,.75842,.9403,.75842,.9403,.88198,1.05882,.88198,1.05882,.88198,1.05882,1,1,.9831,1.01756,.9831,1.01756,.95782,.8717,.95782,.8717,.95782,.8717,.95782,1.09011,.95782,.8717,.84784,1.11551,.71387,1.09011,1,1,.99414,.84636,1.09011,1,1,.84636,1.0536,.84636,.94298,.84636,1.23297,1.03584,1.01756,1,1,1.03584,1.01756,1.00323,1.03444,1.01756,.96924,1.05356,.96924,1.05356,.96924,1.05356,.93066,.98293,.82826,1.04399,1,1,.82826,1.04399,.79649,.84863,.79649,.84863,.79649,.84863,.79649,.84863,1,1,.85771,1.17318,.85771,1.21968,.95132,1.01756,.95132,1.01756,.95132,1.01756,.95132,1.01756,.95132,1.01756,.95132,1.01756,.98965,1.00068,.8287,.96777,.8287,.93365,.9043,.93365,.9043,.93365,.9043,1.08571,.96927,.96706,.91484,.86035,.93575,.96924,.95923,1,1,.85771,1.21968,1.11437,1.11437,.93109,.91202,.60411,.84164,.55572,1.01173,.97361,.81818,.81818,.96635,.78032,.72727,.92366,.98601,1.03405,.77968,1.09799,1.2,.96706,.85944,.85638,.96491,.75842,.93365,.9831,.96924,.95782,.86969,.94152,1.07796,1.03584,.78437,.96924,.98715,.83968,.83491,.85771,.8287,.94492,.88433,.9287,1.0098,.95782,.8287,1.0625,.98248,1.03424,1.2,1.01071,1.0625,.95246,1.03809,1.04912,.98248,1.00221,1.03424,1.05443,1.2,1.04785,.99609,1.00169,1.05176,.99346,1.05356,.9087,1.03004,.95542,.93117,1.23362,1.01071,1.07831,1.02512,1.05205,1.03502,1.2,1.01071,1.05356,1.01071,1.03502,.75842,1,1,1,1,1,1,1,1,1,1,1,1,1.03719,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.9403,1,1,1,1,1,1,1,1,1,1,1,1,1.04021,1,1,1,1,1,1,.98965,1.00068,.98965,1.00068,.98965,1.00068,.8287,.96777,1,1.20088,.89903,1,1,.75155,1.03077,1.03077,1.03077,1.03077,1.13196,1.13196,1.13196,.67428,.67428,1.16039,.73291,1.20996,1.22135,1.06483,.94868,.94868,.95996,1.24633,1,1.07497,.87796,.96927,1.01518,.96927,1,1,1,.77295,1,1,1.10539,1.10539,1.11358,1.06967,.86279,.94434,.86279,.94434,.86182,1,1,1.083,1,.91578,.86507,1.1714,1.18416,1.14589,.69825,.97622,1.9697,1.24822,1.24822,1.17238,1.24822,1.24822,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1.42603,1,.99862,.99862,1,.87025,.87025,.87025,.87025,1.18083,1.42603,1,1.42603,1.42603,.99862,1,1,1,1,1,1.2886,1.04315,1.15296,1.34163,1,1,1,1.10938,1.10938,1,1,1,1.05425,1.09971,1.09971,1.09971,1,1,1,1,1,1,1,1,1,1,1],Gs={lineHeight:1.33008,lineGap:0},Vs=getLookupTableFactory(function(e){e["MyriadPro-Regular"]=e["PdfJS-Fallback-Regular"]={name:"LiberationSans-Regular",factors:Ns,baseWidths:Fs,baseMapping:Ts,metrics:Es};e["MyriadPro-Bold"]=e["PdfJS-Fallback-Bold"]={name:"LiberationSans-Bold",factors:Os,baseWidths:vs,baseMapping:Ss,metrics:Rs};e["MyriadPro-It"]=e["MyriadPro-Italic"]=e["PdfJS-Fallback-Italic"]={name:"LiberationSans-Italic",factors:Ds,baseWidths:Cs,baseMapping:Is,metrics:Ms};e["MyriadPro-BoldIt"]=e["MyriadPro-BoldItalic"]=e["PdfJS-Fallback-BoldItalic"]={name:"LiberationSans-BoldItalic",factors:Hs,baseWidths:As,baseMapping:xs,metrics:Bs};e.ArialMT=e.Arial=e["Arial-Regular"]={name:"LiberationSans-Regular",baseWidths:Fs,baseMapping:Ts};e["Arial-BoldMT"]=e["Arial-Bold"]={name:"LiberationSans-Bold",baseWidths:vs,baseMapping:Ss};e["Arial-ItalicMT"]=e["Arial-Italic"]={name:"LiberationSans-Italic",baseWidths:Cs,baseMapping:Is};e["Arial-BoldItalicMT"]=e["Arial-BoldItalic"]={name:"LiberationSans-BoldItalic",baseWidths:As,baseMapping:xs};e["Calibri-Regular"]={name:"LiberationSans-Regular",factors:ms,baseWidths:Fs,baseMapping:Ts,metrics:ps};e["Calibri-Bold"]={name:"LiberationSans-Bold",factors:os,baseWidths:vs,baseMapping:Ss,metrics:ls};e["Calibri-Italic"]={name:"LiberationSans-Italic",factors:hs,baseWidths:Cs,baseMapping:Is,metrics:us};e["Calibri-BoldItalic"]={name:"LiberationSans-BoldItalic",factors:fs,baseWidths:As,baseMapping:xs,metrics:cs};e["Segoeui-Regular"]={name:"LiberationSans-Regular",factors:Ks,baseWidths:Fs,baseMapping:Ts,metrics:Gs};e["Segoeui-Bold"]={name:"LiberationSans-Bold",factors:_s,baseWidths:vs,baseMapping:Ss,metrics:zs};e["Segoeui-Italic"]={name:"LiberationSans-Italic",factors:Xs,baseWidths:Cs,baseMapping:Is,metrics:Ws};e["Segoeui-BoldItalic"]={name:"LiberationSans-BoldItalic",factors:Ls,baseWidths:As,baseMapping:xs,metrics:Us};e["Helvetica-Regular"]=e.Helvetica={name:"LiberationSans-Regular",factors:ys,baseWidths:Fs,baseMapping:Ts,metrics:qs};e["Helvetica-Bold"]={name:"LiberationSans-Bold",factors:ds,baseWidths:vs,baseMapping:Ss,metrics:gs};e["Helvetica-Italic"]={name:"LiberationSans-Italic",factors:js,baseWidths:Cs,baseMapping:Is,metrics:ks};e["Helvetica-BoldItalic"]={name:"LiberationSans-BoldItalic",factors:bs,baseWidths:As,baseMapping:xs,metrics:ws}});function getXfaFontName(e){const t=normalizeFontName(e);return Vs()[t]}function getXfaFontDict(e){const t=function getXfaFontWidths(e){const t=getXfaFontName(e);if(!t)return null;const{baseWidths:a,baseMapping:n,factors:s}=t,r=s?a.map((e,t)=>e*s[t]):a;let i,o=-2;const l=[];for(const[e,t]of n.map((e,t)=>[e,t]).sort(([e],[t])=>e-t))if(-1!==e)if(e===o+1){i.push(r[t]);o+=1}else{o=e;i=[r[t]];l.push(e,i)}return l}(e),a=new Dict(null);a.set("BaseFont",Name.get(e));a.set("Type",Name.get("Font"));a.set("Subtype",Name.get("CIDFontType2"));a.set("Encoding",Name.get("Identity-H"));a.set("CIDToGIDMap",Name.get("Identity"));a.set("W",t);a.set("FirstChar",t[0]);a.set("LastChar",t.at(-2)+t.at(-1).length-1);const n=new Dict(null);a.set("FontDescriptor",n);const s=new Dict(null);s.set("Ordering","Identity");s.set("Registry","Adobe");s.set("Supplement",0);a.set("CIDSystemInfo",s);return a}const $s={number:0,lbrace:1,rbrace:2,true:3,false:4,add:5,sub:6,mul:7,div:8,idiv:9,mod:10,exp:11,eq:12,ne:13,gt:14,ge:15,lt:16,le:17,and:18,or:19,xor:20,bitshift:21,abs:22,neg:23,ceiling:24,floor:25,round:26,truncate:27,not:28,sqrt:29,sin:30,cos:31,ln:32,log:33,atan:34,cvi:35,cvr:36,dup:37,exch:38,pop:39,copy:40,index:41,roll:42,if:43,ifelse:44,eof:45,min:46,max:47};class Token{constructor(e,t=null){this.id=e;this.value=t}}class lexer_Lexer{static#re=null;static#ie=null;static#oe(){const e=Object.create(null),t=Object.create(null);for(const[a,n]of Object.entries($s)){if("number"===a)continue;const s=n>=$s.true&&n<=$s.ifelse,r=new Token(n,s?a:null);e[a]=r;s&&(t[a]=r)}this.#re=e;this.#ie=t}constructor(e){lexer_Lexer.#re||lexer_Lexer.#oe();this.data=e;this.pos=0;this.len=e.length;this._numberPattern=/[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y;this._identifierPattern=/[a-z]+/y}_skipComment(){const e=this.data.indexOf("\n",this.pos),t=this.data.indexOf("\r",this.pos),a=Math.min(e<0?this.len:e,t<0?this.len:t);this.pos=Math.min(a+1,this.len)}_getNumber(){this._numberPattern.lastIndex=this.pos;const e=this._numberPattern.exec(this.data);if(!e)return new Token($s.number,0);const t=parseFloat(e[0]);if(!Number.isFinite(t))return new Token($s.number,0);this.pos=this._numberPattern.lastIndex;return new Token($s.number,t)}_getOperator(){this._identifierPattern.lastIndex=this.pos;const e=this._identifierPattern.exec(this.data);if(!e)return new Token($s.number,0);this.pos=this._identifierPattern.lastIndex;const t=e[0],a=lexer_Lexer.#ie[t];return a||new Token($s.number,0)}next(){for(;this.pos=48&&e<=57){this.pos--;return this._getNumber()}if(e>=97&&e<=122){this.pos--;return this._getOperator()}return new Token($s.number,0)}}return lexer_Lexer.#re.eof}}const Ys=0,Js=1,Qs=2,Zs=0,er=1,tr=2,ar=3,nr=4,sr=5,rr=6,ir=7,or=8,lr=9,fr=10;class PsNode{constructor(e){this.type=e}}class PsProgram extends PsNode{constructor(e){super(Zs);this.body=e}}class PsBlock extends PsNode{constructor(e){super(er);this.instructions=e}}class PsNumber extends PsNode{constructor(e){super(tr);this.value=e}}class PsOperator extends PsNode{constructor(e){super(ar);this.op=e}}class PsIf extends PsNode{constructor(e){super(nr);this.then=e}}class PsIfElse extends PsNode{constructor(e,t){super(sr);this.then=e;this.otherwise=t}}class PsArgNode extends PsNode{constructor(e){super(rr);this.index=e;this.valueType=Ys}}class PsConstNode extends PsNode{constructor(e){super(ir);this.value=e;this.valueType="boolean"==typeof e?Js:Ys}}class PsUnaryNode extends PsNode{constructor(e,t,a=Qs){super(or);this.op=e;this.operand=t;this.valueType=a}}class PsBinaryNode extends PsNode{constructor(e,t,a,n=Qs){super(lr);this.op=e;this.first=t;this.second=a;this.valueType=n}}class PsTernaryNode extends PsNode{constructor(e,t,a,n=Qs){super(fr);this.cond=e;this.then=t;this.otherwise=a;this.valueType=n}}class ast_Parser{constructor(e){this.lexer=e;this._token=null}static _isRegularOperator(e){return e>=$s.true&&e<$s.if}_advance(){this._token=this.lexer.next()}_expect(e){if(this._token.id!==e)throw new FormatError(`PostScript function: expected token id ${e}, got ${this._token.id}.`);const t=this._token;this._advance();return t}parse(){this._advance();this._expect($s.lbrace);const e=this._parseBlock();this._expect($s.rbrace);this._token.id!==$s.eof&&warn("PostScript function: unexpected content after closing brace.");return new PsProgram(e)}_parseBlock(){const e=[];for(;;){const t=this._token;switch(t.id){case $s.number:e.push(new PsNumber(t.value));this._advance();break;case $s.lbrace:{this._advance();const t=this._parseBlock();this._expect($s.rbrace);if(this._token.id===$s.if){this._advance();e.push(new PsIf(t))}else{if(this._token.id!==$s.lbrace)throw new FormatError("PostScript function: a procedure block must be followed by 'if' or '{…} ifelse'.");{this._advance();const a=this._parseBlock();this._expect($s.rbrace);this._expect($s.ifelse);e.push(new PsIfElse(t,a))}}break}case $s.rbrace:case $s.eof:return new PsBlock(e);case $s.if:case $s.ifelse:throw new FormatError(`PostScript function: unexpected '${t.value}' operator.`);default:if(ast_Parser._isRegularOperator(t.id)){e.push(new PsOperator(t.id));this._advance();break}throw new FormatError(`PostScript function: unexpected token id ${t.id}.`)}}}}function parsePostScriptFunction(e){return new ast_Parser(new lexer_Lexer(e)).parse()}function _nodesEqual(e,t){if(e===t)return!0;if(e.type!==t.type)return!1;switch(e.type){case rr:return e.index===t.index;case ir:return e.value===t.value;case or:return e.op===t.op&&_nodesEqual(e.operand,t.operand);case lr:return e.op===t.op&&_nodesEqual(e.first,t.first)&&_nodesEqual(e.second,t.second);case fr:return _nodesEqual(e.cond,t.cond)&&_nodesEqual(e.then,t.then)&&_nodesEqual(e.otherwise,t.otherwise);default:return!1}}const cr=100;class PSStackToTree{static#le=null;static#fe=null;static#ce=null;static#he=null;static#ue(){this.#le=new Set([$s.add,$s.sub,$s.mul,$s.div,$s.idiv,$s.mod,$s.exp,$s.atan,$s.eq,$s.ne,$s.gt,$s.ge,$s.lt,$s.le,$s.and,$s.or,$s.xor,$s.bitshift]);this.#fe=new Set([$s.abs,$s.neg,$s.ceiling,$s.floor,$s.round,$s.truncate,$s.sqrt,$s.sin,$s.cos,$s.ln,$s.log,$s.cvi,$s.cvr,$s.not]);this.#ce=new Set([$s.abs,$s.ceiling,$s.cvi,$s.cvr,$s.floor,$s.round,$s.truncate]);this.#he=new Map([[$s.eq,$s.ne],[$s.ne,$s.eq],[$s.lt,$s.ge],[$s.le,$s.gt],[$s.gt,$s.le],[$s.ge,$s.lt]])}evaluate(e,t){PSStackToTree.#le||PSStackToTree.#ue();this._failed=!1;if(t>cr)return null;const a=[];for(let e=0;e{if(!e||e.type===rr||e.type===ir)return;const a=t.get(e)??0;t.set(e,a+1);if(!(a>0))switch(e.type){case or:visit(e.operand);break;case lr:visit(e.first);visit(e.second);break;case fr:visit(e.cond);visit(e.then);visit(e.otherwise)}};for(const t of e)visit(t);for(const[e,a]of t)if(a>1){e.shared=!0;e.sharedCount=a}}_evalBlock(e,t){this._evalBlockFrom(e.instructions,0,t)}_evalBlockFrom(e,t,a){for(let n=t;ncr&&(this._failed=!0);break;case ar:this._evalOp(t.op,a);break;case nr:{if(a.length<1){this._failed=!0;break}const s=a.pop(),r=a.slice();this._evalBlock(t.then,a);if(this._failed)break;if(a.length===r.length)for(let e=0;er.length){if(s.type===ir){if(!s.value){a.length=0;a.push(...r)}break}const t=a.slice();this._evalBlockFrom(e,n+1,t);if(this._failed)break;const i=r;this._evalBlockFrom(e,n+1,i);if(this._failed)break;if(t.length!==i.length){const e=new PsConstNode(0);for(;t.lengthcr&&(this._failed=!0);break;case $s.false:t.push(new PsConstNode(!1));t.length>cr&&(this._failed=!0);break;case $s.dup:if(t.length<1){this._failed=!0;break}t.push(t.at(-1));t.length>cr&&(this._failed=!0);break;case $s.exch:{if(t.length<2){this._failed=!0;break}const e=t.pop(),a=t.pop();t.push(e,a);break}case $s.pop:if(t.length<1){this._failed=!0;break}t.pop();break;case $s.copy:{if(t.length<1){this._failed=!0;break}const e=t.pop();if(e.type===ir){const a=0|e.value;if(0===a);else if(a<0||a>t.length)this._failed=!0;else{t.push(...t.slice(-a));t.length>cr&&(this._failed=!0)}}else this._failed=!0;break}case $s.index:{if(t.length<1){this._failed=!0;break}const e=t.pop();if(e.type===ir){const a=0|e.value;a<0||a>=t.length?this._failed=!0:t.push(t.at(-a-1))}else this._failed=!0;break}case $s.roll:{if(t.length<2){this._failed=!0;break}const e=t.pop(),a=t.pop();if(a.type===ir&&e.type===ir){const n=0|a.value;if(0===n);else if(n<0||n>t.length)this._failed=!0;else{const a=((0|e.value)%n+n)%n;if(a>0){const e=t.splice(-n,n);t.push(...e.slice(n-a),...e.slice(0,n-a))}}}else this._failed=!0;break}default:this._failed=!0}}_makeBinary(e,t,a){if(t.type===ir&&a.type===ir){const n=function _evalBinaryConst(e,t,a){switch(e){case $s.add:return t+a;case $s.sub:return t-a;case $s.mul:return t*a;case $s.div:return 0!==a?t/a:0;case $s.idiv:return 0!==a?Math.trunc(t/a):0;case $s.mod:return 0!==a?t-Math.trunc(t/a)*a:0;case $s.exp:{const e=t**a;return Number.isFinite(e)?e:void 0}case $s.atan:{let e=Math.atan2(t,a)*(180/Math.PI);e<0&&(e+=360);return e}case $s.eq:return t===a;case $s.ne:return t!==a;case $s.gt:return t>a;case $s.ge:return t>=a;case $s.lt:return t=0?t<>-a|0;case $s.min:return Math.min(t,a);case $s.max:return Math.max(t,a);default:return}}(e,a.value,t.value);if(void 0!==n)return new PsConstNode(n)}if(_nodesEqual(t,a))switch(e){case $s.sub:return new PsConstNode(0);case $s.xor:return new PsConstNode(t.valueType!==Js&&0);case $s.and:case $s.or:case $s.min:case $s.max:return t;case $s.eq:case $s.ge:case $s.le:return new PsConstNode(!0);case $s.ne:case $s.gt:case $s.lt:return new PsConstNode(!1)}if(t.type===ir){const n=t.value;switch(e){case $s.add:case $s.sub:if(0===n)return a;break;case $s.mul:if(1===n)return a;if(0===n)return t;if(-1===n)return this._makeUnary($s.neg,a);break;case $s.div:if(0!==n)return this._makeBinary($s.mul,new PsConstNode(1/n),a);break;case $s.idiv:if(1===n)return a;break;case $s.exp:if(1===n)return a;if(-1===n)return this._makeBinary($s.div,a,new PsConstNode(1));if(.5===n)return this._makeUnary($s.sqrt,a);if(.25===n){const e=this._makeUnary($s.sqrt,a);return this._makeUnary($s.sqrt,e)}if(2===n)return this._makeBinary($s.mul,a,a);if(3===n)return this._makeBinary($s.mul,this._makeBinary($s.mul,a,a),a);if(4===n){const e=this._makeBinary($s.mul,a,a);return this._makeBinary($s.mul,e,e)}if(0===n)return new PsConstNode(1);break;case $s.and:if(!0===n)return a;if(!1===n)return t;break;case $s.or:if(!1===n)return a;if(!0===n)return t;break;case $s.min:if(a.type===lr&&a.op===$s.max&&a.first.type===ir&&a.first.value>=n)return t;break;case $s.max:if(a.type===lr&&a.op===$s.min&&a.first.type===ir&&a.first.value<=n)return t}}if(a.type===ir){const n=a.value;switch(e){case $s.add:if(0===n)return t;break;case $s.sub:if(0===n)return this._makeUnary($s.neg,t);break;case $s.mul:if(1===n)return t;if(0===n)return a;if(-1===n)return this._makeUnary($s.neg,t);break;case $s.and:if(!0===n)return t;if(!1===n)return a;break;case $s.or:if(!1===n)return t;if(!0===n)return a}}return new PsBinaryNode(e,t,a,function _binaryValueType(e,t,a){switch(e){case $s.eq:case $s.ne:case $s.gt:case $s.ge:case $s.lt:case $s.le:return Js;case $s.and:case $s.or:case $s.xor:return t===a&&t!==Qs?t:Qs;default:return Ys}}(e,t.valueType,a.valueType))}_makeUnary(e,t){if(t.type===ir){const a=function _evalUnaryConst(e,t){switch(e){case $s.abs:return Math.abs(t);case $s.neg:return-t;case $s.ceiling:return Math.ceil(t);case $s.floor:return Math.floor(t);case $s.round:return Math.round(t);case $s.truncate:return Math.trunc(t);case $s.sqrt:{const e=Math.sqrt(t);return Number.isFinite(e)?e:void 0}case $s.sin:return Math.sin(t%360*Math.PI/180);case $s.cos:return Math.cos(t%360*Math.PI/180);case $s.ln:{const e=Math.log(t);return Number.isFinite(e)?e:void 0}case $s.log:{const e=Math.log10(t);return Number.isFinite(e)?e:void 0}case $s.cvi:return Math.trunc(t);case $s.cvr:return t;case $s.not:return"boolean"==typeof t?!t:~t;default:return}}(e,t.value);if(void 0!==a)return new PsConstNode(a)}if(e===$s.not&&t.type===lr){const e=PSStackToTree.#he.get(t.op);if(void 0!==e)return new PsBinaryNode(e,t.first,t.second,Js)}if(e===$s.neg&&t.type===lr&&t.op===$s.sub)return this._makeBinary($s.sub,t.second,t.first);if(t.type===or){if(e===$s.neg&&t.op===$s.neg||e===$s.not&&t.op===$s.not)return t.operand;if(e===$s.abs&&t.op===$s.neg)return this._makeUnary($s.abs,t.operand);if(PSStackToTree.#ce.has(e)&&e===t.op)return t}return new PsUnaryNode(e,t,function _unaryValueType(e,t){return e===$s.not?t:Ys}(e,t.valueType))}_makeTernary(e,t,a){if(e.type===ir)return e.value?t:a;if(_nodesEqual(t,a))return t;if(t.type===ir&&a.type===ir){if(!0===t.value&&!1===a.value)return e;if(!1===t.value&&!0===a.value)return this._makeUnary($s.not,e)}if(e.type===lr){const{op:n,first:s,second:r}=e;if(n===$s.gt||n===$s.ge){if(_nodesEqual(t,s)&&_nodesEqual(a,r))return this._makeBinary($s.min,s,r);if(_nodesEqual(t,r)&&_nodesEqual(a,s))return this._makeBinary($s.max,s,r)}else if(n===$s.lt||n===$s.le){if(_nodesEqual(t,s)&&_nodesEqual(a,r))return this._makeBinary($s.max,s,r);if(_nodesEqual(t,r)&&_nodesEqual(a,s))return this._makeBinary($s.min,s,r)}}return new PsTernaryNode(e,t,a,t.valueType===a.valueType?t.valueType:Qs)}}const hr=0,ur=1,mr=2,pr=3,dr=4,gr=5,br=6,wr=7,jr=8,kr=9,yr=10,qr=11,vr=12,Sr=13,Ar=14,xr=15,Cr=16,Ir=17,Fr=18,Tr=19,Or=20,Rr=21,Hr=22,Dr=23,Mr=24,Nr=25,Pr=26,Er=27,_r=28,zr=29,Lr=30,Ur=31,Xr=32,Wr=33,Kr=34,Gr=35,Vr=36,$r=37,Yr=38,Jr=39,Qr=40,Zr=Math.PI/180,ei=180/Math.PI;class PsJsCompiler{static#pe=new Float64Array(64);static#de=new Float64Array(64);constructor(e,t){this.nIn=e.length>>1;this.nOut=t.length>>1;this.range=t;this.ir=[];this._tmpMap=new Map;this._nextTmp=0}_compileNode(e){if(e.shared){const t=this._tmpMap.get(e);if(void 0!==t){this.ir.push(Qr,t);return!0}if(!this._compileNodeImpl(e))return!1;const a=this._nextTmp++;this._tmpMap.set(e,a);this.ir.push(Jr,a);return!0}return this._compileNodeImpl(e)}_compileNodeImpl(e){switch(e.type){case rr:this.ir.push(hr,e.index);return!0;case ir:{const t=e.value;this.ir.push(ur,"boolean"==typeof t?Number(t):t);return!0}case or:return this._compileUnary(e);case lr:return this._compileBinary(e);case fr:return this._compileTernary(e);default:return!1}}_compileUnary(e){const{op:t,operand:a,valueType:n}=e;if(t===$s.cvr)return this._compileNode(a);if(!this._compileNode(a))return!1;switch(t){case $s.abs:this.ir.push(gr);break;case $s.neg:this.ir.push(br);break;case $s.ceiling:this.ir.push(wr);break;case $s.floor:this.ir.push(jr);break;case $s.round:this.ir.push(kr);break;case $s.truncate:this.ir.push(yr);break;case $s.sqrt:this.ir.push(Sr);break;case $s.sin:this.ir.push(Ar);break;case $s.cos:this.ir.push(xr);break;case $s.ln:this.ir.push(Cr);break;case $s.log:this.ir.push(Ir);break;case $s.cvi:this.ir.push(Fr);break;case $s.not:if(n===Js)this.ir.push(qr);else{if(n!==Ys)return!1;this.ir.push(vr)}break;default:return!1}return!0}_compileBinary(e){const{op:t,first:a,second:n}=e;if(t===$s.bitshift){if(a.type!==ir||!Number.isInteger(a.value))return!1;if(!this._compileNode(n))return!1;this.ir.push(Tr,a.value);return!0}if(!this._compileNode(n))return!1;if(!this._compileNode(a))return!1;switch(t){case $s.add:this.ir.push(Or);break;case $s.sub:this.ir.push(Rr);break;case $s.mul:this.ir.push(Hr);break;case $s.div:this.ir.push(Dr);break;case $s.idiv:this.ir.push(Mr);break;case $s.mod:this.ir.push(Nr);break;case $s.exp:this.ir.push(Pr);break;case $s.eq:this.ir.push(Er);break;case $s.ne:this.ir.push(_r);break;case $s.gt:this.ir.push(zr);break;case $s.ge:this.ir.push(Lr);break;case $s.lt:this.ir.push(Ur);break;case $s.le:this.ir.push(Xr);break;case $s.and:this.ir.push(Wr);break;case $s.or:this.ir.push(Kr);break;case $s.xor:this.ir.push(Gr);break;case $s.atan:this.ir.push(Vr);break;case $s.min:this.ir.push($r);break;case $s.max:this.ir.push(Yr);break;default:return!1}return!0}_compileTernary(e){if(!this._compileNode(e.cond))return!1;this.ir.push(pr,0);const t=this.ir.length-1;if(!this._compileNode(e.then))return!1;this.ir.push(dr,0);const a=this.ir.length-1;this.ir[t]=this.ir.length;if(!this._compileNode(e.otherwise))return!1;this.ir[a]=this.ir.length;return!0}compile(e){const t=(new PSStackToTree).evaluate(e,this.nIn);if(!t||t.length0?a<>-t:a;break}case Or:{const e=l[--i];l[i-1]+=e;break}case Rr:{const e=l[--i];l[i-1]-=e;break}case Hr:{const e=l[--i];l[i-1]*=e;break}case Dr:{const e=l[--i];l[i-1]=0!==e?l[i-1]/e:0;break}case Mr:{const e=l[--i];l[i-1]=0!==e?Math.trunc(l[i-1]/e):0;break}case Nr:{const e=l[--i];l[i-1]=0!==e?l[i-1]%e:0;break}case Pr:{const e=l[--i];l[i-1]**=e;break}case Er:{const e=l[--i];l[i-1]=l[i-1]===e?1:0;break}case _r:{const e=l[--i];l[i-1]=l[i-1]!==e?1:0;break}case zr:{const e=l[--i];l[i-1]=l[i-1]>e?1:0;break}case Lr:{const e=l[--i];l[i-1]=l[i-1]>=e?1:0;break}case Ur:{const e=l[--i];l[i-1]=l[i-1]e?1:0;break}case $s.ge:{const e=t[--this.#ge];t[this.#ge-1]=t[this.#ge-1]>=e?1:0;break}case $s.lt:{const e=t[--this.#ge];t[this.#ge-1]=t[this.#ge-1]0?a<>-e;break}case $s.min:{const e=t[--this.#ge];t[this.#ge-1]=Math.min(t[this.#ge-1],e);break}case $s.max:{const e=t[--this.#ge];t[this.#ge-1]=Math.max(t[this.#ge-1],e);break}case $s.dup:this.#be(t[this.#ge-1]);break;case $s.exch:{const e=t[--this.#ge],a=t[--this.#ge];this.#be(e);this.#be(a);break}case $s.pop:this.#ge--;break;case $s.copy:{const e=Math.trunc(t[--this.#ge]),a=this.#ge-e;for(let n=0;n1&&0!==e){const n=(e%a+a)%a;if(0!==n){const e=this.#ge-a,s=t.slice(e,this.#ge);for(let r=0;r>1,s=a.length>>1,{instructions:r}=e.body;return(e,t,i,o)=>{this.#ge=0;for(let a=0;a=0?this.#pe[l+e]:0;i[o+e]=MathClamp(a[2*e+1],a[2*e],t)}}}}const ti={if:4,else:5,end:11,select:27,call:16,local_get:32,local_set:33,local_tee:34,i32_const:65,i32_eqz:69,i32_and:113,i32_or:114,i32_xor:115,i32_shl:116,i32_shr_s:117,i32_trunc_f64_s:170,f64_const:68,f64_eq:97,f64_ne:98,f64_lt:99,f64_gt:100,f64_le:101,f64_ge:102,f64_abs:153,f64_neg:154,f64_ceil:155,f64_floor:156,f64_trunc:157,f64_nearest:158,f64_sqrt:159,f64_add:160,f64_sub:161,f64_mul:162,f64_div:163,f64_min:164,f64_max:165,f64_convert_i32_s:183,f64_store:57},ai=124,ni=1,si=2,ri=3,ii=5,oi=7,li=10;function unsignedLEB128(e){const t=[];do{let a=127&e;0!==(e>>>=7)&&(a|=128);t.push(a)}while(0!==e);return t}function encodeASCIIString(e){return[...unsignedLEB128(e.length),...Array.from(e,e=>e.charCodeAt(0))]}function section(e,t){return[e,...unsignedLEB128(t.length),...t]}function vec(e){const t=unsignedLEB128(e.length);for(const a of e)if("number"!=typeof a)for(const e of a)t.push(e);else t.push(a);return t}const fi=[["sin","Math","sin",[ai],[ai]],["cos","Math","cos",[ai],[ai]],["atan2","Math","atan2",[ai,ai],[ai]],["log","Math","log",[ai],[ai]],["log10","Math","log10",[ai],[ai]],["pow","Math","pow",[ai,ai],[ai]]],ci={Math:Object.fromEntries(fi.map(([e])=>[e,Math[e]]))};class PsWasmCompiler{static#ke=!1;static#ye=null;static#qe=null;static#ve=0;static#Se=0;static#Ae=null;static#xe=null;static#Ce=null;static#Ie=null;static#Fe=null;static#Te=null;static#Oe=null;static#Re=null;static#ue(){this.#ye=new Map([[$s.eq,ti.f64_eq],[$s.ne,ti.f64_ne],[$s.lt,ti.f64_lt],[$s.le,ti.f64_le],[$s.gt,ti.f64_gt],[$s.ge,ti.f64_ge]]);this.#qe=Object.create(null);for(let e=0;e[96,...vec(e),...vec(t)]);this.#xe=new Uint8Array(section(si,vec(fi.map(([,e,t],a)=>[...encodeASCIIString(e),...encodeASCIIString(t),0,...unsignedLEB128(a+1)]))));this.#Ce=new Uint8Array(section(ri,vec([[0]])));this.#Ie=new Uint8Array(section(ii,vec([[0,1]])));this.#Fe=new Uint8Array(section(oi,vec([[...encodeASCIIString("fn"),0,...unsignedLEB128(fi.length)],[...encodeASCIIString("mem"),2,0]])));this.#Te=new Uint8Array([0,97,115,109,1,0,0,0]);const e=new ArrayBuffer(8);this.#Oe=new DataView(e);this.#Re=new Uint8Array(e);this.#ke=!0}constructor(e,t){PsWasmCompiler.#ke||PsWasmCompiler.#ue();this._nIn=e.length>>1;this._nOut=t.length>>1;this._range=t;this._code=[];this._nextLocal=this._nIn;this._freeLocals=[];this._sharedLocals=new Map}_allocLocal(){return this._freeLocals.pop()??this._nextLocal++}_releaseLocal(e){this._freeLocals.push(e)}_emitULEB128(e){do{let t=127&e;0!==(e>>>=7)&&(t|=128);this._code.push(t)}while(0!==e)}_emitF64Const(e){this._code.push(ti.f64_const);PsWasmCompiler.#Oe.setFloat64(0,e,!0);for(let e=0;e<8;e++)this._code.push(PsWasmCompiler.#Re[e])}_emitLocalGet(e){this._code.push(ti.local_get);this._emitULEB128(e)}_emitLocalSet(e){this._code.push(ti.local_set);this._emitULEB128(e)}_emitLocalTee(e){this._code.push(ti.local_tee);this._emitULEB128(e)}_compileNode(e){if(e.shared){const t=this._sharedLocals.get(e);if(void 0!==t){this._emitLocalGet(t.local);0===--t.remaining&&this._releaseLocal(t.local);return!0}if(!this._compileNodeImpl(e))return!1;const a=this._allocLocal();this._sharedLocals.set(e,{local:a,remaining:e.sharedCount-1});this._emitLocalTee(a);return!0}return this._compileNodeImpl(e)}_compileNodeImpl(e){switch(e.type){case rr:this._emitLocalGet(e.index);return!0;case ir:{let t=e.value;"boolean"==typeof t&&(t=t?1:0);this._emitF64Const(t);return!0}case or:return this._compileUnaryNode(e);case lr:return this._compileBinaryNode(e);case fr:return this._compileTernaryNode(e);default:return!1}}_compileSinCosNode(e){const t=this._allocLocal();try{if(!this._compileNode(e.operand))return!1;const a=this._code;this._emitLocalSet(t);this._emitLocalGet(t);this._emitLocalGet(t);this._emitF64Const(360);a.push(ti.f64_div,ti.f64_trunc);this._emitF64Const(360);a.push(ti.f64_mul,ti.f64_sub);this._emitF64Const(PsWasmCompiler.#ve);a.push(ti.f64_mul,ti.call);this._emitULEB128(PsWasmCompiler.#qe[e.op===$s.sin?"sin":"cos"]);return!0}finally{this._releaseLocal(t)}}_compileUnaryNode(e){const t=this._code;if(e.op===$s.sin||e.op===$s.cos)return this._compileSinCosNode(e);if(e.op===$s.not){if(e.valueType===Js){if(!this._compileNodeAsBoolI32(e.operand))return!1;t.push(ti.i32_eqz,ti.f64_convert_i32_s);return!0}if(e.valueType===Ys){if(!this._compileNode(e.operand))return!1;t.push(ti.i32_trunc_f64_s,ti.i32_const,127,ti.i32_xor,ti.f64_convert_i32_s);return!0}return!1}if(!this._compileNode(e.operand))return!1;switch(e.op){case $s.abs:t.push(ti.f64_abs);break;case $s.neg:t.push(ti.f64_neg);break;case $s.sqrt:t.push(ti.f64_sqrt);break;case $s.floor:t.push(ti.f64_floor);break;case $s.ceiling:t.push(ti.f64_ceil);break;case $s.round:this._emitF64Const(.5);t.push(ti.f64_add,ti.f64_floor);break;case $s.truncate:t.push(ti.f64_trunc);break;case $s.cvi:t.push(ti.i32_trunc_f64_s,ti.f64_convert_i32_s);break;case $s.cvr:break;case $s.ln:t.push(ti.call);this._emitULEB128(PsWasmCompiler.#qe.log);break;case $s.log:t.push(ti.call);this._emitULEB128(PsWasmCompiler.#qe.log10);break;default:return!1}return!0}_compileSafeDivNode(e,t){const a=this._allocLocal();try{if(!this._compileNode(t))return!1;if(!this._compileNode(e))return!1;const n=this._code;this._emitLocalTee(a);n.push(ti.f64_div);this._emitF64Const(0);this._emitLocalGet(a);this._emitF64Const(0);n.push(ti.f64_ne,ti.select);return!0}finally{this._releaseLocal(a)}}_compileSafeIdivNode(e,t){const a=this._allocLocal();try{if(!this._compileNode(t))return!1;if(!this._compileNode(e))return!1;const n=this._code;this._emitLocalTee(a);n.push(ti.f64_div,ti.f64_trunc);this._emitF64Const(0);this._emitLocalGet(a);this._emitF64Const(0);n.push(ti.f64_ne,ti.select);return!0}finally{this._releaseLocal(a)}}_compileBitshiftNode(e,t){if(e.type!==ir||!Number.isInteger(e.value))return!1;if(!this._compileNode(t))return!1;const a=this._code;a.push(ti.i32_trunc_f64_s);const n=e.value;if(n>0){a.push(ti.i32_const);this._emitULEB128(n);a.push(ti.i32_shl)}else if(n<0){a.push(ti.i32_const);this._emitULEB128(-n);a.push(ti.i32_shr_s)}a.push(ti.f64_convert_i32_s);return!0}_compileModNode(e,t){if(e.type===ir&&0===e.value){if(!this._compileNode(t))return!1;this._code.push(ti.drop);this._emitF64Const(0);return!0}const a=this._allocLocal();try{if(!this._compileNode(t))return!1;this._emitLocalTee(a);const n=this._code;if(e.type===ir){this._emitLocalGet(a);this._emitF64Const(e.value);n.push(ti.f64_div,ti.f64_trunc);this._emitF64Const(e.value);n.push(ti.f64_mul,ti.f64_sub)}else{const t=this._allocLocal();try{if(!this._compileNode(e))return!1;this._emitLocalSet(t);this._emitLocalGet(a);this._emitLocalGet(t);n.push(ti.f64_div,ti.f64_trunc);this._emitLocalGet(t);n.push(ti.f64_mul,ti.f64_sub);this._emitF64Const(0);this._emitLocalGet(t);this._emitF64Const(0);n.push(ti.f64_ne,ti.select)}finally{this._releaseLocal(t)}}return!0}finally{this._releaseLocal(a)}}_compileAtanNode(e,t){const a=this._allocLocal();try{if(!this._compileNode(t))return!1;if(!this._compileNode(e))return!1;const n=this._code;n.push(ti.call);this._emitULEB128(PsWasmCompiler.#qe.atan2);this._emitF64Const(PsWasmCompiler.#Se);n.push(ti.f64_mul);this._emitLocalTee(a);this._emitF64Const(0);n.push(ti.f64_lt,ti.if,ai);this._emitLocalGet(a);this._emitF64Const(360);n.push(ti.f64_add,ti.else);this._emitLocalGet(a);n.push(ti.end);return!0}finally{this._releaseLocal(a)}}_compileBitwiseNode(e,t,a){if(!this._compileBitwiseOperandI32(a))return!1;if(!this._compileBitwiseOperandI32(t))return!1;const n=this._code;switch(e){case $s.and:n.push(ti.i32_and);break;case $s.or:n.push(ti.i32_or);break;case $s.xor:n.push(ti.i32_xor);break;default:return!1}n.push(ti.f64_convert_i32_s);return!0}_compileBitwiseOperandI32(e){if(e.valueType===Js)return this._compileNodeAsBoolI32(e);if(!this._compileNode(e))return!1;this._code.push(ti.i32_trunc_f64_s);return!0}_compileStandardBinaryNode(e,t,a){if(t!==a||t.type===rr||t.type===ir||t.shared){if(!this._compileNode(a))return!1;if(!this._compileNode(t))return!1}else{const e=this._allocLocal();try{if(!this._compileNode(t))return!1;this._emitLocalTee(e);this._emitLocalGet(e)}finally{this._releaseLocal(e)}}const n=this._code;switch(e){case $s.add:n.push(ti.f64_add);break;case $s.sub:n.push(ti.f64_sub);break;case $s.mul:n.push(ti.f64_mul);break;case $s.exp:n.push(ti.call);this._emitULEB128(PsWasmCompiler.#qe.pow);break;case $s.eq:n.push(ti.f64_eq,ti.f64_convert_i32_s);break;case $s.ne:n.push(ti.f64_ne,ti.f64_convert_i32_s);break;case $s.lt:n.push(ti.f64_lt,ti.f64_convert_i32_s);break;case $s.le:n.push(ti.f64_le,ti.f64_convert_i32_s);break;case $s.gt:n.push(ti.f64_gt,ti.f64_convert_i32_s);break;case $s.ge:n.push(ti.f64_ge,ti.f64_convert_i32_s);break;case $s.min:n.push(ti.f64_min);break;case $s.max:n.push(ti.f64_max);break;default:return!1}return!0}_compileBinaryNode(e){const{op:t,first:a,second:n}=e;return t===$s.bitshift?this._compileBitshiftNode(a,n):t===$s.div?this._compileSafeDivNode(a,n):t===$s.idiv?this._compileSafeIdivNode(a,n):t===$s.mod?this._compileModNode(a,n):t===$s.atan?this._compileAtanNode(a,n):t===$s.and||t===$s.or||t===$s.xor?this._compileBitwiseNode(t,a,n):this._compileStandardBinaryNode(t,a,n)}_compileNodeAsBoolI32(e){if(e.type===lr){const t=PsWasmCompiler.#ye.get(e.op);if(void 0!==t){if(!this._compileNode(e.second))return!1;if(!this._compileNode(e.first))return!1;this._code.push(t);return!0}if(e.valueType===Js&&(e.op===$s.and||e.op===$s.or||e.op===$s.xor)){if(!this._compileNodeAsBoolI32(e.second))return!1;if(!this._compileNodeAsBoolI32(e.first))return!1;switch(e.op){case $s.and:this._code.push(ti.i32_and);break;case $s.or:this._code.push(ti.i32_or);break;case $s.xor:this._code.push(ti.i32_xor)}return!0}}if(e.type===or&&e.op===$s.not&&e.valueType===Js){if(!this._compileNodeAsBoolI32(e.operand))return!1;this._code.push(ti.i32_eqz);return!0}if(!this._compileNode(e))return!1;if(e.valueType===Js)this._code.push(ti.i32_trunc_f64_s);else{this._emitF64Const(0);this._code.push(ti.f64_ne)}return!0}_compileTernaryNode(e){if(!this._compileNodeAsBoolI32(e.cond))return!1;this._code.push(ti.if,ai);if(!this._compileNode(e.then))return!1;this._code.push(ti.else);if(!this._compileNode(e.otherwise))return!1;this._code.push(ti.end);return!0}compile(e){const t=(new PSStackToTree).evaluate(e,this._nIn);if(!t||t.length0?[[...unsignedLEB128(s),ai]]:[]),l=o.length+a.length,f=new Uint8Array(section(li,vec([[...unsignedLEB128(l),...o,...a]]))),c=PsWasmCompiler.#Te,h=PsWasmCompiler.#xe,u=PsWasmCompiler.#Ce,m=PsWasmCompiler.#Ie,p=PsWasmCompiler.#Fe,d=c.length+i.length+h.length+u.length+m.length+p.length+f.length,g=new Uint8Array(d);let b=0;g.set(c,b);b+=c.length;g.set(i,b);b+=i.length;g.set(h,b);b+=h.length;g.set(u,b);b+=u.length;g.set(m,b);b+=m.length;g.set(p,b);b+=p.length;g.set(f,b);return g}}function buildPostScriptWasmFunction(e,t,a){const n=function compilePostScriptToWasm(e,t,a){return new PsWasmCompiler(t,a).compile(parsePostScriptFunction(e))}(e,t,a);if(!n)return null;try{return function _makeWrapper(e,t,a){const{fn:n,mem:s}=e,r=new Float64Array(s.buffer,0,a);let i;switch(a){case 1:i=(e,t)=>{e[t]=r[0]};break;case 2:i=(e,t)=>{e[t]=r[0];e[t+1]=r[1]};break;case 3:i=(e,t)=>{e[t]=r[0];e[t+1]=r[1];e[t+2]=r[2]};break;case 4:i=(e,t)=>{e[t]=r[0];e[t+1]=r[1];e[t+2]=r[2];e[t+3]=r[3]};break;default:i=(e,t)=>{for(let n=0;n{n(e[t]);i(a,s)};case 2:return(e,t,a,s)=>{n(e[t],e[t+1]);i(a,s)};case 3:return(e,t,a,s)=>{n(e[t],e[t+1],e[t+2]);i(a,s)};case 4:return(e,t,a,s)=>{n(e[t],e[t+1],e[t+2],e[t+3]);i(a,s)};default:{const e=new Float64Array(t);return(a,s,r,o)=>{for(let n=0;n>1,a.length>>1)}catch{return null}}class BaseLocalCache{constructor(e){this._onlyRefs=!0===e?.onlyRefs;if(!this._onlyRefs){this._nameRefMap=new Map;this._imageMap=new Map}this._imageCache=new RefSetCache}getByName(e){this._onlyRefs&&unreachable("Should not call `getByName` method.");const t=this._nameRefMap.get(e);return t?this.getByRef(t):this._imageMap.get(e)||null}getByRef(e){return this._imageCache.get(e)||null}set(e,t,a){unreachable("Abstract method `set` called.")}}class LocalImageCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalImageCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalColorSpaceCache extends BaseLocalCache{set(e=null,t=null,a){if("string"!=typeof e&&!t)throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');if(t){if(this._imageCache.has(t))return;null!==e&&this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalFunctionCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalFunctionCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class LocalGStateCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalGStateCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalTilingPatternCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalTilingPatternCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class RegionalImageCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('RegionalImageCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class GlobalColorSpaceCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('GlobalColorSpaceCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}clear(){this._imageCache.clear()}}class GlobalImageCache{static NUM_PAGES_THRESHOLD=2;static MIN_IMAGES_TO_CACHE=10;static MAX_BYTE_SIZE=5e7;#He=new RefSet;constructor(){this._refCache=new RefSetCache;this._imageCache=new RefSetCache}get#Be(){let e=0;for(const t of this._imageCache)e+=t.byteSize;return e}get#De(){return!(this._imageCache.size+e):null}class PDFFunction{static getSampleArray(e,t,a,n){let s=t;for(const t of e)s*=t;const r=new Array(s);let i=0,o=0;const l=1/(2**a-1),f=n.getBytes((s*a+7)/8);let c=0;for(let e=0;e>i)*l;o&=(1<0?r[h-1]:a[0],m=h{PsJsCompiler.execute(r,e,t,a,n)}:PSStackBasedInterpreter.build(s,t,a)}(r,n,s)}}function isPDFFunction(e){let t;if(e instanceof Dict)t=e;else{if(!(e instanceof BaseStream))return!1;t=e.dict}return t.has("FunctionType")}const di=["BN","BN","BN","BN","BN","BN","BN","BN","BN","S","B","S","WS","B","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","B","B","B","S","WS","ON","ON","ET","ET","ET","ON","ON","ON","ON","ON","ES","CS","ES","CS","CS","EN","EN","EN","EN","EN","EN","EN","EN","EN","EN","CS","ON","ON","ON","ON","ON","ON","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","ON","ON","ON","ON","ON","ON","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","ON","ON","ON","ON","BN","BN","BN","BN","BN","BN","B","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","BN","CS","ON","ET","ET","ET","ET","ON","ON","ON","ON","L","ON","ON","BN","ON","ON","ET","ET","EN","EN","ON","L","ON","ON","ON","EN","L","ON","ON","ON","ON","ON","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","ON","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","L","ON","L","L","L","L","L","L","L","L"],gi=["AN","AN","AN","AN","AN","AN","ON","ON","AL","ET","ET","AL","CS","AL","ON","ON","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","AL","AL","","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","NSM","AN","AN","AN","AN","AN","AN","AN","AN","AN","AN","ET","AN","AN","AL","AL","AL","NSM","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","AL","NSM","NSM","NSM","NSM","NSM","NSM","NSM","AN","ON","NSM","NSM","NSM","NSM","NSM","NSM","AL","AL","NSM","NSM","ON","NSM","NSM","NSM","NSM","AL","AL","EN","EN","EN","EN","EN","EN","EN","EN","EN","EN","AL","AL","AL","AL","AL","AL"];function isOdd(e){return!!(1&e)}function isEven(e){return!(1&e)}function findUnequal(e,t,a){let n,s;for(n=t,s=e.length;n4){n=!0;t=0}else{n=!1;t=1}const l=[];for(r=0;r=0&&"ET"===wi[e];--e)wi[e]="EN";for(let e=r+1;e=0;e--){const a=wi[e];if("L"===a){t="L";break}if("R"===a||"EN"===a||"AN"===a){t="R";break}}let a=h;for(let t=e;tp&&isOdd(p)&&(g=p)}for(p=d;p>=g;--p){let e=-1;for(r=0,i=l.length;r=0){reverseValues(bi,e,r);e=-1}}else e<0&&(e=r);e>=0&&reverseValues(bi,e,l.length)}for(r=0,i=bi.length;r"!==e||(bi[r]="")}return createBidiText(bi.join(""),n)}const ji={style:"normal",weight:"normal"},ki={style:"normal",weight:"bold"},yi={style:"italic",weight:"normal"},qi={style:"italic",weight:"bold"},vi=new Map([["Times-Roman",{local:["Times New Roman","Times-Roman","Times","Liberation Serif","Nimbus Roman","Nimbus Roman L","Tinos","Thorndale","TeX Gyre Termes","FreeSerif","Linux Libertine O","Libertinus Serif","PT Astra Serif","DejaVu Serif","Bitstream Vera Serif","Ubuntu"],style:ji,ultimate:"serif"}],["Times-Bold",{alias:"Times-Roman",style:ki,ultimate:"serif"}],["Times-Italic",{alias:"Times-Roman",style:yi,ultimate:"serif"}],["Times-BoldItalic",{alias:"Times-Roman",style:qi,ultimate:"serif"}],["Helvetica",{local:["Helvetica","Helvetica Neue","Arial","Arial Nova","Liberation Sans","Arimo","Nimbus Sans","Nimbus Sans L","A030","TeX Gyre Heros","FreeSans","DejaVu Sans","Albany","Bitstream Vera Sans","Arial Unicode MS","Microsoft Sans Serif","Apple Symbols","Cantarell"],path:"LiberationSans-Regular.ttf",style:ji,ultimate:"sans-serif"}],["Helvetica-Bold",{alias:"Helvetica",path:"LiberationSans-Bold.ttf",style:ki,ultimate:"sans-serif"}],["Helvetica-Oblique",{alias:"Helvetica",path:"LiberationSans-Italic.ttf",style:yi,ultimate:"sans-serif"}],["Helvetica-BoldOblique",{alias:"Helvetica",path:"LiberationSans-BoldItalic.ttf",style:qi,ultimate:"sans-serif"}],["Courier",{local:["Courier","Courier New","Liberation Mono","Nimbus Mono","Nimbus Mono L","Cousine","Cumberland","TeX Gyre Cursor","FreeMono","Linux Libertine Mono O","Libertinus Mono"],style:ji,ultimate:"monospace"}],["Courier-Bold",{alias:"Courier",style:ki,ultimate:"monospace"}],["Courier-Oblique",{alias:"Courier",style:yi,ultimate:"monospace"}],["Courier-BoldOblique",{alias:"Courier",style:qi,ultimate:"monospace"}],["ArialBlack",{local:["Arial Black"],style:{style:"normal",weight:"900"},fallback:"Helvetica-Bold"}],["ArialBlack-Bold",{alias:"ArialBlack"}],["ArialBlack-Italic",{alias:"ArialBlack",style:{style:"italic",weight:"900"},fallback:"Helvetica-BoldOblique"}],["ArialBlack-BoldItalic",{alias:"ArialBlack-Italic"}],["ArialNarrow",{local:["Arial Narrow","Liberation Sans Narrow","Helvetica Condensed","Nimbus Sans Narrow","TeX Gyre Heros Cn"],style:ji,fallback:"Helvetica"}],["ArialNarrow-Bold",{alias:"ArialNarrow",style:ki,fallback:"Helvetica-Bold"}],["ArialNarrow-Italic",{alias:"ArialNarrow",style:yi,fallback:"Helvetica-Oblique"}],["ArialNarrow-BoldItalic",{alias:"ArialNarrow",style:qi,fallback:"Helvetica-BoldOblique"}],["Calibri",{local:["Calibri","Carlito"],style:ji,fallback:"Helvetica"}],["Calibri-Bold",{alias:"Calibri",style:ki,fallback:"Helvetica-Bold"}],["Calibri-Italic",{alias:"Calibri",style:yi,fallback:"Helvetica-Oblique"}],["Calibri-BoldItalic",{alias:"Calibri",style:qi,fallback:"Helvetica-BoldOblique"}],["Wingdings",{local:["Wingdings","URW Dingbats"],style:ji}],["Wingdings-Regular",{alias:"Wingdings"}],["Wingdings-Bold",{alias:"Wingdings"}],["ËÎÌå",{local:["SimSun","SimSun Regular","NSimSun"],style:ji,ultimate:"serif"}],["ºÚÌå",{local:["SimHei","SimHei Regular"],style:ji,ultimate:"sans-serif"}],["¿¬Ìå",{local:["KaiTi","SimKai","SimKai Regular"],style:ji,ultimate:"sans-serif"}],["·ÂËÎ",{local:["FangSong","SimFang","SimFang Regular"],style:ji,ultimate:"serif"}],["¿¬Ìå_GB2312",{alias:"¿¬Ìå"}],["·ÂËÎ_GB2312",{alias:"·ÂËÎ"}],["Á¥Êé",{local:["SimLi","SimLi Regular"],style:ji,ultimate:"serif"}],["ÐÂËÎ",{alias:"ËÎÌå"}]]),Si=new Map([["Arial-Black","ArialBlack"]]);function getFamilyName(e){const t=new Set(["thin","extralight","ultralight","demilight","semilight","light","book","regular","normal","medium","demibold","semibold","bold","extrabold","ultrabold","black","heavy","extrablack","ultrablack","roman","italic","oblique","ultracondensed","extracondensed","condensed","semicondensed","normal","semiexpanded","expanded","extraexpanded","ultraexpanded","bolditalic"]);return e.split(/[- ,+]+/g).filter(e=>!t.has(e.toLowerCase())).join(" ")}function generateFont({alias:e,local:t,path:a,fallback:n,style:s,ultimate:r},i,o,l=!0,f=!0,c=""){const h={style:null,ultimate:null};if(t){const e=c?` ${c}`:"";for(const a of t)i.push(`local(${a}${e})`)}if(e){const t=vi.get(e),r=c||function getStyleToAppend(e){switch(e){case ki:return"Bold";case yi:return"Italic";case qi:return"Bold Italic";default:if("bold"===e?.weight)return"Bold";if("italic"===e?.style)return"Italic"}return""}(s);Object.assign(h,generateFont(t,i,o,l&&!n,f&&!a,r))}s&&(h.style=s);r&&(h.ultimate=r);if(l&&n){const e=vi.get(n),{ultimate:t}=generateFont(e,i,o,l,f&&!a,c);h.ultimate||=t}f&&a&&o&&i.push(`url(${o}${a})`);return h}function getFontSubstitution(e,t,a,n,s,r){if(n.startsWith("InvalidPDFjsFont_"))return null;"TrueType"!==r&&"Type1"!==r||!/^[A-Z]{6}\+/.test(n)||(n=n.slice(7));const i=n=normalizeFontName(n);let o=e.get(i);if(o)return o;let l=vi.get(n);if(!l)for(const[e,t]of Si)if(n.startsWith(e)){n=`${t}${n.substring(e.length)}`;l=vi.get(n);break}let f=!1;if(!l){l=vi.get(s);f=!0}const c=`${t.getDocId()}_s${t.createFontId()}`;if(!l){if(!validateFontName(n)){warn(`Cannot substitute the font because of its name: ${n}`);e.set(i,null);return null}const t=/bold/gi.test(n),a=/oblique|italic/gi.test(n),s=t&&a&&qi||t&&ki||a&&yi||ji;o={css:`"${getFamilyName(n)}",${c}`,guessFallback:!0,loadedName:c,baseFontName:n,src:`local(${n})`,style:s};e.set(i,o);return o}const h=[];f&&validateFontName(n)&&h.push(`local(${n})`);const{style:u,ultimate:m}=generateFont(l,h,a),p=null===m,d=p?"":`,${m}`;o={css:`"${getFamilyName(n)}",${c}${d}`,guessFallback:p,loadedName:c,baseFontName:n,src:h.join(","),style:u};e.set(i,o);return o}const Ai=3285377520,xi=4294901760,Ci=65535;class MurmurHash3_64{constructor(e){this.h1=e?4294967295&e:Ai;this.h2=e?4294967295&e:Ai}update(e){let t,a;if("string"==typeof e){t=new Uint8Array(2*e.length);a=0;for(let n=0,s=e.length;n>>8;t[a++]=255&s}}}else{if(!ArrayBuffer.isView(e))throw new Error("Invalid data format, must be a string or TypedArray.");t=e.slice();a=t.byteLength}const n=a>>2,s=a-4*n,r=new Uint32Array(t.buffer,0,n);let i=0,o=0,l=this.h1,f=this.h2;const c=3432918353,h=461845907,u=11601,m=13715;for(let e=0;e>>17;i=i*h&xi|i*m&Ci;l^=i;l=l<<13|l>>>19;l=5*l+3864292196}else{o=r[e];o=o*c&xi|o*u&Ci;o=o<<15|o>>>17;o=o*h&xi|o*m&Ci;f^=o;f=f<<13|f>>>19;f=5*f+3864292196}i=0;switch(s){case 3:i^=t[4*n+2]<<16;case 2:i^=t[4*n+1]<<8;case 1:i^=t[4*n];i=i*c&xi|i*u&Ci;i=i<<15|i>>>17;i=i*h&xi|i*m&Ci;1&n?l^=i:f^=i}this.h1=l;this.h2=f}hexdigest(){let e=this.h1,t=this.h2;e^=t>>>1;e=3981806797*e&xi|36045*e&Ci;t=4283543511*t&xi|(2950163797*(t<<16|e>>>16)&xi)>>>16;e^=t>>>1;e=444984403*e&xi|60499*e&Ci;t=3301882366*t&xi|(3120437893*(t<<16|e>>>16)&xi)>>>16;e^=t>>>1;return(e>>>0).toString(16).padStart(8,"0")+(t>>>0).toString(16).padStart(8,"0")}}class PDFImage{constructor({xref:e,res:t,image:a,isInline:n=!1,smask:s=null,mask:r=null,isMask:i=!1,pdfFunctionFactory:o,globalColorSpaceCache:l,localColorSpaceCache:f}){this.image=a;const c=a.dict,h=c.get("F","Filter");let u;if(h instanceof Name)u=h.name;else if(Array.isArray(h)){const t=e.fetchIfRef(h[0]);t instanceof Name&&(u=t.name)}switch(u){case"JPXDecode":({width:a.width,height:a.height,componentsCount:a.numComps,bitsPerComponent:a.bitsPerComponent}=JpxImage.parseImageProperties(a.stream));a.stream.reset();const e=ImageResizer.getReducePowerForJPX(a.width,a.height,a.numComps);this.jpxDecoderOptions={numComponents:0,isIndexedColormap:!1,smaskInData:c.has("SMaskInData"),reducePower:e};if(e){const t=2**e;a.width=Math.ceil(a.width/t);a.height=Math.ceil(a.height/t)}break;case"JBIG2Decode":a.bitsPerComponent=1;a.numComps=1}let m=c.get("W","Width"),p=c.get("H","Height");if(Number.isInteger(a.width)&&a.width>0&&Number.isInteger(a.height)&&a.height>0&&(a.width!==m||a.height!==p)){warn("PDFImage - using the Width/Height of the image data, rather than the image dictionary.");m=a.width;p=a.height}else{const e="number"==typeof m&&m>0,t="number"==typeof p&&p>0;if(!e||!t){if(!a.fallbackDims)throw new FormatError(`Invalid image width: ${m} or height: ${p}`);warn("PDFImage - using the Width/Height of the parent image, for SMask/Mask data.");e||(m=a.fallbackDims.width);t||(p=a.fallbackDims.height)}}this.width=m;this.height=p;this.interpolate=c.get("I","Interpolate");this.imageMask=c.get("IM","ImageMask")||!1;this.matte=c.get("Matte")||!1;let d=a.bitsPerComponent;if(!d){d=c.get("BPC","BitsPerComponent");if(!d){if(!this.imageMask)throw new FormatError(`Bits per component missing in image: ${this.imageMask}`);d=1}}this.bpc=d;if(this.imageMask)this.numComps=1;else{let s=c.getRaw("CS")||c.getRaw("ColorSpace");const r=!!s;if(r)this.jpxDecoderOptions?.smaskInData&&(s=Name.get("DeviceRGBA"));else if(this.jpxDecoderOptions)s=Name.get("DeviceRGBA");else switch(a.numComps){case 1:s=Name.get("DeviceGray");break;case 3:s=Name.get("DeviceRGB");break;case 4:s=Name.get("DeviceCMYK");break;default:throw new Error(`Images with ${a.numComps} color components not supported.`)}this.colorSpace=ColorSpaceUtils.parse({cs:s,xref:e,resources:n?t:null,pdfFunctionFactory:o,globalColorSpaceCache:l,localColorSpaceCache:f});this.numComps=this.colorSpace.numComps;if(this.jpxDecoderOptions){this.jpxDecoderOptions.numComponents=r?this.numComps:0;this.jpxDecoderOptions.isIndexedColormap="Indexed"===this.colorSpace.name}}this.decode=c.getArray("D","Decode");this.needsDecode=!1;if(this.decode&&(this.colorSpace&&!this.colorSpace.isDefaultDecode(this.decode,d)||i&&!ColorSpace.isDefaultDecode(this.decode,1))){this.needsDecode=!0;const e=(1<0,l=(n+7>>3)*s,f=await e.getImageData(l),c=1===n&&1===s&&o===(0===f.length||!!(128&f[0]));if(c)return{isSingleOpaquePixel:c};if(t){if(ImageResizer.needsToBeResized(n,s)){const e=new Uint8ClampedArray(n*s*4);convertBlackAndWhiteToRGBA({src:f,dest:e,width:n,height:s,nonBlackColor:0,inverseDecode:o});return ImageResizer.createImage({kind:O,data:e,width:n,height:s,interpolate:r})}const e=new OffscreenCanvas(n,s),t=e.getContext("2d"),a=t.createImageData(n,s);convertBlackAndWhiteToRGBA({src:f,dest:a.data,width:n,height:s,nonBlackColor:0,inverseDecode:o});t.putImageData(a,0,0);return{data:null,width:n,height:s,interpolate:r,bitmap:e.transferToImageBitmap()}}const h=f.byteLength;let u;if(e instanceof DecodeStream&&(!o||l===h))u=f;else if(o){u=new Uint8Array(l);u.set(f);u.fill(255,h)}else u=new Uint8Array(f);if(o)for(let e=0;e>7&1;i[u+1]=h>>6&1;i[u+2]=h>>5&1;i[u+3]=h>>4&1;i[u+4]=h>>3&1;i[u+5]=h>>2&1;i[u+6]=h>>1&1;i[u+7]=1&h;u+=8}if(u>=1}}}}else{let a=0;h=0;for(u=0,c=r;u>n;s<0?s=0:s>f&&(s=f);i[u]=s;h&=(1<this.smask.fillGrayBuffer(e,{...n,destWidth:t,destHeight:a});else if(this.mask)if(this.mask instanceof PDFImage)r=(e,n)=>this.mask.fillGrayBuffer(e,{...n,invertOutput:!0,destWidth:t,destHeight:a});else{if(!Array.isArray(this.mask))throw new FormatError("Unknown mask format.");r=(e,{maxRows:a,offset:n,stride:r})=>{for(let i=0,o=t*a;ithis.mask[r+1]){t=255;break}}e[i*r+n]=t}}}else r=(e,{maxRows:a,offset:n,stride:s})=>{for(let r=0,i=t*a;r>3,c=t&&ImageResizer.needsToBeResized(a,n);if(!this.smask&&!this.mask&&"DeviceRGBA"===this.colorSpace.name){s.kind=O;const e=s.data=await this.getImageBytes(o*i*4,{internal:t&&c});return t?c?ImageResizer.createImage(s,!1):this.createBitmap(O,a,n,e):s}if(!e){let e;"DeviceGray"===this.colorSpace.name&&1===l?e=F:"DeviceRGB"!==this.colorSpace.name||8!==l||this.needsDecode||(e=T);if(e&&!this.smask&&!this.mask&&a===i&&n===o){const r=await this.#Me(i,o);if(r)return r;const l=await this.getImageBytes(o*f,{internal:t&&c});if(t)return c?ImageResizer.createImage({data:l,kind:e,width:a,height:n,interpolate:this.interpolate},this.needsDecode):this.createBitmap(e,i,o,l);s.kind=e;s.data=l;if(this.needsDecode){assert(e===F,"PDFImage.createImageData: The image must be grayscale.");const t=s.data;for(let e=0,a=t.length;e>3,u=await this.getImageBytes(f*h,{internal:!0}),m=this.getComponents(u),p=t??l,d=a??f,g=p!==l||d!==f,b=void 0===s?d:Math.min(d,s);let w=l,j=0,k=null;if(g){w=p;j=f/d;const e=l/p;k=new Uint32Array(p);for(let t=0;t0&&n[0].count++}class TimeSlotManager{static TIME_SLOT_DURATION_MS=20;static CHECK_TIME_EVERY=100;constructor(){this.reset()}check(){if(++this.checked!isFinite(e))&&(m=null);l.has("OC")&&(h=await this.parseMarkedContentProps(l.get("OC"),e));void 0!==h&&n.addOp(xt,["OC",h]);const p=l.get("Group");let d;const g=[f&&new Float32Array(f),!p&&m||null],b=l.get("Resources");if(p){u={matrix:f,bbox:m,smask:a,isolated:!1,knockout:!1,needsIsolation:!1};let t=null;if(isName(p.get("S"),"Transparency")){u.isolated=p.get("I")||!1;u.knockout=p.get("K")||!1;if(p.has("CS")){const a=this._getColorSpace(p.getRaw("CS"),e,i);t=a instanceof ColorSpace?a:await this._handleColorSpace(a)}}if(a?.backdrop){t||=ColorSpaceUtils.rgb;a.backdrop=t.getRgbHex(a.backdrop,0)}d=new CheckedOperatorList}else{d=n;n.addOp(Tt,g)}await this.getOperatorList({stream:t,task:s,resources:b instanceof Dict?b:e,operatorList:d,initialState:r,prevRefs:o});if(p){u.needsIsolation=d.needsIsolation||!!a;n.addOp(Rt,[u]);n.addOp(Tt,g);n.addOpList(d);n.addOp(Ot,[]);n.addOp(Ht,[u])}else n.addOp(Ot,[]);void 0!==h&&n.addOp(Ct,[])}_sendImgData(e,t,a=!1){const n=t?[t.bitmap||t.data.buffer]:null;return this.parsingType3Font||a?this.handler.send("commonobj",[e,"Image",t],n):this.handler.send("obj",[e,this.pageIndex,"Image",t],n)}async buildPaintImageXObject({resources:e,image:t,isInline:a=!1,operatorList:n,cacheKey:s,localImageCache:r,localColorSpaceCache:i}){const{maxImageSize:o,ignoreErrors:l,isOffscreenCanvasSupported:f}=this.options,{dict:c}=t,h=c.objId,u=c.get("W","Width"),m=c.get("H","Height");if(!u||"number"!=typeof u||!m||"number"!=typeof m){warn("Image dimensions are missing, or not numbers.");return}if(-1!==o&&u*m>o){const e="Image exceeded maximum allowed size and was removed.";if(!l)throw new Error(e);warn(e);return}let p;c.has("OC")&&(p=await this.parseMarkedContentProps(c.get("OC"),e));let d,g,b;if(c.get("IM","ImageMask")||!1){d=await PDFImage.createMask({image:t,isOffscreenCanvasSupported:f&&!this.parsingType3Font});if(d.isSingleOpaquePixel){g=Ut;b=[];n.addImageOps(g,b,p);if(s){const e={fn:g,args:b,optionalContent:p};r.set(s,h,e);h&&this._regionalImageCache.set(null,h,e)}return}if(this.parsingType3Font){b=function compileType3Glyph({data:e,width:t,height:a}){if(t>1e3||a>1e3)return null;const n=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),s=t+1,r=new Uint8Array(s*(a+1));let i,o,l;const f=t+7&-8,c=new Uint8Array(f*a);let h=0;for(const t of e){let e=128;for(;e>0;){c[h++]=t&e?0:255;e>>=1}}let u=0;h=0;if(0!==c[h]){r[0]=1;++u}for(o=1;o>2)+(c[h+1]?4:0)+(c[h-f+1]?8:0);if(n[e]){r[l+o]=n[e];++u}h++}if(c[h-f]!==c[h]){r[l+o]=c[h]?2:4;++u}if(u>1e3)return null}h=f*(a-1);l=i*s;if(0!==c[h]){r[l]=8;++u}for(o=1;o1e3)return null;const m=new Int32Array([0,s,-1,0,-s,0,0,0,1]),p=[],{a:d,b:g,c:b,d:w,e:j,f:k}=(new DOMMatrix).scaleSelf(1/t,-1/a).translateSelf(0,-a);for(i=0;u&&i<=a;i++){let e=i*s;const a=e+t;for(;e>4;r[e]&=f>>2|f<<2}n=e%s;o=e/s|0;p.push($t,d*n+b*o+j,g*n+w*o+k);r[e]||--u}while(l!==e);--i}return[Gt,[new Float32Array(p)],new Float32Array([0,0,t,a])]}(d);if(b){n.addImageOps(Xt,b,p);return}warn("Cannot compile Type3 glyph.");n.addImageOps(Mt,[d],p);return}const e=`mask_${this.idFactory.createObjId()}`;n.addDependency(e);d.dataLen=d.bitmap?d.width*d.height*4:d.data.length;this._sendImgData(e,d);g=Mt;b=[{data:e,width:d.width,height:d.height,interpolate:d.interpolate,count:1}];n.addImageOps(g,b,p);if(s){const t={objId:e,fn:g,args:b,optionalContent:p};r.set(s,h,t);h&&this._regionalImageCache.set(null,h,t)}return}const w=c.has("SMask")||c.has("Mask");if(a&&u+m<200&&!w){try{const s=new PDFImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:i});d=await s.createImageData(!0,!1);n.addImageOps(Et,[d],p)}catch(e){const t=`Unable to decode inline image: "${e}".`;if(!l)throw new Error(t);warn(t)}return}let j=`img_${this.idFactory.createObjId()}`,k=!1,y=null;if(this.parsingType3Font)j=`${this.idFactory.getDocId()}_type3_${j}`;else if(s&&h){k=this.globalImageCache.shouldCache(h,this.pageIndex);if(k){assert(!a,"Cannot cache an inline image globally.");j=`${this.idFactory.getDocId()}_${j}`}}n.addDependency(j);g=Pt;b=[j,u,m];n.addImageOps(g,b,p,w);if(k){y={objId:j,fn:g,args:b,optionalContent:p,hasMask:w,byteSize:0};if(this.globalImageCache.hasDecodeFailed(h)){this.globalImageCache.setData(h,y);this._sendImgData(j,null,k);return}if(u*m>25e4||w){const e=await this.handler.sendWithPromise("commonobj",[j,"CopyLocalImage",{imageRef:h}]);if(e){this.globalImageCache.setData(h,y);this.globalImageCache.addByteSize(h,e);return}}}PDFImage.buildImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:i}).then(async e=>{d=await e.createImageData(!1,f);d.dataLen=d.bitmap?d.width*d.height*4:d.data.length;d.ref=h;k&&this.globalImageCache.addByteSize(h,d.dataLen);return this._sendImgData(j,d,k)}).catch(e=>{warn(`Unable to decode image "${j}": "${e}".`);h&&this.globalImageCache.addDecodeFailed(h);return this._sendImgData(j,null,k)});if(s){const e={objId:j,fn:g,args:b,optionalContent:p,hasMask:w};r.set(s,h,e);if(h){this._regionalImageCache.set(null,h,e);if(k){assert(y,"The global cache-data must be available.");this.globalImageCache.setData(h,y)}}}}handleSMask(e,t,a,n,s,r,i){const o=e.get("G"),l={subtype:e.get("S").name,backdrop:e.get("BC")},f=e.get("TR");if(isPDFFunction(f)){const e=this._pdfFunctionFactory.create(f),t=new Uint8Array(256),a=new Float32Array(1);for(let n=0;n<256;n++){a[0]=n/255;e(a,0,a,0);t[n]=255*a[0]|0}l.transferMap=t}return this.buildFormXObject(t,o,l,a,n,s.state.clone({newPath:!0}),r,i)}handleTransferFunction(e){let t;if(Array.isArray(e)){t=e;e.length>1&&e.every(t=>t===e[0])&&(t=[e[0]])}else{if(!isPDFFunction(e))return null;t=[e]}const a=[];let n=0,s=0;for(const e of t){const t=this.xref.fetchIfRef(e);n++;if(isName(t,"Identity")){a.push(null);continue}if(!isPDFFunction(t))return null;const r=this._pdfFunctionFactory.create(t),i=new Uint8Array(256),o=new Float32Array(1);for(let e=0;e<256;e++){o[0]=e/255;r(o,0,o,0);i[e]=255*o[0]|0}a.push(i);s++}return 1!==n&&4!==n||0===s?null:a}handleTilingType(e,t,a,n,s,r,i,o){const l=new CheckedOperatorList,f=Dict.merge({xref:this.xref,dictArray:[s.get("Resources"),a]});return this.getOperatorList({stream:n,task:i,resources:f,operatorList:l}).then(function(){const a=l.getIR(),{needsIsolation:n}=l,i=getTilingPatternIR(a,s,t,n);r.addDependencies(l.dependencies);r.addOp(e,i);s.objId&&o.set(null,s.objId,{operatorListIR:a,needsIsolation:n,dict:s})}).catch(e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`handleTilingType - ignoring pattern: "${e}".`)}})}async handleSetFont(e,t,a,n,s,r,i=null,o=null){const l=t?.[0]instanceof Name?t[0].name:null,f=await this.loadFont(l,a,e,s,i,o);f.font.isType3Font&&n.addDependencies(f.type3Dependencies);r.font=f.font;f.send(this.handler);return f.loadedName}handleText(e,t){const a=t.font,n=a.charsToGlyphs(e);if(a.data){(!!(t.textRenderingMode&C)||"Pattern"===t.fillColorSpace.name||"Pattern"===t.strokeColorSpace.name||a.disableFontFace)&&PartialEvaluator.buildFontPaths(a,n,this.handler,this.options)}return n}ensureStateFont(e){if(e.font)return;const t=new FormatError("Missing setFont (Tf) operator before text rendering operator.");if(!this.options.ignoreErrors)throw t;warn(`ensureStateFont: "${t}".`)}async setGState({resources:e,gState:t,operatorList:a,cacheKey:n,task:s,stateManager:r,localGStateCache:i,localColorSpaceCache:o,seenRefs:l}){const f=t.objId;let c=!0;const h=[];let u=Promise.resolve();for(const[n,i]of t)switch(n){case"Type":break;case"LW":if("number"!=typeof i){warn(`Invalid LW (line width): ${i}`);break}h.push([n,Math.abs(i)]);break;case"LC":case"LJ":case"ML":case"D":case"RI":case"FL":case"CA":case"ca":h.push([n,i]);break;case"Font":c=!1;u=u.then(()=>this.handleSetFont(e,null,i[0],a,s,r.state).then(function(e){a.addDependency(e);h.push([n,[e,i[1]]])}));break;case"BM":h.push([n,normalizeBlendMode(i)]);break;case"SMask":if(isName(i,"None")){h.push([n,!1]);break}if(i instanceof Dict){c=!1;u=u.then(()=>this.handleSMask(i,e,a,s,r,o,l));h.push([n,!0])}else warn("Unsupported SMask type");break;case"TR":const t=this.handleTransferFunction(i);h.push([n,t]);break;case"OP":case"op":case"OPM":case"BG":case"BG2":case"UCR":case"UCR2":case"TR2":case"HT":case"SM":case"SA":case"AIS":case"TK":info("graphic state operator "+n);break;default:info("Unknown graphic state operator "+n)}await u;h.length>0&&a.addOp(je,[h]);c&&i.set(n,f,h)}loadFont(e,t,a,n,s=null,r=null){const errorFont=async()=>new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Font "${e}" is not available.`),dict:t});let i;if(t)t instanceof Ref&&(i=t);else{const t=a.get("Font");t&&(i=t.getRaw(e))}if(i){if(this.type3FontRefs?.has(i))return errorFont();if(this.fontCache.has(i))return this.fontCache.get(i);try{t=this.xref.fetchIfRef(i)}catch(e){warn(`loadFont - lookup failed: "${e}".`)}}if(!(t instanceof Dict)){if(!this.options.ignoreErrors&&!this.parsingType3Font){warn(`Font "${e}" is not available.`);return errorFont()}warn(`Font "${e}" is not available -- attempting to fallback to a default font.`);t=s||PartialEvaluator.fallbackFontDict}if(t.cacheKey&&this.fontCache.has(t.cacheKey))return this.fontCache.get(t.cacheKey);const{promise:o,resolve:l}=Promise.withResolvers();let f;try{f=this.preEvaluateFont(t);f.cssFontInfo=r}catch(e){warn(`loadFont - preEvaluateFont failed: "${e}".`);return errorFont()}const{descriptor:c,hash:h}=f,u=i instanceof Ref;let m;if(h&&c instanceof Dict){const e=c.fontAliases||=Object.create(null);if(e[h]){const t=e[h].aliasRef;if(u&&t&&this.fontCache.has(t)){this.fontCache.putAlias(i,t);return this.fontCache.get(i)}}else e[h]={fontID:this.idFactory.createFontId()};u&&(e[h].aliasRef=i);m=e[h].fontID}else m=this.idFactory.createFontId();assert(m?.startsWith("f"),'The "fontID" must be (correctly) defined.');if(u)this.fontCache.put(i,o);else{t.cacheKey=`cacheKey_${m}`;this.fontCache.put(t.cacheKey,o)}t.loadedName=`${this.idFactory.getDocId()}_${m}`;this.translateFont(f).then(async e=>{const s=new TranslatedFont({loadedName:t.loadedName,font:e,dict:t});if(e.isType3Font)try{await s.loadType3Data(this,a,n)}catch(e){throw new Error(`Type3 font load error: ${e}`)}l(s)}).catch(e=>{warn(`loadFont - translateFont failed: "${e}".`);l(new TranslatedFont({loadedName:t.loadedName,font:new ErrorFont(e?.message),dict:t}))});return o}buildPath(e,t,a){const{pathMinMax:n,pathBuffer:s}=a;switch(0|e){case Fe:{const e=a.currentPointX=t[0],r=a.currentPointY=t[1],i=t[2],o=t[3],l=e+i,f=r+o;0===i||0===o?s.push(Vt,e,r,$t,l,f,Qt):s.push(Vt,e,r,$t,l,r,$t,l,f,$t,e,f,Qt);Util.rectBoundingBox(e,r,l,f,n);break}case ve:{const e=a.currentPointX=t[0],r=a.currentPointY=t[1];s.push(Vt,e,r);Util.pointBoundingBox(e,r,n);break}case Se:{const e=a.currentPointX=t[0],r=a.currentPointY=t[1];s.push($t,e,r);Util.pointBoundingBox(e,r,n);break}case Ae:{const e=a.currentPointX,r=a.currentPointY,[i,o,l,f,c,h]=t;a.currentPointX=c;a.currentPointY=h;s.push(Yt,i,o,l,f,c,h);Util.bezierBoundingBox(e,r,i,o,l,f,c,h,n);break}case xe:{const e=a.currentPointX,r=a.currentPointY,[i,o,l,f]=t;a.currentPointX=l;a.currentPointY=f;s.push(Yt,e,r,i,o,l,f);Util.bezierBoundingBox(e,r,e,r,i,o,l,f,n);break}case Ce:{const e=a.currentPointX,r=a.currentPointY,[i,o,l,f]=t;a.currentPointX=l;a.currentPointY=f;s.push(Yt,i,o,l,f,l,f);Util.bezierBoundingBox(e,r,i,o,l,f,l,f,n);break}case Ie:s.push(Qt)}}_getColorSpace(e,t,a){return ColorSpaceUtils.parse({cs:e,xref:this.xref,resources:t,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:a,asyncIfNotCached:!0})}async _handleColorSpace(e){try{return await e}catch(e){if(e instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`_handleColorSpace - ignoring ColorSpace: "${e}".`);return null}throw e}}parseShading({shading:e,resources:t,localColorSpaceCache:a,localShadingPatternCache:n}){let s,r=n.get(e);if(r)return r;try{s=Pattern.parseShading(e,this.xref,t,this._pdfFunctionFactory,this.globalColorSpaceCache,a).getIR()}catch(t){if(t instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseShading - ignoring shading: "${t}".`);n.set(e,null);return null}throw t}r=`pattern_${this.idFactory.createObjId()}`;this.parsingType3Font&&(r=`${this.idFactory.getDocId()}_type3_${r}`);n.set(e,r);if(this.parsingType3Font){const e=function compilePatternInfo(e){let t,a=null,n=[],s=[],r=[],i=null,o=null;switch(e[0]){case"RadialAxial":t="axial"===e[1]?1:2;a=e[2];r=e[3];1===t?n.push(...e[4],...e[5]):n.push(e[4][0],e[4][1],e[6],e[5][0],e[5][1],e[7]);break;case"Mesh":t=3;i=e[1];n=e[2];s=e[3];a=e[6];o=e[7];break;default:throw new Error(`Unsupported pattern type: ${e[0]}`)}const l=Math.floor(n.length/2),f=Math.floor(s.length/4),c=r.length,h=new ArrayBuffer(20+8*l+4*f+8*c+(a?16:0)+(o?3:0)),u=new DataView(h),m=new Uint8Array(h);u.setUint8(PATTERN_INFO.KIND,t);u.setUint8(PATTERN_INFO.HAS_BBOX,a?1:0);u.setUint8(PATTERN_INFO.HAS_BACKGROUND,o?1:0);u.setUint8(PATTERN_INFO.SHADING_TYPE,i);u.setUint32(PATTERN_INFO.N_COORD,l,!0);u.setUint32(PATTERN_INFO.N_COLOR,f,!0);u.setUint32(PATTERN_INFO.N_STOP,c,!0);u.setUint32(PATTERN_INFO.N_FIGURES,0,!0);let p=20;new Float32Array(h,p,2*l).set(n);p+=8*l;m.set(s,p);p+=4*f;for(const[e,t]of r){u.setFloat32(p,e,!0);p+=4;u.setUint32(p,parseInt(t.slice(1),16),!0);p+=4}if(a)for(const e of a){u.setFloat32(p,e,!0);p+=4}o&&m.set(o,p);return h}(s);this.handler.send("commonobj",[r,"Pattern",e],[e])}else this.handler.send("obj",[r,this.pageIndex,"Pattern",s]);return r}handleColorN(e,t,a,n,s,r,i,o,l,f){const c=a.pop();if(c instanceof Name){const h=s.getRaw(c.name),u=h instanceof Ref&&l.getByRef(h);if(u)try{const s=n.base?n.base.getRgbHex(a,0):null,r=getTilingPatternIR(u.operatorListIR,u.dict,s,u.needsIsolation);e.addOp(t,r);return}catch{}const m=this.xref.fetchIfRef(h);if(m){const s=m instanceof BaseStream?m.dict:m,c=s.get("PatternType");if(c===Fi){const o=n.base?n.base.getRgbHex(a,0):null;return this.handleTilingType(t,o,r,m,s,e,i,l)}if(c===Ti){const a=s.get("Shading"),n=this.parseShading({shading:a,resources:r,localColorSpaceCache:o,localShadingPatternCache:f});if(n){const a=lookupMatrix(s.getArray("Matrix"),null);e.addOp(t,["Shading",n,a])}return}throw new FormatError(`Unknown PatternType: ${c}`)}}throw new FormatError(`Unknown PatternName: ${c}`)}_parseVisibilityExpression(e,t,a){if(++t>10){warn("Visibility expression is too deeply nested");return}const n=e.length,s=this.xref.fetchIfRef(e[0]);if(!(n<2)&&s instanceof Name){switch(s.name){case"And":case"Or":case"Not":a.push(s.name);break;default:warn(`Invalid operator ${s.name} in visibility expression`);return}for(let s=1;s0)return{type:"OCMD",expression:t}}const t=a.get("OCGs");if(Array.isArray(t)||t instanceof Dict){const e=[];if(Array.isArray(t))for(const a of t)e.push(a.toString());else e.push(t.objId);return{type:n,ids:e,policy:a.get("P")instanceof Name?a.get("P").name:null,expression:null}}if(t instanceof Ref)return{type:n,id:t.toString()}}return null}async getOperatorList({stream:e,task:a,resources:n,operatorList:s,initialState:r=null,fallbackFontDict:i=null,prevRefs:o=null}){if(e.isAsync){const t=await e.asyncGetBytes();t&&(e=new Stream(t,0,t.length,e.dict))}const l=e.dict?.objId,f=new RefSet(o);if(l){if(o?.has(l))throw new Error(`getOperatorList - ignoring circular reference: ${l}`);f.put(l)}n||=Dict.empty;r||=new EvalState;if(!s)throw new Error('getOperatorList: missing "operatorList" parameter');const c=this,h=this.xref,u=new LocalImageCache,m=new LocalColorSpaceCache,p=new LocalGStateCache,d=new LocalTilingPatternCache,g=new Map,b=n.get("XObject")||Dict.empty,w=n.get("Pattern")||Dict.empty,j=new StateManager(r),k=new EvaluatorPreprocessor(e,h,j),y=new TimeSlotManager;function closePendingRestoreOPS(e){for(let e=0,t=k.savedStatesDepth;e{j.state.fillColorSpace=e||ColorSpaceUtils.gray}));return}case it:{const t=c._getColorSpace(e[0],n,m);if(t instanceof ColorSpace){j.state.strokeColorSpace=t;continue}next(c._handleColorSpace(t).then(e=>{j.state.strokeColorSpace=e||ColorSpaceUtils.gray}));return}case ct:S=j.state.fillColorSpace;e=[S.getRgbHex(e,0)];r=dt;break;case lt:S=j.state.strokeColorSpace;e=[S.getRgbHex(e,0)];r=pt;break;case mt:j.state.fillColorSpace=ColorSpaceUtils.gray;e=[ColorSpaceUtils.gray.getRgbHex(e,0)];r=dt;break;case ut:j.state.strokeColorSpace=ColorSpaceUtils.gray;e=[ColorSpaceUtils.gray.getRgbHex(e,0)];r=pt;break;case bt:j.state.fillColorSpace=ColorSpaceUtils.cmyk;e=[ColorSpaceUtils.cmyk.getRgbHex(e,0)];r=dt;break;case gt:j.state.strokeColorSpace=ColorSpaceUtils.cmyk;e=[ColorSpaceUtils.cmyk.getRgbHex(e,0)];r=pt;break;case dt:j.state.fillColorSpace=ColorSpaceUtils.rgb;e=[ColorSpaceUtils.rgb.getRgbHex(e,0)];break;case pt:j.state.strokeColorSpace=ColorSpaceUtils.rgb;e=[ColorSpaceUtils.rgb.getRgbHex(e,0)];break;case ht:S=j.state.patternFillColorSpace;if(!S){if(isNumberArray(e,null)){e=[ColorSpaceUtils.gray.getRgbHex(e,0)];r=dt;break}e=[];r=Kt;break}if("Pattern"===S.name){next(c.handleColorN(s,ht,e,S,w,n,a,m,d,g));return}e=[S.getRgbHex(e,0)];r=dt;break;case ft:S=j.state.patternStrokeColorSpace;if(!S){if(isNumberArray(e,null)){e=[ColorSpaceUtils.gray.getRgbHex(e,0)];r=pt;break}e=[];r=Wt;break}if("Pattern"===S.name){next(c.handleColorN(s,ft,e,S,w,n,a,m,d,g));return}e=[S.getRgbHex(e,0)];r=pt;break;case wt:let F;try{const t=n.get("Shading");if(!t)throw new FormatError("No shading resource found");F=t.get(e[0].name);if(!F)throw new FormatError("No shading object found")}catch(e){if(e instanceof AbortException)continue;if(c.options.ignoreErrors){warn(`getOperatorList - ignoring Shading: "${e}".`);continue}throw e}const T=c.parseShading({shading:F,resources:n,localColorSpaceCache:m,localShadingPatternCache:g});if(!T)continue;e=[T];r=wt;break;case je:C=e[0]instanceof Name;x=e[0].name;if(C){const t=p.getByName(x);if(t){t.length>0&&s.addOp(je,[t]);e=null;continue}}next(new Promise(function(e,t){if(!C)throw new FormatError("GState must be referred to by name.");const r=n.get("ExtGState");if(!(r instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const i=r.get(x);if(!(i instanceof Dict))throw new FormatError("GState should be a dictionary.");c.setGState({resources:n,gState:i,operatorList:s,cacheKey:x,task:a,stateManager:j,localGStateCache:p,localColorSpaceCache:m,seenRefs:f}).then(e,t)}).catch(function(e){if(!(e instanceof AbortException)){if(!c.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring ExtGState: "${e}".`)}}));return;case ue:{const[t]=e;if("number"!=typeof t){warn(`Invalid setLineWidth: ${t}`);continue}e[0]=Math.abs(t);break}case ge:{const t=e[1];if("number"!=typeof t){warn(`Invalid setDash: ${t}`);continue}const a=e[0];if(!Array.isArray(a)){warn(`Invalid setDash: ${a}`);continue}a.some(e=>"number"!=typeof e)&&(e[0]=a.filter(e=>"number"==typeof e));break}case ve:case Se:case Ae:case xe:case Ce:case Ie:case Fe:c.buildPath(r,e,j.state);continue;case Te:case Oe:case Re:case He:case Be:case De:case Me:case Ne:case Pe:{const{state:{pathBuffer:e,pathMinMax:a}}=j;r!==Oe&&r!==Me&&r!==Ne||e.push(Qt);if(0===e.length)s.addOp(Xt,[r,[null],null]);else{s.addOp(Xt,[r,[new Float32Array(e)],a.slice()]);e.length=0;a.set(t,0)}continue}case Qe:s.addOp(r,[new Float32Array(e)]);continue;case vt:case St:case It:case Ft:continue;case xt:if(!(e[0]instanceof Name)){warn(`Expected name for beginMarkedContentProps arg0=${e[0]}`);s.addOp(xt,["OC",null]);continue}if("OC"===e[0].name){next(c.parseMarkedContentProps(e[1],n).then(e=>{s.addOp(xt,["OC",e])}).catch(e=>{if(!(e instanceof AbortException)){if(!c.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring beginMarkedContentProps: "${e}".`);s.addOp(xt,["OC",null])}}));return}e=[e[0].name,e[1]instanceof Dict?e[1].get("MCID"):null];break;default:if(null!==e){for(q=0,v=e.length;q{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring errors during "${a.name}" task: "${e}".`);closePendingRestoreOPS()}})}async getTextContent({stream:e,task:t,resources:a,stateManager:s=null,includeMarkedContent:r=!1,sink:i,seenStyles:o=new Set,viewBox:l,lang:f=null,markedContentData:c=null,disableNormalization:h=!1,keepWhiteSpace:u=!1,prevRefs:m=null,intersector:p=null}){if(e.isAsync){const t=await e.asyncGetBytes();t&&(e=new Stream(t,0,t.length,e.dict))}const d=e.dict?.objId,g=new RefSet(m);if(d){if(m?.has(d))throw new Error(`getTextContent - ignoring circular reference: ${d}`);g.put(d)}a||=Dict.empty;s||=new StateManager(new TextState);r&&(c||={level:0});const b={items:[],styles:Object.create(null),lang:f},w={initialized:!1,str:[],totalWidth:0,totalHeight:0,width:0,height:0,vertical:!1,prevTransform:null,prevTextRise:0,textAdvanceScale:0,spaceInFlowMin:0,spaceInFlowMax:0,trackingSpaceMin:1/0,negativeSpaceMax:-1/0,notASpace:-1/0,transform:null,fontName:null,hasEOL:!1},j=[" "," "];let k=0;function saveLastChar(e){const t=(k+1)%2,a=" "!==j[k]&&" "===j[t];j[k]=e;k=t;return!u&&a}function shouldAddWhitepsace(){return!u&&" "!==j[k]&&" "===j[(k+1)%2]}function resetLastChars(){j[0]=j[1]=" ";k=0}const y=this,q=this.xref,v=[];let S=null;const x=new LocalImageCache,C=new LocalGStateCache,F=new EvaluatorPreprocessor(e,q,s);let T,O;function pushWhitespace({width:e=0,height:t=0,transform:a=w.prevTransform,fontName:n=w.fontName}){p?.addExtraChar(" ");b.items.push({str:" ",dir:"ltr",width:e,height:t,transform:a,fontName:n,hasEOL:!1})}function getCurrentTextTransform(){const e=T.font,t=[T.fontSize*T.textHScale,0,0,T.fontSize,0,T.textRise];if(e.isType3Font&&(T.fontSize<=1||e.isCharBBox)&&!isArrayEqual(T.fontMatrix,n)){const a=e.bbox[3]-e.bbox[1];a>0&&(t[3]*=a*T.fontMatrix[3])}return Util.transform(T.ctm,Util.transform(T.textMatrix,t))}function ensureTextContentItem(){if(w.initialized)return w;const{font:e,loadedName:t}=T;if(!o.has(t)){o.add(t);b.styles[t]={fontFamily:e.fallbackName,ascent:e.ascent,descent:e.descent,vertical:e.vertical};if(y.options.fontExtraProperties&&e.systemFontInfo){const a=b.styles[t];a.fontSubstitution=e.systemFontInfo.css;a.fontSubstitutionLoadedName=e.systemFontInfo.loadedName}}w.fontName=t;const a=w.transform=getCurrentTextTransform();if(e.vertical){w.width=w.totalWidth=Math.hypot(a[0],a[1]);w.height=w.totalHeight=0;w.vertical=!0}else{w.width=w.totalWidth=0;w.height=w.totalHeight=Math.hypot(a[2],a[3]);w.vertical=!1}const n=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),s=Math.hypot(T.ctm[0],T.ctm[1]);w.textAdvanceScale=s*n;const{fontSize:r}=T;w.trackingSpaceMin=.102*r;w.notASpace=.03*r;w.negativeSpaceMax=-.2*r;w.spaceInFlowMin=.102*r;w.spaceInFlowMax=.6*r;w.hasEOL=!1;w.initialized=!0;return w}function updateAdvanceScale(){if(!w.initialized)return;const e=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),t=Math.hypot(T.ctm[0],T.ctm[1])*e;if(t!==w.textAdvanceScale){if(w.vertical){w.totalHeight+=w.height*w.textAdvanceScale;w.height=0}else{w.totalWidth+=w.width*w.textAdvanceScale;w.width=0}w.textAdvanceScale=t}}function runBidiTransform(e){let t=e.str.join("");h||(t=function normalizeUnicode(e){if(!sa){sa=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;ra=new Map([["ſt","ſt"]])}return e.replaceAll(sa,(e,t,a)=>t?t.normalize("NFKC"):ra.get(a))}(t));const a=bidi(t,-1,e.vertical);return{str:a.str,dir:a.dir,width:Math.abs(e.totalWidth),height:Math.abs(e.totalHeight),transform:e.transform,fontName:e.fontName,hasEOL:e.hasEOL}}async function handleSetFont(e,s){const r=await y.loadFont(e,s,a,t);T.loadedName=r.loadedName;T.font=r.font;T.fontMatrix=r.font.fontMatrix||n}function applyInverseRotation(e,t,a){const n=Math.hypot(a[0],a[1]);return[(a[0]*e+a[1]*t)/n,(a[2]*e+a[3]*t)/n]}function compareWithLastPosition(e){const t=getCurrentTextTransform();let a=t[4],n=t[5];if(T.font?.vertical){if(al[2]||n+el[3])return!1}else if(a+el[2]||nl[3])return!1;if(!T.font||!w.prevTransform)return!0;let s=w.prevTransform[4],r=w.prevTransform[5];if(s===a&&r===n)return!0;let i=-1;t[0]&&0===t[1]&&0===t[2]?i=t[0]>0?0:180:t[1]&&0===t[0]&&0===t[3]&&(i=t[1]>0?90:270);switch(i){case 0:break;case 90:[a,n]=[n,a];[s,r]=[r,s];break;case 180:[a,n,s,r]=[-a,-n,-s,-r];break;case 270:[a,n]=[-n,-a];[s,r]=[-r,-s];break;default:[a,n]=applyInverseRotation(a,n,t);[s,r]=applyInverseRotation(s,r,w.prevTransform)}if(T.font.vertical){const e=(r-n)/w.textAdvanceScale,t=a-s,i=Math.sign(w.height||w.totalHeight);if(e.5*w.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>w.width){appendEOL();return!0}e<=i*w.notASpace&&resetLastChars();if(e<=i*w.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else w.height+=e;else if(!addFakeSpaces(e,w.prevTransform,i))if(0===w.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else w.height+=e;Math.abs(t)>.25*w.width&&flushTextContentItem();return!0}const o=(a-s)/w.textAdvanceScale,f=n-r,c=Math.sign(w.width||w.totalWidth);if(o.5*w.height){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}const h=T.textRise-w.prevTextRise,u=0===h?f:f-t[3]/T.fontSize*h;if(Math.abs(u)>w.height){appendEOL();return!0}o<=c*w.notASpace&&resetLastChars();if(o<=c*w.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({width:Math.abs(o)})}else w.width+=o;else if(!addFakeSpaces(o,w.prevTransform,c))if(0===w.str.length){resetLastChars();pushWhitespace({width:Math.abs(o)})}else w.width+=o;Math.abs(f)>.25*w.height&&flushTextContentItem();return!0}function buildTextContentItem({chars:e,extraSpacing:t}){if(O!==T&&(O.fontSize!==T.fontSize||O.fontName!==T.fontName&&(O.font.name!==T.font.name||O.font.vertical!==T.font.vertical))){flushTextContentItem();O=T.clone()}const a=T.font,n=a.vertical?-T.charSpacing:T.charSpacing;if(!e){const e=n+t;e&&(a.vertical?T.translateTextMatrix(0,-e):T.translateTextMatrix(e*T.textHScale,0));u&&compareWithLastPosition(0);return}const s=a.charsToGlyphs(e),r=T.fontMatrix[0]*T.fontSize;for(let e=0,i=s.length;e0){const e=v.join("");v.length=0;buildTextContentItem({chars:e,extraSpacing:0})}break;case et:if(!s.state.font){y.ensureStateFont(s.state);continue}buildTextContentItem({chars:j[0],extraSpacing:0});break;case at:if(!s.state.font){y.ensureStateFont(s.state);continue}T.carriageReturn();buildTextContentItem({chars:j[0],extraSpacing:0});break;case nt:if(!s.state.font){y.ensureStateFont(s.state);continue}T.wordSpacing=j[0];T.charSpacing=j[1];T.carriageReturn();buildTextContentItem({chars:j[2],extraSpacing:0});break;case qt:flushTextContentItem();S??=a.get("XObject")||Dict.empty;w=j[0]instanceof Name;d=j[0].name;if(w&&x.getByName(d))break;next(new Promise(function(e,n){if(!w)throw new FormatError("XObject must be referred to by name.");let m=S.getRaw(d);if(m instanceof Ref){if(x.getByRef(m)){e();return}if(y.globalImageCache.getData(m,y.pageIndex)){e();return}m=q.fetch(m)}if(!(m instanceof BaseStream))throw new FormatError("XObject should be a stream");const{dict:p}=m,b=p.get("Subtype");if(!(b instanceof Name))throw new FormatError("XObject should have a Name subtype");if("Form"!==b.name){x.set(d,p.objId,!0);e();return}const j=s.state.clone(),k=new StateManager(j),v=lookupMatrix(p.getArray("Matrix"),null);v&&k.transform(v);const C=p.get("Resources");enqueueChunk();const F={enqueueInvoked:!1,enqueue(e,t){this.enqueueInvoked=!0;i.enqueue(e,t)},get desiredSize(){return i.desiredSize??0},get ready(){return i.ready}};y.getTextContent({stream:m,task:t,resources:C instanceof Dict?C:a,stateManager:k,includeMarkedContent:r,sink:i&&F,seenStyles:o,viewBox:l,lang:f,markedContentData:c,disableNormalization:h,keepWhiteSpace:u,prevRefs:g}).then(function(){F.enqueueInvoked||x.set(d,p.objId,!0);e()},n)}).catch(function(e){if(!(e instanceof AbortException)){if(!y.options.ignoreErrors)throw e;warn(`getTextContent - ignoring XObject: "${e}".`)}}));return;case je:w=j[0]instanceof Name;d=j[0].name;if(w&&C.getByName(d))break;next(new Promise(function(e,t){if(!w)throw new FormatError("GState must be referred to by name.");const n=a.get("ExtGState");if(!(n instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const s=n.get(d);if(!(s instanceof Dict))throw new FormatError("GState should be a dictionary.");const r=s.get("Font");if(r){flushTextContentItem();T.fontName=null;T.fontSize=r[1];handleSetFont(null,r[0]).then(e,t)}else{C.set(d,s.objId,!0);e()}}).catch(function(e){if(!(e instanceof AbortException)){if(!y.options.ignoreErrors)throw e;warn(`getTextContent - ignoring ExtGState: "${e}".`)}}));return;case At:flushTextContentItem();if(r){c.level++;b.items.push({type:"beginMarkedContent",tag:j[0]instanceof Name?j[0].name:null})}break;case xt:flushTextContentItem();if(r){c.level++;let e=null;j[1]instanceof Dict&&(e=j[1].get("MCID"));b.items.push({type:"beginMarkedContentProps",id:Number.isInteger(e)?`${y.idFactory.getPageObjId()}_mc${e}`:null,tag:j[0]instanceof Name?j[0].name:null})}break;case Ct:flushTextContentItem();if(r){if(0===c.level)break;c.level--;b.items.push({type:"endMarkedContent"})}}if(b.items.length>=(i?.desiredSize??1)){p=!0;break}}if(p)next(Oi);else{flushTextContentItem();enqueueChunk();e()}}).catch(e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getTextContent - ignoring errors during "${t.name}" task: "${e}".`);flushTextContentItem();enqueueChunk()}})}async extractDataStructures(e,t){const a=this.xref;let n;const s=this.readToUnicode(t.toUnicode);if(t.composite){const a=e.get("CIDSystemInfo");a instanceof Dict&&!t.cidSystemInfo&&(t.cidSystemInfo={registry:stringToPDFString(a.get("Registry")),ordering:stringToPDFString(a.get("Ordering")),supplement:a.get("Supplement")});try{const t=e.get("CIDToGIDMap");t instanceof BaseStream&&(n=t.getBytes())}catch(e){if(!this.options.ignoreErrors)throw e;warn(`extractDataStructures - ignoring CIDToGIDMap data: "${e}".`)}}const r=[];let i,o=null;if(e.has("Encoding")){i=e.get("Encoding");if(i instanceof Dict){o=i.get("BaseEncoding");o=o instanceof Name?o.name:null;if(i.has("Differences")){const e=i.get("Differences");let t=0;for(const n of e){const e=a.fetchIfRef(n);if("number"==typeof e)t=e;else{if(!(e instanceof Name))throw new FormatError(`Invalid entry in 'Differences' array: ${e}`);r[t++]=e.name}}}}else if(i instanceof Name)o=i.name;else{const e="Encoding is not a Name nor a Dict";if(!this.options.ignoreErrors)throw new FormatError(e);warn(e)}"MacRomanEncoding"!==o&&"MacExpertEncoding"!==o&&"WinAnsiEncoding"!==o&&(o=null)}const l=!t.file||t.isInternalFont,f=_n()[t.name];o&&l&&f&&(o=null);if("WinAnsiEncoding"===o&&l&&t.name?.charCodeAt(0)>=183){const e=t.name;if(["ËÎÌå","ºÚÌå","¿¬Ìå","·ÂËÎ","¿¬Ìå_GB2312","·ÂËÎ_GB2312","Á¥Êé","ÐÂËÎ","·ÂËÎÌå","С±êËÎ"].includes(e)){o=null;t.defaultEncoding="Adobe-GB1-UCS2";t.composite=!0;t.cidEncoding=Name.get("GBK-EUC-H");const e=await CMapFactory.create({encoding:t.cidEncoding,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null});t.cMap=e;t.vertical=t.cMap.vertical;t.cidSystemInfo={registry:"Adobe",ordering:"GB1",supplement:0}}}if(o)t.defaultEncoding=getEncoding(o);else{let e=!!(t.flags&qn);const a=!!(t.flags&vn);if("TrueType"===t.type&&e&&a&&0!==r.length){t.flags&=~qn;e=!1}i=fn;"TrueType"!==t.type||a||(i=cn);if(e||f){i=ln;l&&(/Symbol/i.test(t.name)?i=hn:/Dingbats/i.test(t.name)?i=un:/Wingdings/i.test(t.name)&&(i=cn))}t.defaultEncoding=i}t.differences=r;t.baseEncodingName=o;t.hasEncoding=!!o||r.length>0;t.dict=e;t.toUnicode=await s;const c=await this.buildToUnicode(t);t.toUnicode=c;n&&(t.cidToGidMap=this.readCidToGidMap(n,c));return t}_simpleFontToUnicode(e,t=!1){assert(!e.composite,"Must be a simple font.");const a=[],n=e.defaultEncoding.slice(),s=e.baseEncodingName,r=e.differences;for(const e in r){const t=r[e];".notdef"!==t&&(n[e]=t)}const i=mn();for(const r in n){let o=n[r];if(""===o)continue;let l=i[o];if(void 0!==l){a[r]=String.fromCharCode(l);continue}let f=0;switch(o[0]){case"G":3===o.length&&(f=parseInt(o.substring(1),16));break;case"g":5===o.length&&(f=parseInt(o.substring(1),16));break;case"C":case"c":if(o.length>=3&&o.length<=4){const a=o.substring(1);if(t){f=parseInt(a,16);break}f=+a;if(Number.isNaN(f)&&Number.isInteger(parseInt(a,16)))return this._simpleFontToUnicode(e,!0)}break;case"u":l=getUnicodeForGlyph(o,i);-1!==l&&(f=l);break;default:switch(o){case"f_h":case"f_t":case"T_h":a[r]=o.replaceAll("_","");continue}}if(f>0&&f<=1114111&&Number.isInteger(f)){if(s&&f===+r){const e=getEncoding(s);if(e&&(o=e[r])){a[r]=String.fromCharCode(i[o]);continue}}a[r]=String.fromCodePoint(f)}}return a}async buildToUnicode(e){e.hasIncludedToUnicodeMap=e.toUnicode?.length>0;if(e.hasIncludedToUnicodeMap){!e.composite&&e.hasEncoding&&(e.fallbackToUnicode=this._simpleFontToUnicode(e));return e.toUnicode}if(!e.composite)return new ToUnicodeMap(this._simpleFontToUnicode(e));if(e.composite&&(e.cMap.builtInCMap&&!(e.cMap instanceof IdentityCMap)||"Adobe"===e.cidSystemInfo?.registry&&("GB1"===e.cidSystemInfo.ordering||"CNS1"===e.cidSystemInfo.ordering||"Japan1"===e.cidSystemInfo.ordering||"Korea1"===e.cidSystemInfo.ordering))){const{registry:t,ordering:a}=e.cidSystemInfo,n=Name.get(`${t}-${a}-UCS2`),s=await CMapFactory.create({encoding:n,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null}),r=[],i=[];e.cMap.forEach(function(e,t){if(t>65535)throw new FormatError("Max size of CID is 65,535");const a=s.lookup(t);if(a){i.length=0;for(let e=0,t=a.length;e>1;(0!==s||t.has(r))&&(a[r]=s)}return a}extractWidths(e,t,a){const n=this.xref;let s=[],r=0;const i=[];let o;if(a.composite){const t=e.get("DW");r="number"==typeof t?Math.ceil(t):1e3;const l=e.get("W");if(Array.isArray(l))for(let e=0,t=l.length;e{const t=l.get(e),n=new OperatorList;return r.getOperatorList({stream:t,task:a,resources:f,operatorList:n}).then(()=>{switch(n.fnArray[0]){case rt:this.#Ee(n,b);break;case st:b||this.#_e(n)}c[e]=n.getIR();for(const e of n.dependencies)s.add(e)}).catch(function(t){warn(`Type3 font resource "${e}" is not available.`);const a=new OperatorList;c[e]=a.getIR()})});this.#Pe=o.then(()=>{n.charProcOperatorList=c;if(this._bbox){n.isCharBBox=!0;n.bbox=this._bbox}});return this.#Pe}#Ee(e,a=NaN){const n=Util.normalizeRect(e.argsArray[0].slice(2)),s=n[2]-n[0],r=n[3]-n[1],i=Math.hypot(s,r);if(0===s||0===r){e.fnArray.splice(0,1);e.argsArray.splice(0,1)}else if(0===a||Math.round(i/a)>=10){this._bbox??=t.slice();Util.rectBoundingBox(...n,this._bbox)}let o=0,l=e.length;for(;o=ve&&r<=Pe;if(s.variableArgs)o>i&&info(`Command ${n}: expected [0, ${i}] args, but received ${o} args.`);else{if(o!==i){const e=this.nonProcessedArgs;for(;o>i;){e.push(t.shift());o--}for(;oEvaluatorPreprocessor.MAX_INVALID_PATH_OPS)throw new FormatError(`Invalid ${e}`);warn(`Skipping ${e}`);null!==t&&(t.length=0);continue}}this.preprocessCommand(r,t);e.fn=r;e.args=t;return!0}if(a===oa)return!1;if(null!==a){null===t&&(t=[]);t.push(a);if(t.length>33)throw new FormatError("Too many arguments")}}}preprocessCommand(e,t){switch(0|e){case ke:this.stateManager.save();break;case ye:this.stateManager.restore();break;case qe:this.stateManager.transform(t)}}}class DefaultAppearanceEvaluator extends EvaluatorPreprocessor{constructor(e){super(new StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:a,args:n}=e;switch(0|a){case Ge:const[e,a]=n;e instanceof Name&&(t.fontName=e.name);"number"==typeof a&&a>0&&(t.fontSize=a);break;case dt:ColorSpaceUtils.rgb.getRgbItem(n,0,t.fontColor,0);break;case mt:ColorSpaceUtils.gray.getRgbItem(n,0,t.fontColor,0);break;case bt:ColorSpaceUtils.cmyk.getRgbItem(n,0,t.fontColor,0)}}}catch(e){warn(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()}class AppearanceStreamEvaluator extends EvaluatorPreprocessor{constructor(e,t,a){super(e);this.stream=e;this.xref=t;this.globalColorSpaceCache=a;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:ColorSpaceUtils.gray},a=!1;const n=[];try{for(;;){e.args.length=0;if(a||!this.read(e))break;const{fn:s,args:r}=e;switch(0|s){case ke:n.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case ye:t=n.pop()||t;break;case Qe:t.scaleFactor*=Math.hypot(r[0],r[1]);break;case Ge:const[e,s]=r;e instanceof Name&&(t.fontName=e.name);"number"==typeof s&&s>0&&(t.fontSize=s*t.scaleFactor);break;case ot:t.fillColorSpace=ColorSpaceUtils.parse({cs:r[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:this._localColorSpaceCache});break;case ct:t.fillColorSpace.getRgbItem(r,0,t.fontColor,0);break;case dt:ColorSpaceUtils.rgb.getRgbItem(r,0,t.fontColor,0);break;case mt:ColorSpaceUtils.gray.getRgbItem(r,0,t.fontColor,0);break;case bt:ColorSpaceUtils.cmyk.getRgbItem(r,0,t.fontColor,0);break;case et:case tt:case at:case nt:a=!0}}}catch(e){warn(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return shadow(this,"_localColorSpaceCache",new LocalColorSpaceCache)}get _pdfFunctionFactory(){return shadow(this,"_pdfFunctionFactory",new PDFFunctionFactory({xref:this.xref}))}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){return`${numberToString(e[0]/255)} ${t?"g":"G"}`}return Array.from(e,e=>numberToString(e/255)).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{static#ze=1;constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const a=new OffscreenCanvas(1,1);this.ctxMeasure=a.getContext("2d",{willReadFrequently:!0});this.fontName=Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont.#ze++}`)}get fontDescriptorRef(){if(!FakeUnicodeFont._fontDescriptorRef){const e=new Dict(this.xref);e.setIfName("Type","FontDescriptor");e.set("FontName",this.fontName);e.set("FontFamily","MyriadPro Regular");e.set("FontBBox",[0,0,0,0]);e.setIfName("FontStretch","Normal");e.set("FontWeight",400);e.set("ItalicAngle",0);FakeUnicodeFont._fontDescriptorRef=this.xref.getNewPersistentRef(e)}return FakeUnicodeFont._fontDescriptorRef}get descendantFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.setIfName("Type","Font");e.setIfName("Subtype","CIDFontType0");e.setIfName("CIDToGIDMap","Identity");e.set("FirstChar",this.firstChar);e.set("LastChar",this.lastChar);e.set("FontDescriptor",this.fontDescriptorRef);e.set("DW",1e3);const t=[],a=[...this.widths].sort();let n=null,s=null;for(const[e,r]of a)if(n)if(e===n+s.length)s.push(r);else{t.push(n,s);n=e;s=[r]}else{n=e;s=[r]}n&&t.push(n,s);e.set("W",t);const r=new Dict(this.xref);r.set("Ordering","Identity");r.set("Registry","Adobe");r.set("Supplement",0);e.set("CIDSystemInfo",r);return this.xref.getNewPersistentRef(e)}get baseFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.setIfName("Type","Font");e.setIfName("Subtype","Type0");e.setIfName("Encoding","Identity-H");e.set("DescendantFonts",[this.descendantFontRef]);e.setIfName("ToUnicode","Identity-H");return this.xref.getNewPersistentRef(e)}get resources(){const e=new Dict(this.xref),t=new Dict(this.xref);t.set(this.fontName.name,this.baseFontRef);e.set("Font",t);return e}_createContext(){this.widths=new Map;this.ctxMeasure.font=`1000px ${this.fontFamily}`;return this.ctxMeasure}createFontResources(e){const t=this._createContext();for(const a of e.split(/\r\n?|\n/))for(const e of a.split("")){const a=e.charCodeAt(0);if(this.widths.has(a))continue;const n=t.measureText(e),s=Math.ceil(n.width);this.widths.set(a,s);this.firstChar=Math.min(a,this.firstChar);this.lastChar=Math.max(a,this.lastChar)}return this.resources}static getFirstPositionInfo(e,t,a){const[n,i,o,l]=e;let f=o-n,c=l-i;t%180!=0&&([f,c]=[c,f]);const h=s*a;return{coords:[0,c+r*a-h],bbox:[0,0,f,c],matrix:0!==t?getRotationMatrix(t,c,h):void 0}}createAppearance(e,t,a,n,i,o){const l=this._createContext(),f=[];let c=-1/0;for(const t of e.split(/\r\n?|\n/)){f.push(t);const e=l.measureText(t).width;c=Math.max(c,e);for(const e of codePointIter(t)){const t=String.fromCodePoint(e);let a=this.widths.get(e);if(void 0===a){const n=l.measureText(t);a=Math.ceil(n.width);this.widths.set(e,a);this.firstChar=Math.min(e,this.firstChar);this.lastChar=Math.max(e,this.lastChar)}}}c*=n/1e3;const[h,u,m,p]=t;let d=m-h,g=p-u;a%180!=0&&([d,g]=[g,d]);let b=1;c>d&&(b=d/c);let w=1;const j=s*n,k=r*n,y=j*f.length;y>g&&(w=g/y);const q=n*Math.min(b,w),v=["q",`0 0 ${numberToString(d)} ${numberToString(g)} re W n`,"BT",`1 0 0 1 0 ${numberToString(g+k)} Tm 0 Tc ${getPdfColor(i,!0)}`,`/${this.fontName.name} ${numberToString(q)} Tf`],{resources:S}=this;if(1!==(o="number"==typeof o&&o>=0&&o<=1?o:1)){v.push("/R0 gs");const e=new Dict(this.xref),t=new Dict(this.xref);t.set("ca",o);t.set("CA",o);t.setIfName("Type","ExtGState");e.set("R0",t);S.set("ExtGState",e)}const x=numberToString(j);for(const e of f)v.push(`0 -${x} Td <${stringToUTF16HexString(e)}> Tj`);v.push("ET","Q");const C=v.join("\n"),F=new Dict(this.xref);F.setIfName("Subtype","Form");F.setIfName("Type","XObject");F.set("BBox",[0,0,d,g]);F.set("Length",C.length);F.set("Resources",S);if(a){const e=getRotationMatrix(a,d,g);F.set("Matrix",e)}const T=new StringStream(C);T.dict=F;return T}}const Ri=["m/d","m/d/yy","mm/dd/yy","mm/yy","d-mmm","d-mmm-yy","dd-mmm-yy","yy-mm-dd","mmm-yy","mmmm-yy","mmm d, yyyy","mmmm d, yyyy","m/d/yy h:MM tt","m/d/yy HH:MM"],Hi=["HH:MM","h:MM tt","HH:MM:ss","h:MM:ss tt"];class NameOrNumberTree{constructor(e,t,a){this.root=e;this.xref=t;this._type=a}getAll(e=!1){const t=new Map;if(!this.root)return t;const a=this.xref,n=new RefSet;this.root instanceof Ref&&n.put(this.root);const s=[this.root];for(;s.length>0;){const r=a.fetchIfRef(s.shift());if(!(r instanceof Dict))continue;if(r.has("Kids")){const e=r.get("Kids");if(!Array.isArray(e))continue;for(const t of e){if(t instanceof Ref){if(n.has(t))throw new FormatError(`Duplicate entry in "${this._type}" tree.`);n.put(t)}s.push(t)}continue}const i=r.get(this._type);if(Array.isArray(i))for(let n=0,s=i.length;n10){warn(`Search depth limit reached for "${this._type}" tree.`);return null}const s=a.get("Kids");if(!Array.isArray(s))return null;let r=0,i=s.length-1;for(;r<=i;){const n=r+i>>1,o=t.fetchIfRef(s[n]),l=o.get("Limits");if(et.fetchIfRef(l[1]))){a=o;break}r=n+1}}if(r>i)return null}const s=a.get(this._type);if(Array.isArray(s)){let a=0,n=s.length-2;for(;a<=n;){const r=a+n>>1,i=r+(1&r),o=t.fetchIfRef(s[i]);if(eo))return s[i+1];a=i+2}}}return null}get(e){return this.xref.fetchIfRef(this.getRaw(e))}}class NameTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Names")}}class NumberTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Nums")}}function clearGlobalCaches(){!function clearPatternCaches(){Va=Object.create(null)}();!function clearPrimitiveCaches(){la=Object.create(null);fa=Object.create(null);ca=Object.create(null)}();!function clearUnicodeCaches(){wn.clear()}();JBig2CCITTFaxImage.cleanup();JpxImage.cleanup()}function pickPlatformItem(e){if(e instanceof Dict)for(const t of["UF","F","Unix","Mac","DOS"])if(e.has(t))return e.get(t);return null}class FileSpec{#Le=!1;constructor(e,t=!1){if(e instanceof Dict){this.root=e;e.has("FS")&&(this.fs=e.get("FS"));e.has("RF")&&warn("Related file specifications are not supported");t||(e.has("EF")?this.#Le=!0:warn("Non-embedded file specifications are not supported"))}}get filename(){const e=pickPlatformItem(this.root);return e&&"string"==typeof e?stringToPDFString(e,!0).replaceAll("\\\\","\\").replaceAll("\\/","/").replaceAll("\\","/"):""}get content(){if(!this.#Le)return null;const e=pickPlatformItem(this.root?.get("EF"));if(e instanceof BaseStream)return e.getBytes();warn("Embedded file specification points to non-existing/invalid content");return null}get description(){const e=this.root?.get("Desc");return e&&"string"==typeof e?stringToPDFString(e):""}get serializable(){const{filename:e,content:t,description:a}=this;return{rawFilename:e,filename:(n=e,n.substring(n.lastIndexOf("/")+1))||"unnamed",content:t,description:a};var n}}const Bi=0,Di=-2,Mi=-3,Ni=-4,Pi=-5,Ei=-6,_i=-9;function isWhitespace(e,t){const a=e[t];return" "===a||"\n"===a||"\r"===a||"\t"===a}class XMLParserBase{_resolveEntities(e){return e.replaceAll(/&([^;]+);/g,(e,t)=>{if("#x"===t.substring(0,2))return String.fromCodePoint(parseInt(t.substring(2),16));if("#"===t.substring(0,1))return String.fromCodePoint(parseInt(t.substring(1),10));switch(t){case"lt":return"<";case"gt":return">";case"amp":return"&";case"quot":return'"';case"apos":return"'"}return this.onResolveEntity(t)})}_parseContent(e,t){const a=[];let n=t;function skipWs(){for(;n"!==e[n]&&"/"!==e[n];)++n;const s=e.substring(t,n);skipWs();for(;n"!==e[n]&&"/"!==e[n]&&"?"!==e[n];){skipWs();let t="",s="";for(;n"!==e[a]&&"?"!==e[a]&&"/"!==e[a];)++a;const n=e.substring(t,a);!function skipWs(){for(;a"!==e[a+1]);)++a;return{name:n,value:e.substring(s,a),parsed:a-t}}parseXml(e){let t=0;for(;t",a);if(t<0){this.onError(_i);return}this.onEndElement(e.substring(a,t));a=t+1;break;case"?":++a;const n=this._parseProcessingInstruction(e,a);if("?>"!==e.substring(a+n.parsed,a+n.parsed+2)){this.onError(Mi);return}this.onPi(n.name,n.value);a+=n.parsed+2;break;case"!":if("--"===e.substring(a+1,a+3)){t=e.indexOf("--\x3e",a+3);if(t<0){this.onError(Pi);return}this.onComment(e.substring(a+3,t));a=t+3}else if("[CDATA["===e.substring(a+1,a+8)){t=e.indexOf("]]>",a+8);if(t<0){this.onError(Di);return}this.onCdata(e.substring(a+8,t));a=t+3}else{if("DOCTYPE"!==e.substring(a+1,a+8)){this.onError(Ei);return}{const n=e.indexOf("[",a+8);let s=!1;t=e.indexOf(">",a+8);if(t<0){this.onError(Ni);return}if(n>0&&t>n){t=e.indexOf("]>",a+8);if(t<0){this.onError(Ni);return}s=!0}const r=e.substring(a+8,t+(s?1:0));this.onDoctype(r);a=t+(s?2:1)}}break;default:const s=this._parseContent(e,a);if(null===s){this.onError(Ei);return}let r=!1;if("/>"===e.substring(a+s.parsed,a+s.parsed+2))r=!0;else if(">"!==e.substring(a+s.parsed,a+s.parsed+1)){this.onError(_i);return}this.onBeginElement(s.name,s.attributes,r);a+=s.parsed+(r?2:1)}}else{for(;ae.textContent).join(""):this.nodeValue||""}get children(){return this.childNodes||[]}hasChildNodes(){return this.childNodes?.length>0}searchNode(e,t){if(t>=e.length)return this;const a=e[t];if(a.name.startsWith("#")&&t0){n.push([s,0]);s=s.childNodes[0]}else{if(0===n.length)return null;for(;0!==n.length;){const[e,t]=n.pop(),a=t+1;if(a");for(const t of this.childNodes)t.dump(e);e.push(``)}else this.nodeValue?e.push(`>${encodeToXmlString(this.nodeValue)}`):e.push("/>")}else e.push(encodeToXmlString(this.nodeValue))}}class SimpleXMLParser extends XMLParserBase{constructor({hasAttributes:e=!1,lowerCaseName:t=!1}){super();this._currentFragment=null;this._stack=null;this._errorCode=Bi;this._hasAttributes=e;this._lowerCaseName=t}parseFromString(e){this._currentFragment=[];this._stack=[];this._errorCode=Bi;this.parseXml(e);if(this._errorCode!==Bi)return;const[t]=this._currentFragment;return t?{documentElement:t}:void 0}onText(e){if(function isWhitespaceString(e){for(let t=0,a=e.length;t\\376\\377([^<]+)/g,function(e,t){const a=t.replaceAll(/\\([0-3])([0-7])([0-7])/g,function(e,t,a,n){return String.fromCharCode(64*t+8*a+1*n)}).replaceAll(/&(amp|apos|gt|lt|quot);/g,function(e,t){switch(t){case"amp":return"&";case"apos":return"'";case"gt":return">";case"lt":return"<";case"quot":return'"'}throw new Error(`_repair: ${t} isn't defined.`)}),n=[">"];for(let e=0,t=a.length;e=32&&t<127&&60!==t&&62!==t&&38!==t?n.push(String.fromCharCode(t)):n.push("&#x"+(65536+t).toString(16).substring(1)+";")}return n.join("")})}_getSequence(e){const t=e.nodeName;return"rdf:bag"!==t&&"rdf:seq"!==t&&"rdf:alt"!==t?null:e.childNodes.filter(e=>"rdf:li"===e.nodeName)}_parseArray(e){if(!e.hasChildNodes())return;const[t]=e.childNodes,a=this._getSequence(t)||[];this._metadataMap.set(e.nodeName,a.map(e=>e.textContent.trim()))}_parse(e){let t=e.documentElement;if("rdf:rdf"!==t.nodeName){t=t.firstChild;for(;t&&"rdf:rdf"!==t.nodeName;)t=t.nextSibling}if(t&&"rdf:rdf"===t.nodeName&&t.hasChildNodes())for(const e of t.childNodes)if("rdf:description"===e.nodeName)for(const t of e.childNodes){const e=t.nodeName;switch(e){case"#text":continue;case"dc:creator":case"dc:subject":this._parseArray(t);continue}this._metadataMap.set(e,t.textContent.trim())}}get serializable(){return{parsedData:this._metadataMap,rawData:this._data}}}const zi=1,Xi=2,Wi=3,Ki=4,Gi=5;class StructTreeRoot{constructor(e,t,a){this.xref=e;this.dict=t;this.ref=a instanceof Ref?a:null;this.roleMap=new Map;this.structParentIds=null;this.kidRefToPosition=void 0;this.parentTree=null}getKidPosition(e){if(void 0===this.kidRefToPosition){const e=this.dict.get("K");if(Array.isArray(e)){const t=this.kidRefToPosition=new Map;for(let a=0,n=e.length;a=0||(e.parentTreeId=a++);i=!1}}if(i){for(const e of t.values())for(const t of e){delete t.parentTreeId;delete t.structTreeParent}return!1}return!0}async updateStructureTree({newAnnotationsByPage:e,pdfManager:t,changes:a}){const{ref:n,xref:s}=this,r=this.dict.clone(),i=new RefSetCache;i.put(n,r);let o,l=r.getRaw("ParentTree");if(l instanceof Ref)o=s.fetch(l);else{o=l;l=s.getNewTemporaryRef();r.set("ParentTree",l)}o=o.clone();i.put(l,o);let f=o.getRaw("Nums"),c=null;if(f instanceof Ref){c=f;f=s.fetch(c)}f=f.slice();c||o.set("Nums",f);const h=await StructTreeRoot.#Xe({newAnnotationsByPage:e,structTreeRootRef:n,structTreeRoot:this,kids:null,nums:f,xref:s,pdfManager:t,changes:a,cache:i});if(-1!==h){r.set("ParentTreeNextKey",h);c&&i.put(c,f);for(const[e,t]of i.items())a.put(e,{data:t})}}static async#Xe({newAnnotationsByPage:e,structTreeRootRef:t,structTreeRoot:a,kids:n,nums:s,xref:r,pdfManager:i,changes:o,cache:l}){const f=Name.get("OBJR");let c,h=-1;for(const[u,m]of e){const e=await i.getPage(u),{ref:p}=e,d=p instanceof Ref;for(const{accessibilityData:i,ref:g,parentTreeId:b,structTreeParent:w}of m){if(!i?.type)continue;const{structParent:m}=i;if(a&&Number.isInteger(m)&&m>=0){let t=(c||=new Map).get(u);if(void 0===t){t=new StructTreePage(a,e.pageDict).collectObjects(p);c.set(u,t)}const n=t?.get(m);if(n){const e=r.fetch(n).clone();StructTreeRoot.#Ke(e,i);o.put(n,{data:e});continue}}h=Math.max(h,b);const j=r.getNewTemporaryRef(),k=new Dict(r);StructTreeRoot.#Ke(k,i);await this.#Ge({structTreeParent:w,tagDict:k,newTagRef:j,structTreeRootRef:t,fallbackKids:n,xref:r,cache:l});const y=new Dict(r);k.set("K",y);y.set("Type",f);d&&y.set("Pg",p);y.set("Obj",g);l.put(j,k);s.push(b,j)}}return h+1}static#Ke(e,{type:t,title:a,lang:n,alt:s,expanded:r,actualText:i}){e.set("S",Name.get(t));a&&e.set("T",stringToAsciiOrUTF16BE(a));n&&e.set("Lang",stringToAsciiOrUTF16BE(n));s&&e.set("Alt",stringToAsciiOrUTF16BE(s));r&&e.set("E",stringToAsciiOrUTF16BE(r));i&&e.set("ActualText",stringToAsciiOrUTF16BE(i))}static#We({elements:e,xref:t,pageDict:a,numberTree:n}){const s=new Map;for(const t of e)if(t.structTreeParentId){const e=parseInt(t.structTreeParentId.split("_mc")[1],10);s.getOrInsertComputed(e,makeArr).push(t)}const r=a.get("StructParents");if(!Number.isInteger(r))return;const i=n.get(r),updateElement=(e,a,n)=>{const r=s.get(e);if(r){const e=a.getRaw("P"),s=t.fetchIfRef(e);if(e instanceof Ref&&s instanceof Dict){const e={ref:n,dict:a};for(const t of r)t.structTreeParent=e}return!0}return!1};for(const e of i){if(!(e instanceof Ref))continue;const a=t.fetch(e),n=a.get("K");if(Number.isInteger(n))updateElement(n,a,e);else if(Array.isArray(n))for(let s of n){s=t.fetchIfRef(s);if(Number.isInteger(s)&&updateElement(s,a,e))break;if(!(s instanceof Dict))continue;if(!isName(s.get("Type"),"MCR"))break;const n=s.get("MCID");if(Number.isInteger(n)&&updateElement(n,a,e))break}}}static async#Ge({structTreeParent:e,tagDict:t,newTagRef:a,structTreeRootRef:n,fallbackKids:s,xref:r,cache:i}){let o,l=null;if(e){({ref:l}=e);o=e.dict.getRaw("P")||n}else o=n;t.set("P",o);const f=r.fetchIfRef(o);if(!f){s.push(a);return}let c=i.get(o);if(!c){c=f.clone();i.put(o,c)}const h=c.getRaw("K");let u=h instanceof Ref?i.get(h):null;if(!u){u=r.fetchIfRef(h);u=Array.isArray(u)?u.slice():[h];const e=r.getNewTemporaryRef();c.set("K",e);i.put(e,u)}const m=u.indexOf(l);u.splice(m>=0?m+1:u.length,0,a)}}class StructElementNode{constructor(e,t){this.tree=e;this.xref=e.xref;this.dict=t;this.kids=[];this.parseKids()}get role(){const e=this.dict.get("S"),t=e instanceof Name?e.name:"",{root:a}=this.tree;return a.roleMap.get(t)??t}get mathML(){let e=this.dict.get("AF")||[];Array.isArray(e)||(e=[e]);for(let t of e){t=this.xref.fetchIfRef(t);if(!(t instanceof Dict))continue;if(!isName(t.get("Type"),"Filespec"))continue;if(!isName(t.get("AFRelationship"),"Supplement"))continue;const e=t.get("EF");if(!(e instanceof Dict))continue;const a=e.get("UF")||e.get("F");if(a instanceof BaseStream&&(isName(a.dict.get("Type"),"EmbeddedFile")&&isName(a.dict.get("Subtype"),"application/mathml+xml")))return stringToUTF8String(a.getString())}const t=this.dict.get("A");if(t instanceof Dict){if(isName(t.get("O"),"MSFT_Office")){const e=t.get("MSFT_MathML");return e?stringToPDFString(e):null}}return null}parseKids(){let e=null;const t=this.dict.getRaw("Pg");t instanceof Ref&&(e=t.toString());const a=this.dict.get("K");if(Array.isArray(a))for(const t of a){const a=this.parseKid(e,this.xref.fetchIfRef(t));a&&this.kids.push(a)}else{const t=this.parseKid(e,a);t&&this.kids.push(t)}}parseKid(e,t){if(Number.isInteger(t))return this.tree.pageDict.objId!==e?null:new StructElement({type:zi,mcid:t,pageObjId:e});if(!(t instanceof Dict))return null;const a=t.getRaw("Pg");a instanceof Ref&&(e=a.toString());const n=t.get("Type")instanceof Name?t.get("Type").name:null;if("MCR"===n){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Stm");return new StructElement({type:Xi,refObjId:a instanceof Ref?a.toString():null,pageObjId:e,mcid:t.get("MCID")})}if("OBJR"===n){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Obj");return new StructElement({type:Wi,refObjId:a instanceof Ref?a.toString():null,pageObjId:e})}return new StructElement({type:Gi,dict:t})}}class StructElement{constructor({type:e,dict:t=null,mcid:a=null,pageObjId:n=null,refObjId:s=null}){this.type=e;this.dict=t;this.mcid=a;this.pageObjId=n;this.refObjId=s;this.parentNode=null}}class StructTreePage{constructor(e,t){this.root=e;this.xref=e?.xref??null;this.rootDict=e?.dict??null;this.pageDict=t;this.nodes=[]}collectObjects(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return null;const t=this.rootDict.get("ParentTree");if(!t)return null;const a=this.root.structParentIds?.get(e);if(!a)return null;const n=new Map,s=new NumberTree(t,this.xref);for(const[e]of a){const t=s.getRaw(e);t instanceof Ref&&n.set(e,t)}return n}parse(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return;const{parentTree:t}=this.root;if(!t)return;const a=this.pageDict.get("StructParents"),n=this.root.structParentIds?.get(e);if(!Number.isInteger(a)&&!n)return;const s=new Map;if(Number.isInteger(a)){const e=t.get(a);if(Array.isArray(e))for(const t of e)t instanceof Ref&&this.addNode(this.xref.fetch(t),s)}if(n)for(const[e,a]of n){const n=t.get(e);if(n){const e=this.addNode(this.xref.fetchIfRef(n),s);1===e?.kids?.length&&e.kids[0].type===Wi&&(e.kids[0].type=a)}}}addNode(e,t,a=0){if(a>40){warn("StructTree MAX_DEPTH reached.");return null}if(!(e instanceof Dict))return null;if(t.has(e))return t.get(e);const n=new StructElementNode(this,e);t.set(e,n);switch(n.role){case"L":case"LBody":case"LI":case"Table":case"THead":case"TBody":case"TFoot":case"TR":for(const e of n.kids)e.type===Gi&&this.addNode(e.dict,t,a-1)}const s=e.get("P");if(!(s instanceof Dict)||isName(s.get("Type"),"StructTreeRoot")){this.addTopLevelNode(e,n)||t.delete(e);return n}const r=this.addNode(s,t,a+1);if(!r)return n;let i=!1;for(const t of r.kids)if(t.type===Gi&&t.dict===e){t.parentNode=n;i=!0}i||t.delete(e);return n}addTopLevelNode(e,t){const a=this.root.getKidPosition(e.objId);if(isNaN(a))return!1;-1!==a&&(this.nodes[a]=t);return!0}get serializable(){function nodeToSerializable(e,t,a=0){if(a>40){warn("StructTree too deep to be fully serialized.");return}const n=Object.create(null);n.role=e.role;n.children=[];t.children.push(n);let s=e.dict.get("Alt");"string"!=typeof s&&(s=e.dict.get("ActualText"));"string"==typeof s&&(n.alt=stringToPDFString(s));if("Formula"===n.role){const{mathML:t}=e;t&&(n.mathML=t)}const r=e.dict.get("A");if(r instanceof Dict){const e=lookupNormalRect(r.getArray("BBox"),null);if(e)n.bbox=e;else{const e=r.get("Width"),t=r.get("Height");"number"==typeof e&&e>0&&"number"==typeof t&&t>0&&(n.bbox=[0,0,e,t])}}const i=e.dict.get("Lang");"string"==typeof i&&(n.lang=stringToPDFString(i));for(const t of e.kids){const e=t.type===Gi?t.parentNode:null;e?nodeToSerializable(e,n,a+1):t.type===zi||t.type===Xi?n.children.push({type:"content",id:`p${t.pageObjId}_mc${t.mcid}`}):t.type===Wi?n.children.push({type:"object",id:t.refObjId}):t.type===Ki&&n.children.push({type:"annotation",id:`pdfjs_internal_id_${t.refObjId}`})}}const e=Object.create(null);e.children=[];e.role="Root";for(const t of this.nodes)t&&nodeToSerializable(t,e);return e}}const Vi=function _isValidExplicitDest(e,t,a){if(!Array.isArray(a)||a.length<2)return!1;const[n,s,...r]=a;if(!e(n)&&!Number.isInteger(n))return!1;if(!t(s))return!1;const i=r.length;let o=!0;switch(s.name){case"XYZ":if(i<2||i>3)return!1;break;case"Fit":case"FitB":return 0===i;case"FitH":case"FitBH":case"FitV":case"FitBV":if(i>1)return!1;break;case"FitR":if(4!==i)return!1;o=!1;break;default:return!1}for(const e of r)if(!("number"==typeof e||o&&null===e))return!1;return!0}.bind(null,e=>e instanceof Ref,isName);function fetchDest(e){e instanceof Dict&&(e=e.get("D"));return Vi(e)?e:null}function fetchRemoteDest(e){let t=e.get("D");if(t){t instanceof Name&&(t=t.name);if("string"==typeof t)return stringToPDFString(t,!0);if(Vi(t))return JSON.stringify(t)}return null}class Catalog{#Ve=null;#$e=null;builtInCMapCache=new Map;fontCache=new RefSetCache;globalColorSpaceCache=new GlobalColorSpaceCache;globalImageCache=new GlobalImageCache;nonBlendModesSet=new RefSet;pageDictCache=new RefSetCache;pageIndexCache=new RefSetCache;pageKidsCountCache=new RefSetCache;standardFontDataCache=new Map;systemFontCache=new Map;constructor(e,t){this.pdfManager=e;this.xref=t;this.#$e=t.getCatalogObj();if(!(this.#$e instanceof Dict))throw new FormatError("Catalog object is not a dictionary.");this.toplevelPagesDict}cloneDict(){return this.#$e.clone()}get version(){const e=this.#$e.get("Version");if(e instanceof Name){if(ua.test(e.name))return shadow(this,"version",e.name);warn(`Invalid PDF catalog version: ${e.name}`)}return shadow(this,"version",null)}get lang(){const e=this.#$e.get("Lang");return shadow(this,"lang",e&&"string"==typeof e?stringToPDFString(e):null)}get needsRendering(){const e=this.#$e.get("NeedsRendering");return shadow(this,"needsRendering","boolean"==typeof e&&e)}get collection(){let e=null;try{const t=this.#$e.get("Collection");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch Collection entry; assuming no collection is present.")}return shadow(this,"collection",e)}get acroForm(){let e=null;try{const t=this.#$e.get("AcroForm");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch AcroForm entry; assuming no forms are present.")}return shadow(this,"acroForm",e)}get acroFormRef(){const e=this.#$e.getRaw("AcroForm");return shadow(this,"acroFormRef",e instanceof Ref?e:null)}get metadata(){const e=this.#$e.getRaw("Metadata");if(!(e instanceof Ref))return shadow(this,"metadata",null);let t=null;try{const a=this.xref.fetch(e,!this.xref.encrypt?.encryptMetadata);if(a instanceof BaseStream&&a.dict instanceof Dict){const e=a.dict.get("Type"),n=a.dict.get("Subtype");if(isName(e,"Metadata")&&isName(n,"XML")){const e=stringToUTF8String(a.getString());e&&(t=new MetadataParser(e).serializable)}}}catch(e){if(e instanceof MissingDataException)throw e;info(`Skipping invalid Metadata: "${e}".`)}return shadow(this,"metadata",t)}get markInfo(){let e=null;try{e=this.#Ye()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read mark info.")}return shadow(this,"markInfo",e)}#Ye(){const e=this.#$e.get("MarkInfo");if(!(e instanceof Dict))return null;const t={Marked:!1,UserProperties:!1,Suspects:!1};for(const a in t){const n=e.get(a);"boolean"==typeof n&&(t[a]=n)}return t}get hasStructTree(){return this.#$e.has("StructTreeRoot")}get structTreeRoot(){let e=null;try{e=this.#Je()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable read to structTreeRoot info.")}return shadow(this,"structTreeRoot",e)}#Je(){const e=this.#$e.getRaw("StructTreeRoot"),t=this.xref.fetchIfRef(e);if(!(t instanceof Dict))return null;const a=new StructTreeRoot(this.xref,t,e);a.init();return a}get toplevelPagesDict(){const e=this.#$e.get("Pages");if(!(e instanceof Dict))throw new FormatError("Invalid top-level pages dictionary.");return shadow(this,"toplevelPagesDict",e)}get documentOutline(){let e=null;try{e=this.#Qe()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutline",e)}#Qe(e={}){let t=this.#$e.get("Outlines");if(!(t instanceof Dict))return null;t=t.getRaw("First");if(!(t instanceof Ref))return null;const a={items:[]},n=[{obj:t,parent:a}],s=new RefSet;s.put(t);const r=this.xref,i=new Uint8ClampedArray(3);for(;n.length>0;){const a=n.shift(),o=r.fetchIfRef(a.obj);if(null===o)continue;o.has("Title")||warn("Invalid outline item encountered.");const l={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:o,resultObj:l,docBaseUrl:this.baseUrl,docAttachments:this.attachments});const f=o.get("Title"),c=o.get("F")||0,h=o.getArray("C"),u=o.get("Count");let m=i;!isNumberArray(h,3)||0===h[0]&&0===h[1]&&0===h[2]||(m=ColorSpaceUtils.rgb.getRgb(h,0));const p={action:l.action,attachment:l.attachment,dest:l.dest,url:l.url,unsafeUrl:l.unsafeUrl,newWindow:l.newWindow,setOCGState:l.setOCGState,title:"string"==typeof f?stringToPDFString(f):"",color:m,count:Number.isInteger(u)?u:void 0,bold:!!(2&c),italic:!!(1&c),items:[]};e.keepRawDict&&(p.rawDict=o);a.parent.items.push(p);t=o.getRaw("First");if(t instanceof Ref&&!s.has(t)){n.push({obj:t,parent:p});s.put(t)}t=o.getRaw("Next");if(t instanceof Ref&&!s.has(t)){n.push({obj:t,parent:a.parent});s.put(t)}}return a.items.length>0?a.items:null}get documentOutlineForEditor(){let e=null;try{e=this.#Qe({keepRawDict:!0})}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutlineForEditor",e)}get permissions(){let e=null;try{e=this.#Ze()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read permissions.")}return shadow(this,"permissions",e)}#Ze(){const e=this.xref.trailer.get("Encrypt");if(!(e instanceof Dict))return null;let t=e.get("P");if("number"!=typeof t)return null;t+=2**32;const a=[];for(const e in y){const n=y[e];t&n&&a.push(n)}return a}get optionalContentConfig(){let e=null;try{const t=this.#$e.get("OCProperties");if(!t)return shadow(this,"optionalContentConfig",null);const a=t.get("D");if(!a)return shadow(this,"optionalContentConfig",null);const n=t.get("OCGs");if(!Array.isArray(n))return shadow(this,"optionalContentConfig",null);const s=new RefSetCache;for(const e of n)e instanceof Ref&&!s.has(e)&&s.put(e,this.#et(e));e=this.#tt(a,s)}catch(e){if(e instanceof MissingDataException)throw e;warn(`Unable to read optional content config: ${e}`)}return shadow(this,"optionalContentConfig",e)}#et(e){const t=this.xref.fetch(e),a={id:e.toString(),name:null,intent:null,usage:{print:null,view:null},rbGroups:[]},n=t.get("Name");"string"==typeof n&&(a.name=stringToPDFString(n));let s=t.getArray("Intent");Array.isArray(s)||(s=[s]);s.every(e=>e instanceof Name)&&(a.intent=s.map(e=>e.name));const r=t.get("Usage");if(!(r instanceof Dict))return a;const i=a.usage,o=r.get("Print");if(o instanceof Dict){const e=o.get("PrintState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":i.print={printState:e.name}}}const l=r.get("View");if(l instanceof Dict){const e=l.get("ViewState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":i.view={viewState:e.name}}}return a}#tt(e,t){function parseOnOff(e){const a=[];if(Array.isArray(e))for(const n of e)n instanceof Ref&&t.has(n)&&a.push(n.toString());return a}function parseOrder(e,a=0){if(!Array.isArray(e))return null;const s=[];for(const r of e){if(r instanceof Ref&&t.has(r)){n.put(r);s.push(r.toString());continue}const e=parseNestedOrder(r,a);e&&s.push(e)}if(a>0)return s;const r=[];for(const[e]of t.items())n.has(e)||r.push(e.toString());r.length&&s.push({name:null,order:r});return s}function parseNestedOrder(e,t){if(++t>s){warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");return null}const n=a.fetchIfRef(e);if(!Array.isArray(n))return null;const r=a.fetchIfRef(n[0]);if("string"!=typeof r)return null;const i=parseOrder(n.slice(1),t);return i?.length?{name:stringToPDFString(r),order:i}:null}const a=this.xref,n=new RefSet,s=10;!function parseRBGroups(e){if(Array.isArray(e))for(const n of e){const e=a.fetchIfRef(n);if(!Array.isArray(e)||!e.length)continue;const s=new Set;for(const a of e)if(a instanceof Ref&&t.has(a)&&!s.has(a.toString())){s.add(a.toString());t.get(a).rbGroups.push(s)}}}(e.get("RBGroups"));return{name:"string"==typeof e.get("Name")?stringToPDFString(e.get("Name")):null,creator:"string"==typeof e.get("Creator")?stringToPDFString(e.get("Creator")):null,baseState:e.get("BaseState")instanceof Name?e.get("BaseState").name:null,on:parseOnOff(e.get("ON")),off:parseOnOff(e.get("OFF")),order:parseOrder(e.get("Order")),groups:[...t]}}setActualNumPages(e=null){this.#Ve=e}get hasActualNumPages(){return null!==this.#Ve}get _pagesCount(){const e=this.toplevelPagesDict.get("Count");if(!Number.isInteger(e))throw new FormatError("Page count in top-level pages dictionary is not an integer.");return shadow(this,"_pagesCount",e)}get numPages(){return this.#Ve??this._pagesCount}get destinations(){const e=this.#at(),t=Object.create(null);for(const a of e)if(a instanceof NameTree)for(const[e,n]of a.getAll()){const a=fetchDest(n);a&&(t[stringToPDFString(e,!0)]=a)}else if(a instanceof Dict)for(const[e,n]of a){const a=fetchDest(n);a&&(t[stringToPDFString(e,!0)]||=a)}return shadow(this,"destinations",t)}getDestination(e){if(Object.hasOwn(this,"destinations"))return this.destinations[e]??null;const t=this.#at();for(const a of t)if(a instanceof NameTree||a instanceof Dict){const t=fetchDest(a.get(e));if(t)return t}if(t.length){const t=this.destinations[e];if(t)return t}return null}#at(){const e=this.#$e.get("Names"),t=[];e?.has("Dests")&&t.push(new NameTree(e.getRaw("Dests"),this.xref));this.#$e.has("Dests")&&t.push(this.#$e.get("Dests"));return t}get rawPageLabels(){const e=this.#$e.getRaw("PageLabels");if(!e)return null;return new NumberTree(e,this.xref).getAll()}get pageLabels(){let e=null;try{e=this.#nt()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read page labels.")}return shadow(this,"pageLabels",e)}#nt(){const e=this.rawPageLabels;if(!e)return null;const t=new Array(this.numPages);let a=null,n="",s="",r=1;for(let i=0,o=this.numPages;i=1))throw new FormatError("Invalid start in PageLabel dictionary.");r=e}else r=1}switch(a){case"D":s=r;break;case"R":case"r":s=toRomanNumerals(r,"r"===a);break;case"A":case"a":const e=26,t="a"===a?97:65,n=r-1;s=String.fromCharCode(t+n%e).repeat(Math.floor(n/e)+1);break;default:if(a)throw new FormatError(`Invalid style "${a}" in PageLabel dictionary.`);s=""}t[i]=n+s;r++}return t}get pageLayout(){const e=this.#$e.get("PageLayout");let t="";if(e instanceof Name)switch(e.name){case"SinglePage":case"OneColumn":case"TwoColumnLeft":case"TwoColumnRight":case"TwoPageLeft":case"TwoPageRight":t=e.name}return shadow(this,"pageLayout",t)}get pageMode(){const e=this.#$e.get("PageMode");let t="UseNone";if(e instanceof Name)switch(e.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"FullScreen":case"UseOC":case"UseAttachments":t=e.name}return shadow(this,"pageMode",t)}get viewerPreferences(){const e=this.#$e.get("ViewerPreferences");if(!(e instanceof Dict))return shadow(this,"viewerPreferences",null);let t=null;for(const[a,n]of e){let e;switch(a){case"HideToolbar":case"HideMenubar":case"HideWindowUI":case"FitWindow":case"CenterWindow":case"DisplayDocTitle":case"PickTrayByPDFSize":"boolean"==typeof n&&(e=n);break;case"NonFullScreenPageMode":if(n instanceof Name)switch(n.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"UseOC":e=n.name;break;default:e="UseNone"}break;case"Direction":if(n instanceof Name)switch(n.name){case"L2R":case"R2L":e=n.name;break;default:e="L2R"}break;case"ViewArea":case"ViewClip":case"PrintArea":case"PrintClip":if(n instanceof Name)switch(n.name){case"MediaBox":case"CropBox":case"BleedBox":case"TrimBox":case"ArtBox":e=n.name;break;default:e="CropBox"}break;case"PrintScaling":if(n instanceof Name)switch(n.name){case"None":case"AppDefault":e=n.name;break;default:e="AppDefault"}break;case"Duplex":if(n instanceof Name)switch(n.name){case"Simplex":case"DuplexFlipShortEdge":case"DuplexFlipLongEdge":e=n.name;break;default:e="None"}break;case"PrintPageRange":if(Array.isArray(n)&&n.length%2==0){n.every((e,t,a)=>Number.isInteger(e)&&e>0&&(0===t||e>=a[t-1])&&e<=this.numPages)&&(e=n)}break;case"NumCopies":Number.isInteger(n)&&n>0&&(e=n);break;default:warn(`Ignoring non-standard key in ViewerPreferences: ${a}.`);continue}if(void 0!==e){t??=Object.create(null);t[a]=e}else warn(`Bad value, for key "${a}", in ViewerPreferences: ${n}.`)}return shadow(this,"viewerPreferences",t)}get openAction(){const e=this.#$e.get("OpenAction"),t=Object.create(null);if(e instanceof Dict){const a=new Dict(this.xref);a.set("A",e);const n={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:a,resultObj:n});Array.isArray(n.dest)?t.dest=n.dest:n.action&&(t.action=n.action)}else Vi(e)&&(t.dest=e);return shadow(this,"openAction",objectSize(t)>0?t:null)}get attachments(){const e=this.#$e.get("Names");let t=null;if(e instanceof Dict&&e.has("EmbeddedFiles")){const a=new NameTree(e.getRaw("EmbeddedFiles"),this.xref);for(const[e,n]of a.getAll()){const a=new FileSpec(n);t??=Object.create(null);t[stringToPDFString(e,!0)]=a.serializable}}return shadow(this,"attachments",t)}get rawEmbeddedFiles(){const e=this.#$e.get("Names");if(!(e instanceof Dict&&e.has("EmbeddedFiles")))return null;return new NameTree(e.getRaw("EmbeddedFiles"),this.xref).getAll(!0)}get xfaImages(){const e=this.#$e.get("Names");let t=null;if(e instanceof Dict&&e.has("XFAImages")){const a=new NameTree(e.getRaw("XFAImages"),this.xref);for(const[e,n]of a.getAll())if(n instanceof BaseStream){t??=new Map;t.set(stringToPDFString(e,!0),n.getBytes())}}return shadow(this,"xfaImages",t)}#st(){const e=this.#$e.get("Names");let t=null;function appendIfJavaScriptDict(e,a){if(!(a instanceof Dict))return;if(!isName(a.get("S"),"JavaScript"))return;let n=a.get("JS");if(n instanceof BaseStream)n=n.getString();else if("string"!=typeof n)return;n=stringToPDFString(n,!0).replaceAll("\0","");n&&(t||=new Map).set(e,n)}if(e instanceof Dict&&e.has("JavaScript")){const t=new NameTree(e.getRaw("JavaScript"),this.xref);for(const[e,a]of t.getAll())appendIfJavaScriptDict(stringToPDFString(e,!0),a)}const a=this.#$e.get("OpenAction");a&&appendIfJavaScriptDict("OpenAction",a);return t}get jsActions(){const e=this.#st();let t=collectActions(this.xref,this.#$e,oe);if(e){t||=Object.create(null);for(const[a,n]of e)a in t?t[a].push(n):t[a]=[n]}return shadow(this,"jsActions",t)}async cleanup(e=!1){clearGlobalCaches();this.globalColorSpaceCache.clear();this.globalImageCache.clear(e);this.pageKidsCountCache.clear();this.pageIndexCache.clear();this.pageDictCache.clear();this.nonBlendModesSet.clear();for(const{dict:e}of await Promise.all(this.fontCache))delete e.cacheKey;this.fontCache.clear();this.builtInCMapCache.clear();this.standardFontDataCache.clear();this.systemFontCache.clear()}async getPageDict(e){const t=[this.toplevelPagesDict],a=new RefSet,n=this.#$e.getRaw("Pages");n instanceof Ref&&a.put(n);const s=this.xref,r=this.pageKidsCountCache,i=this.pageIndexCache,o=this.pageDictCache;let l=0;for(;t.length;){const n=t.pop();if(n instanceof Ref){const f=r.get(n);if(f>=0&&l+f<=e){l+=f;continue}if(a.has(n))throw new FormatError("Pages tree contains circular reference.");a.put(n);const c=await(o.get(n)||s.fetchAsync(n));if(c instanceof Dict){let t=c.getRaw("Type");t instanceof Ref&&(t=await s.fetchAsync(t));if(isName(t,"Page")||!c.has("Kids")){r.has(n)||r.put(n,1);i.has(n)||i.put(n,l);if(l===e)return[c,n];l++;continue}}t.push(c);continue}if(!(n instanceof Dict))throw new FormatError("Page dictionary kid reference points to wrong type of object.");const{objId:f}=n;let c=n.getRaw("Count");c instanceof Ref&&(c=await s.fetchAsync(c));if(Number.isInteger(c)&&c>=0){f&&!r.has(f)&&r.put(f,c);if(l+c<=e){l+=c;continue}}let h=n.getRaw("Kids");h instanceof Ref&&(h=await s.fetchAsync(h));if(!Array.isArray(h)){let t=n.getRaw("Type");t instanceof Ref&&(t=await s.fetchAsync(t));if(isName(t,"Page")||!n.has("Kids")){if(l===e)return[n,null];l++;continue}throw new FormatError("Page dictionary kids object is not an array.")}for(let e=h.length-1;e>=0;e--){const a=h[e];t.push(a);n===this.toplevelPagesDict&&a instanceof Ref&&!o.has(a)&&o.put(a,s.fetchAsync(a))}}throw new Error(`Page index ${e} not found.`)}async getAllPageDicts(e=!1){const{ignoreErrors:t}=this.pdfManager.evaluatorOptions,a=[{currentNode:this.toplevelPagesDict,posInKids:0}],n=new RefSet,s=this.#$e.getRaw("Pages");s instanceof Ref&&n.put(s);const r=new Map,i=this.xref,o=this.pageIndexCache;let l=0;function addPageDict(e,t){t&&!o.has(t)&&o.put(t,l);r.set(l++,[e,t])}function addPageError(a){if(a instanceof XRefEntryException&&!e)throw a;if(e&&t&&0===l){warn(`getAllPageDicts - Skipping invalid first page: "${a}".`);a=Dict.empty}r.set(l++,[a,null])}for(;a.length>0;){const e=a.at(-1),{currentNode:t,posInKids:s}=e;let r=t.getRaw("Kids");if(r instanceof Ref)try{r=await i.fetchAsync(r)}catch(e){addPageError(e);break}if(!Array.isArray(r)){addPageError(new FormatError("Page dictionary kids object is not an array."));break}if(s>=r.length){a.pop();continue}const o=r[s];let l;if(o instanceof Ref){if(n.has(o)){addPageError(new FormatError("Pages tree contains circular reference."));break}n.put(o);try{l=await i.fetchAsync(o)}catch(e){addPageError(e);break}}else l=o;if(!(l instanceof Dict)){addPageError(new FormatError("Page dictionary kid reference points to wrong type of object."));break}let f=l.getRaw("Type");if(f instanceof Ref)try{f=await i.fetchAsync(f)}catch(e){addPageError(e);break}isName(f,"Page")||!l.has("Kids")?addPageDict(l,o instanceof Ref?o:null):a.push({currentNode:l,posInKids:0});e.posInKids++}return r}getPageIndex(e){const t=this.pageIndexCache.get(e);if(void 0!==t)return Promise.resolve(t);const a=this.xref;let n=0;const next=t=>function pagesBeforeRef(t){let n,s=0;return a.fetchAsync(t).then(function(a){if(isRefsEqual(t,e)&&!isDict(a,"Page")&&!(a instanceof Dict&&!a.has("Type")&&a.has("Contents")))throw new FormatError("The reference does not point to a /Page dictionary.");if(!a)return null;if(!(a instanceof Dict))throw new FormatError("Node must be a dictionary.");n=a.getRaw("Parent");return a.getAsync("Parent")}).then(function(e){if(!e)return null;if(!(e instanceof Dict))throw new FormatError("Parent must be a dictionary.");return e.getAsync("Kids")}).then(function(e){if(!e)return null;const r=[];let i=!1;for(const n of e){if(!(n instanceof Ref))throw new FormatError("Kid must be a reference.");if(isRefsEqual(n,t)){i=!0;break}r.push(a.fetchAsync(n).then(function(e){if(!(e instanceof Dict))throw new FormatError("Kid node must be a dictionary.");e.has("Count")?s+=e.get("Count"):s++}))}if(!i)throw new FormatError("Kid reference not found in parent's kids.");return Promise.all(r).then(()=>[s,n])})}(t).then(t=>{if(!t){this.pageIndexCache.put(e,n);return n}const[a,s]=t;n+=a;return next(s)});return next(e)}get baseUrl(){const e=this.#$e.get("URI");if(e instanceof Dict){const t=e.get("Base");if("string"==typeof t){const e=createValidAbsoluteUrl(t,null,{tryConvertEncoding:!0});if(e)return shadow(this,"baseUrl",e.href)}}return shadow(this,"baseUrl",this.pdfManager.docBaseUrl)}static#rt(e,t){const a=e.fetchIfRef(t);if(!(a instanceof Dict))return null;let n=null;const s=a.getRaw("Pg");s instanceof Ref&&(n=s);if(!n){const t=[a];for(;t.length>0&&!n;){const a=t.shift().get("K");let s;if(Array.isArray(a))s=a;else{if(!a)continue;s=[a]}for(const a of s){const s=e.fetchIfRef(a);if(!(s instanceof Dict))continue;const r=s.getRaw("Pg");if(r instanceof Ref){n=r;break}t.push(s)}}}if(!n){const t=40;let s=a;for(let a=0;ae.startsWith("http://www.xfa.org/schema/xci/")},connectionSet:{id:1,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-connection-set/")},datasets:{id:2,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-data/")},form:{id:3,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-form/")},localeSet:{id:4,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-locale-set/")},pdf:{id:5,check:e=>"http://ns.adobe.com/xdp/pdf/"===e},signature:{id:6,check:e=>"http://www.w3.org/2000/09/xmldsig#"===e},sourceSet:{id:7,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-source-set/")},stylesheet:{id:8,check:e=>"http://www.w3.org/1999/XSL/Transform"===e},template:{id:9,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-template/")},xdc:{id:10,check:e=>e.startsWith("http://www.xfa.org/schema/xdc/")},xdp:{id:11,check:e=>"http://ns.adobe.com/xdp/"===e},xfdf:{id:12,check:e=>"http://ns.adobe.com/xfdf/"===e},xhtml:{id:13,check:e=>"http://www.w3.org/1999/xhtml"===e},xmpmeta:{id:14,check:e=>"http://ns.adobe.com/xmpmeta/"===e}},ml={pt:e=>e,cm:e=>e/2.54*72,mm:e=>e/25.4*72,in:e=>72*e,px:e=>e},pl=/([+-]?\d+\.?\d*)(.*)/;function stripQuotes(e){return e.startsWith("'")||e.startsWith('"')?e.slice(1,-1):e}function getInteger({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const n=parseInt(e,10);return!isNaN(n)&&a(n)?n:t}function getFloat({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const n=parseFloat(e);return!isNaN(n)&&a(n)?n:t}function getKeyword({data:e,defaultValue:t,validate:a}){return e&&a(e=e.trim())?e:t}function getStringOption(e,t){return getKeyword({data:e,defaultValue:t[0],validate:e=>t.includes(e)})}function getMeasurement(e,t="0"){t||="0";if(!e)return getMeasurement(t);const a=e.trim().match(pl);if(!a)return getMeasurement(t);const[,n,s]=a,r=parseFloat(n);if(isNaN(r))return getMeasurement(t);if(0===r)return 0;const i=ml[s];return i?i(r):r}function getRatio(e){if(!e)return{num:1,den:1};const t=e.split(":",2).map(e=>parseFloat(e.trim())).filter(e=>!isNaN(e));1===t.length&&t.push(1);if(0===t.length)return{num:1,den:1};const[a,n]=t;return{num:a,den:n}}function getRelevant(e){return e?e.trim().split(/\s+/).map(e=>({excluded:"-"===e[0],viewname:e.substring(1)})):[]}class HTMLResult{static get FAILURE(){return shadow(this,"FAILURE",new HTMLResult(!1,null,null,null))}static get EMPTY(){return shadow(this,"EMPTY",new HTMLResult(!0,null,null,null))}constructor(e,t,a,n){this.success=e;this.html=t;this.bbox=a;this.breakNode=n}isBreak(){return!!this.breakNode}static breakNode(e){return new HTMLResult(!1,null,null,e)}static success(e,t=null){return new HTMLResult(!0,e,t,null)}}class FontFinder{constructor(e){this.fonts=new Map;this.cache=new Map;this.warned=new Set;this.defaultFont=null;this.add(e)}add(e,t=null){for(const t of e)this.addPdfFont(t);for(const e of this.fonts.values())e.regular||(e.regular=e.italic||e.bold||e.bolditalic);if(!t||0===t.size)return;const a=this.fonts.get("PdfJS-Fallback-PdfJS-XFA");for(const e of t)this.fonts.set(e,a)}addPdfFont(e){const t=e.cssFontInfo,a=t.fontFamily,n=this.fonts.getOrInsertComputed(a,makeObj);this.defaultFont??=n;let s="";const r=parseFloat(t.fontWeight);0!==parseFloat(t.italicAngle)?s=r>=700?"bolditalic":"italic":r>=700&&(s="bold");if(!s){(e.name.includes("Bold")||e.psName?.includes("Bold"))&&(s="bold");(e.name.includes("Italic")||e.name.endsWith("It")||e.psName?.includes("Italic")||e.psName?.endsWith("It"))&&(s+="italic")}s||(s="regular");n[s]=e}getDefault(){return this.defaultFont}find(e,t=!0){let a=this.fonts.get(e)||this.cache.get(e);if(a)return a;const n=/,|-|_| |bolditalic|bold|italic|regular|it/gi;let s=e.replaceAll(n,"");a=this.fonts.get(s);if(a){this.cache.set(e,a);return a}s=s.toLowerCase();const r=[];for(const[e,t]of this.fonts)e.replaceAll(n,"").toLowerCase().startsWith(s)&&r.push(t);if(0===r.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(n,"").toLowerCase().startsWith(s)&&r.push(e);if(0===r.length){s=s.replaceAll(/psmt|mt/gi,"");for(const[e,t]of this.fonts)e.replaceAll(n,"").toLowerCase().startsWith(s)&&r.push(t)}if(0===r.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(n,"").toLowerCase().startsWith(s)&&r.push(e);if(r.length>=1){1!==r.length&&t&&warn(`XFA - Too many choices to guess the correct font: ${e}`);this.cache.set(e,r[0]);return r[0]}if(t&&!this.warned.has(e)){this.warned.add(e);warn(`XFA - Cannot find the font: ${e}`)}return null}}function selectFont(e,t){return"italic"===e.posture?"bold"===e.weight?t.bolditalic:t.italic:"bold"===e.weight?t.bold:t.regular}class FontInfo{constructor(e,t,a,n){this.lineHeight=a;this.paraMargin=t||{top:0,bottom:0,left:0,right:0};if(!e){[this.pdfFont,this.xfaFont]=this.defaultFont(n);return}this.xfaFont={typeface:e.typeface,posture:e.posture,weight:e.weight,size:e.size,letterSpacing:e.letterSpacing};const s=n.find(e.typeface);if(s){this.pdfFont=selectFont(e,s);this.pdfFont||([this.pdfFont,this.xfaFont]=this.defaultFont(n))}else[this.pdfFont,this.xfaFont]=this.defaultFont(n)}defaultFont(e){const t=e.find("Helvetica",!1)||e.find("Myriad Pro",!1)||e.find("Arial",!1)||e.getDefault();if(t?.regular){const e=t.regular;return[e,{typeface:e.cssFontInfo.fontFamily,posture:"normal",weight:"normal",size:10,letterSpacing:0}]}return[null,{typeface:"Courier",posture:"normal",weight:"normal",size:10,letterSpacing:0}]}}class FontSelector{constructor(e,t,a,n){this.fontFinder=n;this.stack=[new FontInfo(e,t,a,n)]}pushData(e,t,a){const n=this.stack.at(-1);for(const t of["typeface","posture","weight","size","letterSpacing"])e[t]||=n.xfaFont[t];for(const e of["top","bottom","left","right"])isNaN(t[e])&&(t[e]=n.paraMargin[e]);const s=new FontInfo(e,t,a||n.lineHeight,this.fontFinder);s.pdfFont||=n.pdfFont;this.stack.push(s)}popFont(){this.stack.pop()}topFont(){return this.stack.at(-1)}}class TextMeasure{constructor(e,t,a,n){this.glyphs=[];this.fontSelector=new FontSelector(e,t,a,n);this.extraHeight=0}pushData(e,t,a){this.fontSelector.pushData(e,t,a)}popFont(e){return this.fontSelector.popFont()}addPara(){const e=this.fontSelector.topFont();this.extraHeight+=e.paraMargin.top+e.paraMargin.bottom}addString(e){if(!e)return;const t=this.fontSelector.topFont(),a=t.xfaFont.size;if(t.pdfFont){const n=t.xfaFont.letterSpacing,s=t.pdfFont,r=s.lineHeight||1.2,i=t.lineHeight||Math.max(1.2,r)*a,o=r-(void 0===s.lineGap?.2:s.lineGap),l=Math.max(1,o)*a,f=a/1e3,c=s.defaultWidth||s.charsToGlyphs(" ")[0].width;for(const t of e.split(/[\u2029\n]/)){const e=s.encodeString(t).join(""),a=s.charsToGlyphs(e);for(const e of a){const t=e.width||c;this.glyphs.push([t*f+n,i,l,e.unicode,!1])}this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop();return}for(const t of e.split(/[\u2029\n]/)){for(const e of t.split(""))this.glyphs.push([a,1.2*a,a,e,!1]);this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}compute(e){let t=-1,a=0,n=0,s=0,r=0,i=0,o=!1,l=!0;for(let f=0,c=this.glyphs.length;fe){n=Math.max(n,r);r=0;s+=i;i=g;t=-1;a=0;o=!0;l=!1}else{i=Math.max(g,i);a=r;r+=c;t=f}else if(r+c>e){s+=i;i=g;if(-1!==t){f=t;n=Math.max(n,a);r=0;t=-1;a=0}else{n=Math.max(n,r);r=c}o=!0;l=!1}else{r+=c;i=Math.max(g,i)}}n=Math.max(n,r);s+=i+this.extraHeight;return{width:1.02*n,height:s,isBroken:o}}}const dl=/^[^.[]+/,gl=/^[^\]]+/,bl=0,wl=1,jl=2,kl=3,yl=4,ql=new Map([["$data",(e,t)=>e.datasets?e.datasets.data:e],["$record",(e,t)=>(e.datasets?e.datasets.data:e)[ko]()[0]],["$template",(e,t)=>e.template],["$connectionSet",(e,t)=>e.connectionSet],["$form",(e,t)=>e.form],["$layout",(e,t)=>e.layout],["$host",(e,t)=>e.host],["$dataWindow",(e,t)=>e.dataWindow],["$event",(e,t)=>e.event],["!",(e,t)=>e.datasets],["$xfa",(e,t)=>e],["xfa",(e,t)=>e],["$",(e,t)=>t]]),vl=new WeakMap;function parseIndex(e){return"*"===(e=e.trim())?1/0:parseInt(e,10)||0}function parseExpression(e,t,a=!0){let n=e.match(dl);if(!n)return null;let[s]=n;const r=[{name:s,cacheName:"."+s,index:0,js:null,formCalc:null,operator:bl}];let i=s.length;for(;i0&&c.push(e)}if(0===c.length&&!o&&0===l){const a=t[So]();if(!(t=a))return null;l=-1;e=[t];continue}e=isFinite(f)?c.filter(e=>fe[f]):c.flat()}return 0===e.length?null:e}function createDataNode(e,t,a){const n=parseExpression(a);if(!n)return null;if(n.some(e=>e.operator===wl))return null;const s=ql.get(n[0].name);let r=0;if(s){e=s(e,t);r=1}else e=t||e;for(let t=n.length;re[rl]()).join("")}get[xl](){const e=Object.getPrototypeOf(this);if(!e._attributes){const t=e._attributes=new Set;for(const e of Object.getOwnPropertyNames(this)){if(null===this[e]||this[e]instanceof XFAObject||this[e]instanceof XFAObjectArray)break;t.add(e)}}return shadow(this,xl,e._attributes)}[Do](e){let t=this;for(;t;){if(t===e)return!0;t=t[So]()}return!1}[So](){return this[Pl]}[vo](){return this[So]()}[ko](e=null){return e?this[e]:this[Cl]}[io](){const e=Object.create(null);this[so]&&(e.$content=this[so]);for(const t of Object.getOwnPropertyNames(this)){const a=this[t];null!==a&&(a instanceof XFAObject?e[t]=a[io]():a instanceof XFAObjectArray?a.isEmpty()||(e[t]=a.dump()):e[t]=a)}return e}[fl](){return null}[ol](){return HTMLResult.EMPTY}*[yo](){for(const e of this[ko]())yield e}*[Rl](e,t){for(const a of this[yo]())if(!e||t===e.has(a[Uo])){const e=this[uo](),t=a[ol](e);t.success||(this[oo].failingNode=a);yield t}}[fo](){return null}[Yi](e,t){this[oo].children.push(e)}[uo](){}[Qi]({filter:e=null,include:t=!0}){if(this[oo].generator){const e=this[uo](),t=this[oo].failingNode[ol](e);if(!t.success)return t;t.html&&this[Yi](t.html,t.bbox);delete this[oo].failingNode}else this[oo].generator=this[Rl](e,t);for(;;){const e=this[oo].generator.next();if(e.done)break;const t=e.value;if(!t.success)return t;t.html&&this[Yi](t.html,t.bbox)}this[oo].generator=null;return HTMLResult.EMPTY}[al](e){this[_l]=new Set(Object.keys(e))}[Bl](e){const t=this[xl],a=this[_l];return[...e].filter(e=>t.has(e)&&!a.has(e))}[Zo](e,t=new Set){for(const a of this[Cl])a[El](e,t)}[El](e,t){const a=this[Hl](e,t);a?this[Sl](a,e,t):this[Zo](e,t)}[Hl](e,t){const{use:a,usehref:n}=this;if(!a&&!n)return null;let s=null,r=null,i=null,o=a;if(n){o=n;n.startsWith("#som(")&&n.endsWith(")")?r=n.slice(5,-1):n.startsWith(".#som(")&&n.endsWith(")")?r=n.slice(6,-1):n.startsWith("#")?i=n.slice(1):n.startsWith(".#")&&(i=n.slice(2))}else a.startsWith("#")?i=a.slice(1):r=a;this.use=this.usehref="";if(i)s=e.get(i);else{s=searchNode(e.get(Qo),this,r,!0,!1);s&&(s=s[0])}if(!s){warn(`XFA - Invalid prototype reference: ${o}.`);return null}if(s[Uo]!==this[Uo]){warn(`XFA - Incompatible prototype: ${s[Uo]} !== ${this[Uo]}.`);return null}if(t.has(s)){warn("XFA - Cycle detected in prototypes use.");return null}t.add(s);const l=s[Hl](e,t);l&&s[Sl](l,e,t);s[Zo](e,t);t.delete(s);return s}[Sl](e,t,a){if(a.has(e)){warn("XFA - Cycle detected in prototypes use.");return}!this[so]&&e[so]&&(this[so]=e[so]);new Set(a).add(e);for(const t of this[Bl](e[_l])){this[t]=e[t];this[_l]&&this[_l].add(t)}for(const n of Object.getOwnPropertyNames(this)){if(this[xl].has(n))continue;const s=this[n],r=e[n];if(s instanceof XFAObjectArray){for(const e of s[Cl])e[El](t,a);for(let n=s[Cl].length,i=r[Cl].length;nXFAObject[Il](e)):"object"==typeof e&&null!==e?Object.assign({},e):e}[ao](){const e=Object.create(Object.getPrototypeOf(this));for(const t of Object.getOwnPropertySymbols(this))try{e[t]=this[t]}catch{shadow(e,t,this[t])}e[cl]=`${e[Uo]}${Ll++}`;e[Cl]=[];for(const t of Object.getOwnPropertyNames(this)){if(this[xl].has(t)){e[t]=XFAObject[Il](this[t]);continue}const a=this[t];e[t]=a instanceof XFAObjectArray?new XFAObjectArray(a[Ml]):null}for(const t of this[Cl]){const a=t[Uo],n=t[ao]();e[Cl].push(n);n[Pl]=e;null===e[a]?e[a]=n:e[a][Cl].push(n)}return e}[ko](e=null){return e?this[Cl].filter(t=>t[Uo]===e):this[Cl]}[mo](e){return this[e]}[po](e,t,a=!0){return Array.from(this[go](e,t,a))}*[go](e,t,a=!0){if("parent"!==e){for(const a of this[Cl]){a[Uo]===e&&(yield a);a.name===e&&(yield a);(t||a[Eo]())&&(yield*a[go](e,t,!1))}a&&this[xl].has(e)&&(yield new XFAAttribute(this,e,this[e]))}else yield this[Pl]}}class XFAObjectArray{constructor(e=1/0){this[Ml]=e;this[Cl]=[]}get isXFAObject(){return!1}get isXFAObjectArray(){return!0}push(e){if(this[Cl].length<=this[Ml]){this[Cl].push(e);return!0}warn(`XFA - node "${e[Uo]}" accepts no more than ${this[Ml]} children`);return!1}isEmpty(){return 0===this[Cl].length}dump(){return 1===this[Cl].length?this[Cl][0][io]():this[Cl].map(e=>e[io]())}[ao](){const e=new XFAObjectArray(this[Ml]);e[Cl]=this[Cl].map(e=>e[ao]());return e}get children(){return this[Cl]}clear(){this[Cl].length=0}}class XFAAttribute{constructor(e,t,a){this[Pl]=e;this[Uo]=t;this[so]=a;this[no]=!1;this[cl]="attribute"+Ll++}[So](){return this[Pl]}[Bo](){return!0}[bo](){return this[so].trim()}[nl](e){e=e.value||"";this[so]=e.toString()}[rl](){return this[so]}[Do](e){return this[Pl]===e||this[Pl][Do](e)}}class XmlObject extends XFAObject{constructor(e,t,a={}){super(e,t);this[so]="";this[Fl]=null;if("#text"!==t){const e=new Map;this[Al]=e;for(const[t,n]of Object.entries(a))e.set(t,new XFAAttribute(this,t,n));if(Object.hasOwn(a,Xo)){const e=a[Xo].xfa.dataNode;void 0!==e&&("dataGroup"===e?this[Fl]=!1:"dataValue"===e&&(this[Fl]=!0))}}this[no]=!1}[ll](e){const t=this[Uo];if("#text"===t){e.push(encodeToXmlString(this[so]));return}const a=utf8StringToString(t),n=this[Lo]===Xl?"xfa:":"";e.push(`<${n}${a}`);for(const[t,a]of this[Al]){const n=utf8StringToString(t);e.push(` ${n}="${encodeToXmlString(a[so])}"`)}null!==this[Fl]&&(this[Fl]?e.push(' xfa:dataNode="dataValue"'):e.push(' xfa:dataNode="dataGroup"'));if(this[so]||0!==this[Cl].length){e.push(">");if(this[so])"string"==typeof this[so]?e.push(encodeToXmlString(this[so])):this[so][ll](e);else for(const t of this[Cl])t[ll](e);e.push(``)}else e.push("/>")}[Wo](e){if(this[so]){const e=new XmlObject(this[Lo],"#text");this[Ji](e);e[so]=this[so];this[so]=""}this[Ji](e);return!0}[Go](e){this[so]+=e}[lo](){if(this[so]&&this[Cl].length>0){const e=new XmlObject(this[Lo],"#text");this[Ji](e);e[so]=this[so];delete this[so]}}[ol](){return"#text"===this[Uo]?HTMLResult.success({name:"#text",value:this[so]}):HTMLResult.EMPTY}[ko](e=null){return e?this[Cl].filter(t=>t[Uo]===e):this[Cl]}[ho](){return this[Al]}[mo](e){const t=this[Al].get(e);return void 0!==t?t:this[ko](e)}*[go](e,t){const a=this[Al].get(e);a&&(yield a);for(const a of this[Cl]){a[Uo]===e&&(yield a);t&&(yield*a[go](e,t))}}*[co](e,t){const a=this[Al].get(e);!a||t&&a[no]||(yield a);for(const a of this[Cl])yield*a[co](e,t)}*[jo](e,t,a){for(const n of this[Cl]){n[Uo]!==e||a&&n[no]||(yield n);t&&(yield*n[jo](e,t,a))}}[Bo](){return null===this[Fl]?0===this[Cl].length||this[Cl][0][Lo]===ul.xhtml.id:this[Fl]}[bo](){return null===this[Fl]?0===this[Cl].length?this[so].trim():this[Cl][0][Lo]===ul.xhtml.id?this[Cl][0][rl]().trim():null:this[so].trim()}[nl](e){e=e.value||"";this[so]=e.toString()}[io](e=!1){const t=Object.create(null);e&&(t.$ns=this[Lo]);this[so]&&(t.$content=this[so]);t.$name=this[Uo];t.children=[];for(const a of this[Cl])t.children.push(a[io](e));t.attributes=Object.create(null);for(const[e,a]of this[Al])t.attributes[e]=a[so];return t}}class ContentObject extends XFAObject{constructor(e,t){super(e,t);this[so]=""}[Go](e){this[so]+=e}[lo](){}}class OptionObject extends ContentObject{constructor(e,t,a){super(e,t);this[Nl]=a}[lo](){this[so]=getKeyword({data:this[so],defaultValue:this[Nl][0],validate:e=>this[Nl].includes(e)})}[Zi](e){super[Zi](e);delete this[Nl]}}class StringObject extends ContentObject{[lo](){this[so]=this[so].trim()}}class IntegerObject extends ContentObject{constructor(e,t,a,n){super(e,t);this[Tl]=a;this[zl]=n}[lo](){this[so]=getInteger({data:this[so],defaultValue:this[Tl],validate:this[zl]})}[Zi](e){super[Zi](e);delete this[Tl];delete this[zl]}}class Option01 extends IntegerObject{constructor(e,t){super(e,t,0,e=>1===e)}}class Option10 extends IntegerObject{constructor(e,t){super(e,t,1,e=>0===e)}}function measureToString(e){return"string"==typeof e?"0px":Number.isInteger(e)?`${e}px`:`${e.toFixed(2)}px`}const Wl={anchorType(e,t){const a=e[vo]();if(a&&(!a.layout||"position"===a.layout)){"transform"in t||(t.transform="");switch(e.anchorType){case"bottomCenter":t.transform+="translate(-50%, -100%)";break;case"bottomLeft":t.transform+="translate(0,-100%)";break;case"bottomRight":t.transform+="translate(-100%,-100%)";break;case"middleCenter":t.transform+="translate(-50%,-50%)";break;case"middleLeft":t.transform+="translate(0,-50%)";break;case"middleRight":t.transform+="translate(-100%,-50%)";break;case"topCenter":t.transform+="translate(-50%,0)";break;case"topRight":t.transform+="translate(-100%,0)"}}},dimensions(e,t){const a=e[vo]();let n=e.w;const s=e.h;if(a.layout?.includes("row")){const t=a[oo],s=e.colSpan;let r;if(-1===s){r=Math.sumPrecise(t.columnWidths.slice(t.currentColumn));t.currentColumn=0}else{r=Math.sumPrecise(t.columnWidths.slice(t.currentColumn,t.currentColumn+s));t.currentColumn=(t.currentColumn+e.colSpan)%t.columnWidths.length}isNaN(r)||(n=e.w=r)}t.width=""!==n?measureToString(n):"auto";t.height=""!==s?measureToString(s):"auto"},position(e,t){const a=e[vo]();if(!a?.layout||"position"===a.layout){t.position="absolute";t.left=measureToString(e.x);t.top=measureToString(e.y)}},rotate(e,t){if(e.rotate){"transform"in t||(t.transform="");t.transform+=`rotate(-${e.rotate}deg)`;t.transformOrigin="top left"}},presence(e,t){switch(e.presence){case"invisible":t.visibility="hidden";break;case"hidden":case"inactive":t.display="none"}},hAlign(e,t){if("para"===e[Uo])switch(e.hAlign){case"justifyAll":t.textAlign="justify-all";break;case"radix":t.textAlign="left";break;default:t.textAlign=e.hAlign}else switch(e.hAlign){case"left":t.alignSelf="start";break;case"center":t.alignSelf="center";break;case"right":t.alignSelf="end"}},margin(e,t){e.margin&&(t.margin=e.margin[fl]().margin)}};function setMinMaxDimensions(e,t){if("position"===e[vo]().layout){e.minW>0&&(t.minWidth=measureToString(e.minW));e.maxW>0&&(t.maxWidth=measureToString(e.maxW));e.minH>0&&(t.minHeight=measureToString(e.minH));e.maxH>0&&(t.maxHeight=measureToString(e.maxH))}}function layoutText(e,t,a,n,s,r){const i=new TextMeasure(t,a,n,s);"string"==typeof e?i.addString(e):e[Vo](i);return i.compute(r)}function layoutNode(e,t){let a=null,n=null,s=!1;if((!e.w||!e.h)&&e.value){let r=0,i=0;if(e.margin){r=e.margin.leftInset+e.margin.rightInset;i=e.margin.topInset+e.margin.bottomInset}let o=null,l=null;if(e.para){l=Object.create(null);o=""===e.para.lineHeight?null:e.para.lineHeight;l.top=""===e.para.spaceAbove?0:e.para.spaceAbove;l.bottom=""===e.para.spaceBelow?0:e.para.spaceBelow;l.left=""===e.para.marginLeft?0:e.para.marginLeft;l.right=""===e.para.marginRight?0:e.para.marginRight}let f=e.font;if(!f){const t=e[Ao]();let a=e[So]();for(;a&&a!==t;){if(a.font){f=a.font;break}a=a[So]()}}const c=(e.w||t.width)-r,h=e[xo].fontFinder;if(e.value.exData&&e.value.exData[so]&&"text/html"===e.value.exData.contentType){const t=layoutText(e.value.exData[so],f,l,o,h,c);n=t.width;a=t.height;s=t.isBroken}else{const t=e.value[rl]();if(t){const e=layoutText(t,f,l,o,h,c);n=e.width;a=e.height;s=e.isBroken}}null===n||e.w||(n+=r);null===a||e.h||(a+=i)}return{w:n,h:a,isBroken:s}}function computeBbox(e,t,a){let n;if(""!==e.w&&""!==e.h)n=[e.x,e.y,e.w,e.h];else{if(!a)return null;let s=e.w;if(""===s){if(0===e.maxW){const t=e[vo]();s="position"===t.layout&&""!==t.w?0:e.minW}else s=Math.min(e.maxW,a.width);t.attributes.style.width=measureToString(s)}let r=e.h;if(""===r){if(0===e.maxH){const t=e[vo]();r="position"===t.layout&&""!==t.h?0:e.minH}else r=Math.min(e.maxH,a.height);t.attributes.style.height=measureToString(r)}n=[e.x,e.y,s,r]}return n}function fixDimensions(e){const t=e[vo]();if(t.layout?.includes("row")){const a=t[oo],n=e.colSpan;let s;s=-1===n?Math.sumPrecise(a.columnWidths.slice(a.currentColumn)):Math.sumPrecise(a.columnWidths.slice(a.currentColumn,a.currentColumn+n));isNaN(s)||(e.w=s)}t.layout&&"position"!==t.layout&&(e.x=e.y=0);"table"===e.layout&&""===e.w&&Array.isArray(e.columnWidths)&&(e.w=Math.sumPrecise(e.columnWidths))}function layoutClass(e){switch(e.layout){case"position":default:return"xfaPosition";case"lr-tb":return"xfaLrTb";case"rl-row":return"xfaRlRow";case"rl-tb":return"xfaRlTb";case"row":return"xfaRow";case"table":return"xfaTable";case"tb":return"xfaTb"}}function toStyle(e,...t){const a=Object.create(null);for(const n of t){const t=e[n];if(null!==t)if(Object.hasOwn(Wl,n))Wl[n](e,a);else if(t instanceof XFAObject){const e=t[fl]();e?Object.assign(a,e):warn(`(DEBUG) - XFA - style for ${n} not implemented yet`)}}return a}function createWrapper(e,t){const{attributes:a}=t,{style:n}=a,s={name:"div",attributes:{class:["xfaWrapper"],style:Object.create(null)},children:[]};a.class.push("xfaWrapped");if(e.border){const{widths:a,insets:r}=e.border[oo];let i,o,l=r[0],f=r[3];const c=r[0]+r[2],h=r[1]+r[3];switch(e.border.hand){case"even":l-=a[0]/2;f-=a[3]/2;i=`calc(100% + ${(a[1]+a[3])/2-h}px)`;o=`calc(100% + ${(a[0]+a[2])/2-c}px)`;break;case"left":l-=a[0];f-=a[3];i=`calc(100% + ${a[1]+a[3]-h}px)`;o=`calc(100% + ${a[0]+a[2]-c}px)`;break;case"right":i=h?`calc(100% - ${h}px)`:"100%";o=c?`calc(100% - ${c}px)`:"100%"}const u=["xfaBorder"];isPrintOnly(e.border)&&u.push("xfaPrintOnly");const m={name:"div",attributes:{class:u,style:{top:`${l}px`,left:`${f}px`,width:i,height:o}},children:[]};for(const e of["border","borderWidth","borderColor","borderRadius","borderStyle"])if(void 0!==n[e]){m.attributes.style[e]=n[e];delete n[e]}s.children.push(m,t)}else s.children.push(t);for(const e of["background","backgroundClip","top","left","width","height","minWidth","minHeight","maxWidth","maxHeight","transform","transformOrigin","visibility"])if(void 0!==n[e]){s.attributes.style[e]=n[e];delete n[e]}s.attributes.style.position="absolute"===n.position?"absolute":"relative";delete n.position;if(n.alignSelf){s.attributes.style.alignSelf=n.alignSelf;delete n.alignSelf}return s}function fixTextIndent(e){const t=getMeasurement(e.textIndent,"0px");if(t>=0)return;const a="padding"+("left"===("right"===e.textAlign?"right":"left")?"Left":"Right"),n=getMeasurement(e[a],"0px");e[a]=n-t+"px"}function setAccess(e,t){switch(e.access){case"nonInteractive":t.push("xfaNonInteractive");break;case"readOnly":t.push("xfaReadOnly");break;case"protected":t.push("xfaDisabled")}}function isPrintOnly(e){return e.relevant.length>0&&!e.relevant[0].excluded&&"print"===e.relevant[0].viewname}function getCurrentPara(e){const t=e[Ao]()[oo].paraStack;return t.length?t.at(-1):null}function setPara(e,t,a){if(a.attributes.class?.includes("xfaRich")){if(t){""===e.h&&(t.height="auto");""===e.w&&(t.width="auto")}const n=getCurrentPara(e);if(n){const e=a.attributes.style;e.display="flex";e.flexDirection="column";switch(n.vAlign){case"top":e.justifyContent="start";break;case"bottom":e.justifyContent="end";break;case"middle":e.justifyContent="center"}const t=n[fl]();for(const[a,n]of Object.entries(t))a in e||(e[a]=n)}}}function setFontFamily(e,t,a,n){if(!a){delete n.fontFamily;return}const s=stripQuotes(e.typeface);n.fontFamily=`"${s}"`;const r=a.find(s);if(r){const{fontFamily:a}=r.regular.cssFontInfo;a!==s&&(n.fontFamily=`"${a}"`);const i=getCurrentPara(t);if(i&&""!==i.lineHeight)return;if(n.lineHeight)return;const o=selectFont(e,r);o&&(n.lineHeight=Math.max(1.2,o.lineHeight))}}function fixURL(e){const t=createValidAbsoluteUrl(e,null,{addDefaultProtocol:!0,tryConvertEncoding:!0});return t?t.href:null}function createLine(e,t){return{name:"div",attributes:{class:["lr-tb"===e.layout?"xfaLr":"xfaRl"]},children:t}}function flushHTML(e){if(!e[oo])return null;const t={name:"div",attributes:e[oo].attributes,children:e[oo].children};if(e[oo].failingNode){const a=e[oo].failingNode[fo]();a&&(e.layout.endsWith("-tb")?t.children.push(createLine(e,[a])):t.children.push(a))}return 0===t.children.length?null:t}function addHTML(e,t,a){const n=e[oo],s=n.availableSpace,[r,i,o,l]=a;switch(e.layout){case"position":n.width=Math.max(n.width,r+o);n.height=Math.max(n.height,i+l);n.children.push(t);break;case"lr-tb":case"rl-tb":if(!n.line||1===n.attempt){n.line=createLine(e,[]);n.children.push(n.line);n.numberInLine=0}n.numberInLine+=1;n.line.children.push(t);if(0===n.attempt){n.currentWidth+=o;n.height=Math.max(n.height,n.prevHeight+l)}else{n.currentWidth=o;n.prevHeight=n.height;n.height+=l;n.attempt=0}n.width=Math.max(n.width,n.currentWidth);break;case"rl-row":case"row":{n.children.push(t);n.width+=o;n.height=Math.max(n.height,l);const e=measureToString(n.height);for(const t of n.children)t.attributes.style.height=e;break}case"table":case"tb":n.width=MathClamp(o,n.width,s.width);n.height+=l;n.children.push(t)}}function getAvailableSpace(e){const t=e[oo].availableSpace,a=e.margin?e.margin.topInset+e.margin.bottomInset:0,n=e.margin?e.margin.leftInset+e.margin.rightInset:0;switch(e.layout){case"lr-tb":case"rl-tb":return 0===e[oo].attempt?{width:t.width-n-e[oo].currentWidth,height:t.height-a-e[oo].prevHeight}:{width:t.width-n,height:t.height-a-e[oo].height};case"rl-row":case"row":return{width:Math.sumPrecise(e[oo].columnWidths.slice(e[oo].currentColumn)),height:t.height-n};case"table":case"tb":return{width:t.width-n,height:t.height-a-e[oo].height};default:return t}}function checkDimensions(e,t){if(null===e[Ao]()[oo].firstUnsplittable)return!0;if(0===e.w||0===e.h)return!0;const a=e[vo](),n=a[oo]?.attempt||0,[,s,r,i]=function getTransformedBBox(e){let t,a,n=""===e.w?NaN:e.w,s=""===e.h?NaN:e.h,[r,i]=[0,0];switch(e.anchorType||""){case"bottomCenter":[r,i]=[n/2,s];break;case"bottomLeft":[r,i]=[0,s];break;case"bottomRight":[r,i]=[n,s];break;case"middleCenter":[r,i]=[n/2,s/2];break;case"middleLeft":[r,i]=[0,s/2];break;case"middleRight":[r,i]=[n,s/2];break;case"topCenter":[r,i]=[n/2,0];break;case"topRight":[r,i]=[n,0]}switch(e.rotate||0){case 0:[t,a]=[-r,-i];break;case 90:[t,a]=[-i,r];[n,s]=[s,-n];break;case 180:[t,a]=[r,i];[n,s]=[-n,-s];break;case 270:[t,a]=[i,-r];[n,s]=[-s,n]}return[e.x+t+Math.min(0,n),e.y+a+Math.min(0,s),Math.abs(n),Math.abs(s)]}(e);switch(a.layout){case"lr-tb":case"rl-tb":return 0===n?e[Ao]()[oo].noLayoutFailure?""!==e.w?Math.round(r-t.width)<=2:t.width>2:!(""!==e.h&&Math.round(i-t.height)>2)&&(""!==e.w?Math.round(r-t.width)<=2||0===a[oo].numberInLine&&t.height>2:t.width>2):!!e[Ao]()[oo].noLayoutFailure||!(""!==e.h&&Math.round(i-t.height)>2)&&((""===e.w||Math.round(r-t.width)<=2||!a[Po]())&&t.height>2);case"table":case"tb":return!!e[Ao]()[oo].noLayoutFailure||(""===e.h||e[No]()?(""===e.w||Math.round(r-t.width)<=2||!a[Po]())&&t.height>2:Math.round(i-t.height)<=2);case"position":if(e[Ao]()[oo].noLayoutFailure)return!0;if(""===e.h||Math.round(i+s-t.height)<=2)return!0;return i+s>e[Ao]()[oo].currentContentArea.h;case"rl-row":case"row":return!!e[Ao]()[oo].noLayoutFailure||(""===e.h||Math.round(i-t.height)<=2);default:return!0}}const Kl=ul.template.id,Gl="http://www.w3.org/2000/svg",Vl=/^H(\d+)$/,$l=new Set(["image/gif","image/jpeg","image/jpg","image/pjpeg","image/png","image/apng","image/x-png","image/bmp","image/x-ms-bmp","image/tiff","image/tif","application/octet-stream"]),Yl=[[[66,77],"image/bmp"],[[255,216,255],"image/jpeg"],[[73,73,42,0],"image/tiff"],[[77,77,0,42],"image/tiff"],[[71,73,70,56,57,97],"image/gif"],[[137,80,78,71,13,10,26,10],"image/png"]];function getBorderDims(e){if(!e||!e.border)return{w:0,h:0};const t=e.border[wo]();return t?{w:t.widths[0]+t.widths[2]+t.insets[0]+t.insets[2],h:t.widths[1]+t.widths[3]+t.insets[1]+t.insets[3]}:{w:0,h:0}}function hasMargin(e){return e.margin&&(e.margin.topInset||e.margin.rightInset||e.margin.bottomInset||e.margin.leftInset)}function _setValue(e,t){if(!e.value){const t=new Value({});e[Ji](t);e.value=t}e.value[nl](t)}function*getContainedChildren(e){for(const t of e[ko]())t instanceof SubformSet?yield*t[yo]():yield t}function isRequired(e){return"error"===e.validate?.nullTest}function setTabIndex(e){for(;e;){if(!e.traversal){e[sl]=e[So]()[sl];return}if(e[sl])return;let t=null;for(const a of e.traversal[ko]())if("next"===a.operation){t=a;break}if(!t||!t.ref){e[sl]=e[So]()[sl];return}const a=e[Ao]();e[sl]=++a[sl];const n=a[el](t.ref,e);if(!n)return;e=n[0]}}function applyAssist(e,t){const a=e.assist;if(a){const e=a[ol]();e&&(t.title=e);const n=a.role.match(Vl);if(n){const e="heading",a=n[1];t.role=e;t["aria-level"]=a}}if("table"===e.layout)t.role="table";else if("row"===e.layout)t.role="row";else{const a=e[So]();"row"===a.layout&&(t.role="TH"===a.assist?.role?"columnheader":"cell")}}function ariaLabel(e){if(!e.assist)return null;const t=e.assist;return t.speak&&""!==t.speak[so]?t.speak[so]:t.toolTip?t.toolTip[so]:null}function valueToHtml(e){return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:Object.create(null)},children:[{name:"span",attributes:{style:Object.create(null)},value:e}]})}function setFirstUnsplittable(e){const t=e[Ao]();if(null===t[oo].firstUnsplittable){t[oo].firstUnsplittable=e;t[oo].noLayoutFailure=!0}}function unsetFirstUnsplittable(e){const t=e[Ao]();t[oo].firstUnsplittable===e&&(t[oo].noLayoutFailure=!1)}function handleBreak(e){if(e[oo])return!1;e[oo]=Object.create(null);if("auto"===e.targetType)return!1;const t=e[Ao]();let a=null;if(e.target){a=t[el](e.target,e[So]());if(!a)return!1;a=a[0]}const{currentPageArea:n,currentContentArea:s}=t[oo];if("pageArea"===e.targetType){a instanceof PageArea||(a=null);if(e.startNew){e[oo].target=a||n;return!0}if(a&&a!==n){e[oo].target=a;return!0}return!1}a instanceof ContentArea||(a=null);const r=a&&a[So]();let i,o=r;if(e.startNew)if(a){const e=r.contentArea.children,t=e.indexOf(s),n=e.indexOf(a);-1!==t&&te;n[oo].noLayoutFailure=!0;const i=t[ol](a);e[Yi](i.html,i.bbox);n[oo].noLayoutFailure=s;t[vo]=r}class AppearanceFilter extends StringObject{constructor(e){super(Kl,"appearanceFilter");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Arc extends XFAObject{constructor(e){super(Kl,"arc",!0);this.circular=getInteger({data:e.circular,defaultValue:0,validate:e=>1===e});this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.startAngle=getFloat({data:e.startAngle,defaultValue:0,validate:e=>!0});this.sweepAngle=getFloat({data:e.sweepAngle,defaultValue:360,validate:e=>!0});this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null;this.fill=null}[ol](){const e=this.edge||new Edge({}),t=e[fl](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[fl]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;let n;const s={xmlns:Gl,style:{width:"100%",height:"100%",overflow:"visible"}};if(360===this.sweepAngle)n={name:"ellipse",attributes:{xmlns:Gl,cx:"50%",cy:"50%",rx:"50%",ry:"50%",style:a}};else{const e=this.startAngle*Math.PI/180,t=this.sweepAngle*Math.PI/180,r=this.sweepAngle>180?1:0,[i,o,l,f]=[50*(1+Math.cos(e)),50*(1-Math.sin(e)),50*(1+Math.cos(e+t)),50*(1-Math.sin(e+t))];n={name:"path",attributes:{xmlns:Gl,d:`M ${i} ${o} A 50 50 0 ${r} 0 ${l} ${f}`,vectorEffect:"non-scaling-stroke",style:a}};Object.assign(s,{viewBox:"0 0 100 100",preserveAspectRatio:"none"})}const r={name:"svg",children:[n],attributes:s};if(hasMargin(this[So]()[So]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[r]});r.attributes.style.position="absolute";return HTMLResult.success(r)}}class Area extends XFAObject{constructor(e){super(Kl,"area",!0);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null;this.area=new XFAObjectArray;this.draw=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[yo](){yield*getContainedChildren(this)}[Eo](){return!0}[Ho](){return!0}[Yi](e,t){const[a,n,s,r]=t;this[oo].width=Math.max(this[oo].width,a+s);this[oo].height=Math.max(this[oo].height,n+r);this[oo].children.push(e)}[uo](){return this[oo].availableSpace}[ol](e){const t=toStyle(this,"position"),a={style:t,id:this[cl],class:["xfaArea"]};isPrintOnly(this)&&a.class.push("xfaPrintOnly");this.name&&(a.xfaName=this.name);const n=[];this[oo]={children:n,width:0,height:0,availableSpace:e};const s=this[Qi]({filter:new Set(["area","draw","field","exclGroup","subform","subformSet"]),include:!0});if(!s.success){if(s.isBreak())return s;delete this[oo];return HTMLResult.FAILURE}t.width=measureToString(this[oo].width);t.height=measureToString(this[oo].height);const r={name:"div",attributes:a,children:n},i=[this.x,this.y,this[oo].width,this[oo].height];delete this[oo];return HTMLResult.success(r,i)}}class Assist extends XFAObject{constructor(e){super(Kl,"assist",!0);this.id=e.id||"";this.role=e.role||"";this.use=e.use||"";this.usehref=e.usehref||"";this.speak=null;this.toolTip=null}[ol](){return this.toolTip?.[so]||null}}class Barcode extends XFAObject{constructor(e){super(Kl,"barcode",!0);this.charEncoding=getKeyword({data:e.charEncoding?e.charEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.checksum=getStringOption(e.checksum,["none","1mod10","1mod10_1mod11","2mod10","auto"]);this.dataColumnCount=getInteger({data:e.dataColumnCount,defaultValue:-1,validate:e=>e>=0});this.dataLength=getInteger({data:e.dataLength,defaultValue:-1,validate:e=>e>=0});this.dataPrep=getStringOption(e.dataPrep,["none","flateCompress"]);this.dataRowCount=getInteger({data:e.dataRowCount,defaultValue:-1,validate:e=>e>=0});this.endChar=e.endChar||"";this.errorCorrectionLevel=getInteger({data:e.errorCorrectionLevel,defaultValue:-1,validate:e=>e>=0&&e<=8});this.id=e.id||"";this.moduleHeight=getMeasurement(e.moduleHeight,"5mm");this.moduleWidth=getMeasurement(e.moduleWidth,"0.25mm");this.printCheckDigit=getInteger({data:e.printCheckDigit,defaultValue:0,validate:e=>1===e});this.rowColumnRatio=getRatio(e.rowColumnRatio);this.startChar=e.startChar||"";this.textLocation=getStringOption(e.textLocation,["below","above","aboveEmbedded","belowEmbedded","none"]);this.truncate=getInteger({data:e.truncate,defaultValue:0,validate:e=>1===e});this.type=getStringOption(e.type?e.type.toLowerCase():"",["aztec","codabar","code2of5industrial","code2of5interleaved","code2of5matrix","code2of5standard","code3of9","code3of9extended","code11","code49","code93","code128","code128a","code128b","code128c","code128sscc","datamatrix","ean8","ean8add2","ean8add5","ean13","ean13add2","ean13add5","ean13pwcd","fim","logmars","maxicode","msi","pdf417","pdf417macro","plessey","postauscust2","postauscust3","postausreplypaid","postausstandard","postukrm4scc","postusdpbc","postusimb","postusstandard","postus5zip","qrcode","rfid","rss14","rss14expanded","rss14limited","rss14stacked","rss14stackedomni","rss14truncated","telepen","ucc128","ucc128random","ucc128sscc","upca","upcaadd2","upcaadd5","upcapwcd","upce","upceadd2","upceadd5","upcean2","upcean5","upsmaxicode"]);this.upsMode=getStringOption(e.upsMode,["usCarrier","internationalCarrier","secureSymbol","standardSymbol"]);this.use=e.use||"";this.usehref=e.usehref||"";this.wideNarrowRatio=getRatio(e.wideNarrowRatio);this.encrypt=null;this.extras=null}}class Bind extends XFAObject{constructor(e){super(Kl,"bind",!0);this.match=getStringOption(e.match,["once","dataRef","global","none"]);this.ref=e.ref||"";this.picture=null}}class BindItems extends XFAObject{constructor(e){super(Kl,"bindItems");this.connection=e.connection||"";this.labelRef=e.labelRef||"";this.ref=e.ref||"";this.valueRef=e.valueRef||""}}class Bookend extends XFAObject{constructor(e){super(Kl,"bookend");this.id=e.id||"";this.leader=e.leader||"";this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||""}}class BooleanElement extends Option01{constructor(e){super(Kl,"boolean");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[ol](e){return valueToHtml(1===this[so]?"1":"0")}}class Border extends XFAObject{constructor(e){super(Kl,"border",!0);this.break=getStringOption(e.break,["close","open"]);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.extras=null;this.fill=null;this.margin=null}[wo](){if(!this[oo]){const e=this.edge.children.slice();if(e.length<4){const t=e.at(-1)||new Edge({});for(let a=e.length;a<4;a++)e.push(t)}const t=e.map(e=>e.thickness),a=[0,0,0,0];if(this.margin){a[0]=this.margin.topInset;a[1]=this.margin.rightInset;a[2]=this.margin.bottomInset;a[3]=this.margin.leftInset}this[oo]={widths:t,insets:a,edges:e}}return this[oo]}[fl](){const{edges:e}=this[wo](),t=e.map(e=>{const t=e[fl]();t.color||="#000000";return t}),a=Object.create(null);this.margin&&Object.assign(a,this.margin[fl]());"visible"===this.fill?.presence&&Object.assign(a,this.fill[fl]());if(this.corner.children.some(e=>0!==e.radius)){const e=this.corner.children.map(e=>e[fl]());if(2===e.length||3===e.length){const t=e.at(-1);for(let a=e.length;a<4;a++)e.push(t)}a.borderRadius=e.map(e=>e.radius).join(" ")}switch(this.presence){case"invisible":case"hidden":a.borderStyle="";break;case"inactive":a.borderStyle="none";break;default:a.borderStyle=t.map(e=>e.style).join(" ")}a.borderWidth=t.map(e=>e.width).join(" ");a.borderColor=t.map(e=>e.color).join(" ");return a}}class Break extends XFAObject{constructor(e){super(Kl,"break",!0);this.after=getStringOption(e.after,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.afterTarget=e.afterTarget||"";this.before=getStringOption(e.before,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.beforeTarget=e.beforeTarget||"";this.bookendLeader=e.bookendLeader||"";this.bookendTrailer=e.bookendTrailer||"";this.id=e.id||"";this.overflowLeader=e.overflowLeader||"";this.overflowTarget=e.overflowTarget||"";this.overflowTrailer=e.overflowTrailer||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class BreakAfter extends XFAObject{constructor(e){super(Kl,"breakAfter",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}}class BreakBefore extends XFAObject{constructor(e){super(Kl,"breakBefore",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}[ol](e){this[oo]={};return HTMLResult.FAILURE}}class Button extends XFAObject{constructor(e){super(Kl,"button",!0);this.highlight=getStringOption(e.highlight,["inverted","none","outline","push"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[ol](e){const t=this[So]()[So](),a={name:"button",attributes:{id:this[cl],class:["xfaButton"],style:{}},children:[]};for(const e of t.event.children){if("click"!==e.activity||!e.script)continue;const t=recoverJsURL(e.script[so]);if(!t)continue;const n=fixURL(t.url);n&&a.children.push({name:"a",attributes:{id:"link"+this[cl],href:n,newWindow:t.newWindow,class:["xfaLink"],style:{}},children:[]})}return HTMLResult.success(a)}}class Calculate extends XFAObject{constructor(e){super(Kl,"calculate",!0);this.id=e.id||"";this.override=getStringOption(e.override,["disabled","error","ignore","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.script=null}}class Caption extends XFAObject{constructor(e){super(Kl,"caption",!0);this.id=e.id||"";this.placement=getStringOption(e.placement,["left","bottom","inline","right","top"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.reserve=Math.ceil(getMeasurement(e.reserve));this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.font=null;this.margin=null;this.para=null;this.value=null}[nl](e){_setValue(this,e)}[wo](e){if(!this[oo]){let{width:t,height:a}=e;switch(this.placement){case"left":case"right":case"inline":t=this.reserve<=0?t:this.reserve;break;case"top":case"bottom":a=this.reserve<=0?a:this.reserve}this[oo]=layoutNode(this,{width:t,height:a})}return this[oo]}[ol](e){if(!this.value)return HTMLResult.EMPTY;this[Yo]();const t=this.value[ol](e).html;if(!t){this[$o]();return HTMLResult.EMPTY}const a=this.reserve;if(this.reserve<=0){const{w:t,h:a}=this[wo](e);switch(this.placement){case"left":case"right":case"inline":this.reserve=t;break;case"top":case"bottom":this.reserve=a}}const n=[];"string"==typeof t?n.push({name:"#text",value:t}):n.push(t);const s=toStyle(this,"font","margin","visibility");switch(this.placement){case"left":case"right":this.reserve>0&&(s.width=measureToString(this.reserve));break;case"top":case"bottom":this.reserve>0&&(s.height=measureToString(this.reserve))}setPara(this,null,t);this[$o]();this.reserve=a;return HTMLResult.success({name:"div",attributes:{style:s,class:["xfaCaption"]},children:n})}}class Certificate extends StringObject{constructor(e){super(Kl,"certificate");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Certificates extends XFAObject{constructor(e){super(Kl,"certificates",!0);this.credentialServerPolicy=getStringOption(e.credentialServerPolicy,["optional","required"]);this.id=e.id||"";this.url=e.url||"";this.urlPolicy=e.urlPolicy||"";this.use=e.use||"";this.usehref=e.usehref||"";this.encryption=null;this.issuers=null;this.keyUsage=null;this.oids=null;this.signing=null;this.subjectDNs=null}}class CheckButton extends XFAObject{constructor(e){super(Kl,"checkButton",!0);this.id=e.id||"";this.mark=getStringOption(e.mark,["default","check","circle","cross","diamond","square","star"]);this.shape=getStringOption(e.shape,["square","round"]);this.size=getMeasurement(e.size,"10pt");this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[ol](e){const t=toStyle(this,"margin"),a=measureToString(this.size);t.width=t.height=a;let n,s,r;const i=this[So]()[So](),o=i.items.children.length&&i.items.children[0][ol]().html||[],l={on:(void 0!==o[0]?o[0]:"on").toString(),off:(void 0!==o[1]?o[1]:"off").toString()},f=(i.value?.[rl]()||"off")===l.on||void 0,c=i[vo](),h=i[cl];let u;if(c instanceof ExclGroup){r=c[cl];n="radio";s="xfaRadio";u=c[ro]?.[cl]||c[cl]}else{n="checkbox";s="xfaCheckbox";u=i[ro]?.[cl]||i[cl]}const m={name:"input",attributes:{class:[s],style:t,fieldId:h,dataId:u,type:n,checked:f,xfaOn:l.on,xfaOff:l.off,"aria-label":ariaLabel(i),"aria-required":!1}};r&&(m.attributes.name=r);if(isRequired(i)){m.attributes["aria-required"]=!0;m.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[m]})}}class ChoiceList extends XFAObject{constructor(e){super(Kl,"choiceList",!0);this.commitOn=getStringOption(e.commitOn,["select","exit"]);this.id=e.id||"";this.open=getStringOption(e.open,["userControl","always","multiSelect","onEntry"]);this.textEntry=getInteger({data:e.textEntry,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[ol](e){const t=toStyle(this,"border","margin"),a=this[So]()[So](),n={fontSize:`calc(${a.font?.size||10}px * var(--total-scale-factor))`},s=[];if(a.items.children.length>0){const e=a.items;let t=0,r=0;if(2===e.children.length){t=e.children[0].save;r=1-t}const i=e.children[t][ol]().html,o=e.children[r][ol]().html;let l=!1;const f=a.value?.[rl]()||"";for(let e=0,t=i.length;eMathClamp(parseInt(e.trim(),10),0,255)).map(e=>isNaN(e)?0:e);if(r.length<3)return{r:a,g:n,b:s};[a,n,s]=r;return{r:a,g:n,b:s}}(e.value):"";this.extras=null}[Co](){return!1}[fl](){return this.value?Util.makeHexColor(this.value.r,this.value.g,this.value.b):null}}class Comb extends XFAObject{constructor(e){super(Kl,"comb");this.id=e.id||"";this.numberOfCells=getInteger({data:e.numberOfCells,defaultValue:0,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||""}}class Connect extends XFAObject{constructor(e){super(Kl,"connect",!0);this.connection=e.connection||"";this.id=e.id||"";this.ref=e.ref||"";this.usage=getStringOption(e.usage,["exportAndImport","exportOnly","importOnly"]);this.use=e.use||"";this.usehref=e.usehref||"";this.picture=null}}class ContentArea extends XFAObject{constructor(e){super(Kl,"contentArea",!0);this.h=getMeasurement(e.h);this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=getMeasurement(e.w);this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null}[ol](e){const t={left:measureToString(this.x),top:measureToString(this.y),width:measureToString(this.w),height:measureToString(this.h)},a=["xfaContentarea"];isPrintOnly(this)&&a.push("xfaPrintOnly");return HTMLResult.success({name:"div",children:[],attributes:{style:t,class:a,id:this[cl]}})}}class Corner extends XFAObject{constructor(e){super(Kl,"corner",!0);this.id=e.id||"";this.inverted=getInteger({data:e.inverted,defaultValue:0,validate:e=>1===e});this.join=getStringOption(e.join,["square","round"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.radius=getMeasurement(e.radius);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[fl](){const e=toStyle(this,"visibility");e.radius=measureToString("square"===this.join?0:this.radius);return e}}class DateElement extends ContentObject{constructor(e){super(Kl,"date");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){const e=this[so].trim();this[so]=e?new Date(e):null}[ol](e){return valueToHtml(this[so]?this[so].toString():"")}}class DateTime extends ContentObject{constructor(e){super(Kl,"dateTime");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){const e=this[so].trim();this[so]=e?new Date(e):null}[ol](e){return valueToHtml(this[so]?this[so].toString():"")}}class DateTimeEdit extends XFAObject{constructor(e){super(Kl,"dateTimeEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.picker=getStringOption(e.picker,["host","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[ol](e){const t=toStyle(this,"border","font","margin"),a=this[So]()[So](),n={name:"input",attributes:{type:"text",fieldId:a[cl],dataId:a[ro]?.[cl]||a[cl],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){n.attributes["aria-required"]=!0;n.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[n]})}}class Decimal extends ContentObject{constructor(e){super(Kl,"decimal");this.fracDigits=getInteger({data:e.fracDigits,defaultValue:2,validate:e=>!0});this.id=e.id||"";this.leadDigits=getInteger({data:e.leadDigits,defaultValue:-1,validate:e=>!0});this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){const e=parseFloat(this[so].trim());this[so]=isNaN(e)?null:e}[ol](e){return valueToHtml(null!==this[so]?this[so].toString():"")}}class DefaultUi extends XFAObject{constructor(e){super(Kl,"defaultUi",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class Desc extends XFAObject{constructor(e){super(Kl,"desc",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class DigestMethod extends OptionObject{constructor(e){super(Kl,"digestMethod",["","SHA1","SHA256","SHA512","RIPEMD160"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class DigestMethods extends XFAObject{constructor(e){super(Kl,"digestMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.digestMethod=new XFAObjectArray}}class Draw extends XFAObject{constructor(e){super(Kl,"draw",!0);this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.border=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.value=null;this.setProperty=new XFAObjectArray}[nl](e){_setValue(this,e)}[ol](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;fixDimensions(this);this[Yo]();const t=this.w,a=this.h,{w:n,h:s,isBroken:r}=layoutNode(this,e);if(n&&""===this.w){if(r&&this[vo]()[Po]()){this[$o]();return HTMLResult.FAILURE}this.w=n}s&&""===this.h&&(this.h=s);setFirstUnsplittable(this);if(!checkDimensions(this,e)){this.w=t;this.h=a;this[$o]();return HTMLResult.FAILURE}unsetFirstUnsplittable(this);const i=toStyle(this,"font","hAlign","dimensions","position","presence","rotate","anchorType","border","margin");setMinMaxDimensions(this,i);if(i.margin){i.padding=i.margin;delete i.margin}const o=["xfaDraw"];this.font&&o.push("xfaFont");isPrintOnly(this)&&o.push("xfaPrintOnly");const l={style:i,id:this[cl],class:o};this.name&&(l.xfaName=this.name);const f={name:"div",attributes:l,children:[]};applyAssist(this,l);const c=computeBbox(this,f,e),h=this.value?this.value[ol](e).html:null;if(null===h){this.w=t;this.h=a;this[$o]();return HTMLResult.success(createWrapper(this,f),c)}f.children.push(h);setPara(this,i,h);this.w=t;this.h=a;this[$o]();return HTMLResult.success(createWrapper(this,f),c)}}class Edge extends XFAObject{constructor(e){super(Kl,"edge",!0);this.cap=getStringOption(e.cap,["square","butt","round"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[fl](){const e=toStyle(this,"visibility");Object.assign(e,{linecap:this.cap,width:measureToString(this.thickness),color:this.color?this.color[fl]():"#000000",style:""});if("visible"!==this.presence)e.style="none";else switch(this.stroke){case"solid":e.style="solid";break;case"dashDot":case"dashDotDot":case"dashed":e.style="dashed";break;case"dotted":e.style="dotted";break;case"embossed":e.style="ridge";break;case"etched":e.style="groove";break;case"lowered":e.style="inset";break;case"raised":e.style="outset"}return e}}class Encoding extends OptionObject{constructor(e){super(Kl,"encoding",["adbe.x509.rsa_sha1","adbe.pkcs7.detached","adbe.pkcs7.sha1"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Encodings extends XFAObject{constructor(e){super(Kl,"encodings",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encoding=new XFAObjectArray}}class Encrypt extends XFAObject{constructor(e){super(Kl,"encrypt",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=null}}class EncryptData extends XFAObject{constructor(e){super(Kl,"encryptData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["encrypt","decrypt"]);this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Encryption extends XFAObject{constructor(e){super(Kl,"encryption",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class EncryptionMethod extends OptionObject{constructor(e){super(Kl,"encryptionMethod",["","AES256-CBC","TRIPLEDES-CBC","AES128-CBC","AES192-CBC"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EncryptionMethods extends XFAObject{constructor(e){super(Kl,"encryptionMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encryptionMethod=new XFAObjectArray}}class Event extends XFAObject{constructor(e){super(Kl,"event",!0);this.activity=getStringOption(e.activity,["click","change","docClose","docReady","enter","exit","full","indexChange","initialize","mouseDown","mouseEnter","mouseExit","mouseUp","postExecute","postOpen","postPrint","postSave","postSign","postSubmit","preExecute","preOpen","prePrint","preSave","preSign","preSubmit","ready","validationState"]);this.id=e.id||"";this.listen=getStringOption(e.listen,["refOnly","refAndDescendents"]);this.name=e.name||"";this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.encryptData=null;this.execute=null;this.script=null;this.signData=null;this.submit=null}}class ExData extends ContentObject{constructor(e){super(Kl,"exData");this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.maxLength=getInteger({data:e.maxLength,defaultValue:-1,validate:e=>e>=-1});this.name=e.name||"";this.rid=e.rid||"";this.transferEncoding=getStringOption(e.transferEncoding,["none","base64","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[Ro](){return"text/html"===this.contentType}[Wo](e){if("text/html"===this.contentType&&e[Lo]===ul.xhtml.id){this[so]=e;return!0}if("text/xml"===this.contentType){this[so]=e;return!0}return!1}[ol](e){return"text/html"===this.contentType&&this[so]?this[so][ol](e):HTMLResult.EMPTY}}class ExObject extends XFAObject{constructor(e){super(Kl,"exObject",!0);this.archive=e.archive||"";this.classId=e.classId||"";this.codeBase=e.codeBase||"";this.codeType=e.codeType||"";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class ExclGroup extends XFAObject{constructor(e){super(Kl,"exclGroup",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.accessKey=e.accessKey||"";this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.margin=null;this.para=null;this.traversal=null;this.validate=null;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.field=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Ho](){return!0}[Co](){return!0}[nl](e){for(const t of this.field.children){if(!t.value){const e=new Value({});t[Ji](e);t.value=e}t.value[nl](e)}}[Po](){return this.layout.endsWith("-tb")&&0===this[oo].attempt&&this[oo].numberInLine>0||this[So]()[Po]()}[No](){const e=this[vo]();if(!e[No]())return!1;if(void 0!==this[oo]._isSplittable)return this[oo]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[oo]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[oo].numberInLine)return!1;this[oo]._isSplittable=!0;return!0}[fo](){return flushHTML(this)}[Yi](e,t){addHTML(this,e,t)}[uo](){return getAvailableSpace(this)}[ol](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[cl],class:[]};setAccess(this,a.class);this[oo]||=Object.create(null);Object.assign(this[oo],{children:t,attributes:a,attempt:0,line:null,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const n=this[No]();n||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const s=new Set(["field"]);if(this.layout.includes("row")){const e=this[vo]().columnWidths;if(Array.isArray(e)&&e.length>0){this[oo].columnWidths=e;this[oo].currentColumn=0}}const r=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),i=["xfaExclgroup"],o=layoutClass(this);o&&i.push(o);isPrintOnly(this)&&i.push("xfaPrintOnly");a.style=r;a.class=i;this.name&&(a.xfaName=this.name);this[Yo]();const l="lr-tb"===this.layout||"rl-tb"===this.layout,f=l?2:1;for(;this[oo].attempte>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.format=null;this.items=new XFAObjectArray(2);this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.validate=null;this.value=null;this.bindItems=new XFAObjectArray;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Ho](){return!0}[nl](e){_setValue(this,e)}[ol](e){setTabIndex(this);if(!this.ui){this.ui=new Ui({});this.ui[xo]=this[xo];this[Ji](this.ui);let e;switch(this.items.children.length){case 0:e=new TextEdit({});this.ui.textEdit=e;break;case 1:e=new CheckButton({});this.ui.checkButton=e;break;case 2:e=new ChoiceList({});this.ui.choiceList=e}this.ui[Ji](e)}if(!this.ui||"hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;this.caption&&delete this.caption[oo];this[Yo]();const t=this.caption?this.caption[ol](e).html:null,a=this.w,n=this.h;let s=0,r=0;if(this.margin){s=this.margin.leftInset+this.margin.rightInset;r=this.margin.topInset+this.margin.bottomInset}let i=null;if(""===this.w||""===this.h){let t=null,a=null,n=0,o=0;if(this.ui.checkButton)n=o=this.ui.checkButton.size;else{const{w:t,h:a}=layoutNode(this,e);if(null!==t){n=t;o=a}else o=function fonts_getMetrics(e,t=!1){let a=null;if(e){const t=stripQuotes(e.typeface),n=e[xo].fontFinder.find(t);a=selectFont(e,n)}if(!a)return{lineHeight:12,lineGap:2,lineNoGap:10};const n=e.size||10,s=a.lineHeight?Math.max(t?0:1.2,a.lineHeight):1.2,r=void 0===a.lineGap?.2:a.lineGap;return{lineHeight:s*n,lineGap:r*n,lineNoGap:Math.max(1,s-r)*n}}(this.font,!0).lineNoGap}i=getBorderDims(this.ui[wo]());n+=i.w;o+=i.h;if(this.caption){const{w:s,h:r,isBroken:i}=this.caption[wo](e);if(i&&this[vo]()[Po]()){this[$o]();return HTMLResult.FAILURE}t=s;a=r;switch(this.caption.placement){case"left":case"right":case"inline":t+=n;break;case"top":case"bottom":a+=o}}else{t=n;a=o}if(t&&""===this.w){t+=s;this.w=Math.min(this.maxW<=0?1/0:this.maxW,this.minW+1e>=1&&e<=5});this.appearanceFilter=null;this.certificates=null;this.digestMethods=null;this.encodings=null;this.encryptionMethods=null;this.handler=null;this.lockDocument=null;this.mdp=null;this.reasons=null;this.timeStamp=null}}class Float extends ContentObject{constructor(e){super(Kl,"float");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){const e=parseFloat(this[so].trim());this[so]=isNaN(e)?null:e}[ol](e){return valueToHtml(null!==this[so]?this[so].toString():"")}}class template_Font extends XFAObject{constructor(e){super(Kl,"font",!0);this.baselineShift=getMeasurement(e.baselineShift);this.fontHorizontalScale=getFloat({data:e.fontHorizontalScale,defaultValue:100,validate:e=>e>=0});this.fontVerticalScale=getFloat({data:e.fontVerticalScale,defaultValue:100,validate:e=>e>=0});this.id=e.id||"";this.kerningMode=getStringOption(e.kerningMode,["none","pair"]);this.letterSpacing=getMeasurement(e.letterSpacing,"0");this.lineThrough=getInteger({data:e.lineThrough,defaultValue:0,validate:e=>1===e||2===e});this.lineThroughPeriod=getStringOption(e.lineThroughPeriod,["all","word"]);this.overline=getInteger({data:e.overline,defaultValue:0,validate:e=>1===e||2===e});this.overlinePeriod=getStringOption(e.overlinePeriod,["all","word"]);this.posture=getStringOption(e.posture,["normal","italic"]);this.size=getMeasurement(e.size,"10pt");this.typeface=e.typeface||"Courier";this.underline=getInteger({data:e.underline,defaultValue:0,validate:e=>1===e||2===e});this.underlinePeriod=getStringOption(e.underlinePeriod,["all","word"]);this.use=e.use||"";this.usehref=e.usehref||"";this.weight=getStringOption(e.weight,["normal","bold"]);this.extras=null;this.fill=null}[Zi](e){super[Zi](e);this[xo].usedTypefaces.add(this.typeface)}[fl](){const e=toStyle(this,"fill"),t=e.color;if(t)if("#000000"===t)delete e.color;else if(!t.startsWith("#")){e.background=t;e.backgroundClip="text";e.color="transparent"}this.baselineShift&&(e.verticalAlign=measureToString(this.baselineShift));e.fontKerning="none"===this.kerningMode?"none":"normal";e.letterSpacing=measureToString(this.letterSpacing);if(0!==this.lineThrough){e.textDecoration="line-through";2===this.lineThrough&&(e.textDecorationStyle="double")}if(0!==this.overline){e.textDecoration="overline";2===this.overline&&(e.textDecorationStyle="double")}e.fontStyle=this.posture;e.fontSize=measureToString(.99*this.size);setFontFamily(this,this,this[xo].fontFinder,e);if(0!==this.underline){e.textDecoration="underline";2===this.underline&&(e.textDecorationStyle="double")}e.fontWeight=this.weight;return e}}class Format extends XFAObject{constructor(e){super(Kl,"format",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null}}class Handler extends StringObject{constructor(e){super(Kl,"handler");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Hyphenation extends XFAObject{constructor(e){super(Kl,"hyphenation");this.excludeAllCaps=getInteger({data:e.excludeAllCaps,defaultValue:0,validate:e=>1===e});this.excludeInitialCap=getInteger({data:e.excludeInitialCap,defaultValue:0,validate:e=>1===e});this.hyphenate=getInteger({data:e.hyphenate,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.pushCharacterCount=getInteger({data:e.pushCharacterCount,defaultValue:3,validate:e=>e>=0});this.remainCharacterCount=getInteger({data:e.remainCharacterCount,defaultValue:3,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||"";this.wordCharacterCount=getInteger({data:e.wordCharacterCount,defaultValue:7,validate:e=>e>=0})}}class Image extends StringObject{constructor(e){super(Kl,"image");this.aspect=getStringOption(e.aspect,["fit","actual","height","none","width"]);this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.name=e.name||"";this.transferEncoding=getStringOption(e.transferEncoding,["base64","none","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[ol](){if(this.contentType&&!$l.has(this.contentType.toLowerCase()))return HTMLResult.EMPTY;let e=this[xo].images?.get(this.href);if(!e&&(this.href||!this[so]))return HTMLResult.EMPTY;e||"base64"!==this.transferEncoding||(e=Uint8Array.fromBase64(this[so]));if(!e)return HTMLResult.EMPTY;if(!this.contentType){for(const[t,a]of Yl)if(e.length>t.length&&t.every((t,a)=>t===e[a])){this.contentType=a;break}if(!this.contentType)return HTMLResult.EMPTY}const t=new Blob([e],{type:this.contentType});let a;switch(this.aspect){case"fit":case"actual":break;case"height":a={height:"100%",objectFit:"fill"};break;case"none":a={width:"100%",height:"100%",objectFit:"fill"};break;case"width":a={width:"100%",objectFit:"fill"}}const n=this[So]();return HTMLResult.success({name:"img",attributes:{class:["xfaImage"],style:a,src:URL.createObjectURL(t),alt:n?ariaLabel(n[So]()):null}})}}class ImageEdit extends XFAObject{constructor(e){super(Kl,"imageEdit",!0);this.data=getStringOption(e.data,["link","embed"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[ol](e){return"embed"===this.data?HTMLResult.success({name:"div",children:[],attributes:{}}):HTMLResult.EMPTY}}class Integer extends ContentObject{constructor(e){super(Kl,"integer");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){const e=parseInt(this[so].trim(),10);this[so]=isNaN(e)?null:e}[ol](e){return valueToHtml(null!==this[so]?this[so].toString():"")}}class Issuers extends XFAObject{constructor(e){super(Kl,"issuers",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Items extends XFAObject{constructor(e){super(Kl,"items",!0);this.id=e.id||"";this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.ref=e.ref||"";this.save=getInteger({data:e.save,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[ol](){const e=[];for(const t of this[ko]())e.push(t[rl]());return HTMLResult.success(e)}}class Keep extends XFAObject{constructor(e){super(Kl,"keep",!0);this.id=e.id||"";const t=["none","contentArea","pageArea"];this.intact=getStringOption(e.intact,t);this.next=getStringOption(e.next,t);this.previous=getStringOption(e.previous,t);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class KeyUsage extends XFAObject{constructor(e){super(Kl,"keyUsage");const t=["","yes","no"];this.crlSign=getStringOption(e.crlSign,t);this.dataEncipherment=getStringOption(e.dataEncipherment,t);this.decipherOnly=getStringOption(e.decipherOnly,t);this.digitalSignature=getStringOption(e.digitalSignature,t);this.encipherOnly=getStringOption(e.encipherOnly,t);this.id=e.id||"";this.keyAgreement=getStringOption(e.keyAgreement,t);this.keyCertSign=getStringOption(e.keyCertSign,t);this.keyEncipherment=getStringOption(e.keyEncipherment,t);this.nonRepudiation=getStringOption(e.nonRepudiation,t);this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Line extends XFAObject{constructor(e){super(Kl,"line",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.slope=getStringOption(e.slope,["\\","/"]);this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null}[ol](){const e=this[So]()[So](),t=this.edge||new Edge({}),a=t[fl](),n=Object.create(null),s="visible"===t.presence?t.thickness:0;n.strokeWidth=measureToString(s);n.stroke=a.color;let r,i,o,l,f="100%",c="100%";if(e.w<=s){[r,i,o,l]=["50%",0,"50%","100%"];f=n.strokeWidth}else if(e.h<=s){[r,i,o,l]=[0,"50%","100%","50%"];c=n.strokeWidth}else"\\"===this.slope?[r,i,o,l]=[0,0,"100%","100%"]:[r,i,o,l]=[0,"100%","100%",0];const h={name:"svg",children:[{name:"line",attributes:{xmlns:Gl,x1:r,y1:i,x2:o,y2:l,style:n}}],attributes:{xmlns:Gl,width:f,height:c,style:{overflow:"visible"}}};if(hasMargin(e))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[h]});h.attributes.style.position="absolute";return HTMLResult.success(h)}}class Linear extends XFAObject{constructor(e){super(Kl,"linear",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toRight","toBottom","toLeft","toTop"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[fl](e){e=e?e[fl]():"#FFFFFF";return`linear-gradient(${this.type.replace(/([RBLT])/," $1").toLowerCase()}, ${e}, ${this.color?this.color[fl]():"#000000"})`}}class LockDocument extends ContentObject{constructor(e){super(Kl,"lockDocument");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}[lo](){this[so]=getStringOption(this[so],["auto","0","1"])}}class Manifest extends XFAObject{constructor(e){super(Kl,"manifest",!0);this.action=getStringOption(e.action,["include","all","exclude"]);this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.ref=new XFAObjectArray}}class Margin extends XFAObject{constructor(e){super(Kl,"margin",!0);this.bottomInset=getMeasurement(e.bottomInset,"0");this.id=e.id||"";this.leftInset=getMeasurement(e.leftInset,"0");this.rightInset=getMeasurement(e.rightInset,"0");this.topInset=getMeasurement(e.topInset,"0");this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[fl](){return{margin:measureToString(this.topInset)+" "+measureToString(this.rightInset)+" "+measureToString(this.bottomInset)+" "+measureToString(this.leftInset)}}}class Mdp extends XFAObject{constructor(e){super(Kl,"mdp");this.id=e.id||"";this.permissions=getInteger({data:e.permissions,defaultValue:2,validate:e=>1===e||3===e});this.signatureType=getStringOption(e.signatureType,["filler","author"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Medium extends XFAObject{constructor(e){super(Kl,"medium");this.id=e.id||"";this.imagingBBox=function getBBox(e){const t=-1;if(!e)return{x:t,y:t,width:t,height:t};const a=e.split(",",4).map(e=>getMeasurement(e.trim(),"-1"));if(a.length<4||a[2]<0||a[3]<0)return{x:t,y:t,width:t,height:t};const[n,s,r,i]=a;return{x:n,y:s,width:r,height:i}}(e.imagingBBox);this.long=getMeasurement(e.long);this.orientation=getStringOption(e.orientation,["portrait","landscape"]);this.short=getMeasurement(e.short);this.stock=e.stock||"";this.trayIn=getStringOption(e.trayIn,["auto","delegate","pageFront"]);this.trayOut=getStringOption(e.trayOut,["auto","delegate"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Message extends XFAObject{constructor(e){super(Kl,"message",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.text=new XFAObjectArray}}class NumericEdit extends XFAObject{constructor(e){super(Kl,"numericEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[ol](e){const t=toStyle(this,"border","font","margin"),a=this[So]()[So](),n={name:"input",attributes:{type:"text",fieldId:a[cl],dataId:a[ro]?.[cl]||a[cl],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){n.attributes["aria-required"]=!0;n.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[n]})}}class Occur extends XFAObject{constructor(e){super(Kl,"occur",!0);this.id=e.id||"";this.initial=""!==e.initial?getInteger({data:e.initial,defaultValue:"",validate:e=>!0}):"";this.max=""!==e.max?getInteger({data:e.max,defaultValue:-1,validate:e=>!0}):"";this.min=""!==e.min?getInteger({data:e.min,defaultValue:1,validate:e=>!0}):"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Zi](){const e=this[So](),t=this.min;""===this.min&&(this.min=e instanceof PageArea||e instanceof PageSet?0:1);""===this.max&&(this.max=""===t?e instanceof PageArea||e instanceof PageSet?-1:1:this.min);-1!==this.max&&this.max!0});this.name=e.name||"";this.numbered=getInteger({data:e.numbered,defaultValue:1,validate:e=>!0});this.oddOrEven=getStringOption(e.oddOrEven,["any","even","odd"]);this.pagePosition=getStringOption(e.pagePosition,["any","first","last","only","rest"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.desc=null;this.extras=null;this.medium=null;this.occur=null;this.area=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.draw=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray}[_o](){if(!this[oo]){this[oo]={numberOfUse:0};return!0}return!this.occur||-1===this.occur.max||this[oo].numberOfUsee.oddOrEven===t&&e.pagePosition===a);if(n)return n;n=this.pageArea.children.find(e=>"any"===e.oddOrEven&&e.pagePosition===a);if(n)return n;n=this.pageArea.children.find(e=>"any"===e.oddOrEven&&"any"===e.pagePosition);return n||this.pageArea.children[0]}}class Para extends XFAObject{constructor(e){super(Kl,"para",!0);this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.lineHeight=e.lineHeight?getMeasurement(e.lineHeight,"0pt"):"";this.marginLeft=e.marginLeft?getMeasurement(e.marginLeft,"0pt"):"";this.marginRight=e.marginRight?getMeasurement(e.marginRight,"0pt"):"";this.orphans=getInteger({data:e.orphans,defaultValue:0,validate:e=>e>=0});this.preserve=e.preserve||"";this.radixOffset=e.radixOffset?getMeasurement(e.radixOffset,"0pt"):"";this.spaceAbove=e.spaceAbove?getMeasurement(e.spaceAbove,"0pt"):"";this.spaceBelow=e.spaceBelow?getMeasurement(e.spaceBelow,"0pt"):"";this.tabDefault=e.tabDefault?getMeasurement(this.tabDefault):"";this.tabStops=(e.tabStops||"").trim().split(/\s+/).map((e,t)=>t%2==1?getMeasurement(e):e);this.textIndent=e.textIndent?getMeasurement(e.textIndent,"0pt"):"";this.use=e.use||"";this.usehref=e.usehref||"";this.vAlign=getStringOption(e.vAlign,["top","bottom","middle"]);this.widows=getInteger({data:e.widows,defaultValue:0,validate:e=>e>=0});this.hyphenation=null}[fl](){const e=toStyle(this,"hAlign");""!==this.marginLeft&&(e.paddingLeft=measureToString(this.marginLeft));""!==this.marginRight&&(e.paddingRight=measureToString(this.marginRight));""!==this.spaceAbove&&(e.paddingTop=measureToString(this.spaceAbove));""!==this.spaceBelow&&(e.paddingBottom=measureToString(this.spaceBelow));if(""!==this.textIndent){e.textIndent=measureToString(this.textIndent);fixTextIndent(e)}this.lineHeight>0&&(e.lineHeight=measureToString(this.lineHeight));""!==this.tabDefault&&(e.tabSize=measureToString(this.tabDefault));this.tabStops.length;this.hyphenatation&&Object.assign(e,this.hyphenatation[fl]());return e}}class PasswordEdit extends XFAObject{constructor(e){super(Kl,"passwordEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.passwordChar=e.passwordChar||"*";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}}class template_Pattern extends XFAObject{constructor(e){super(Kl,"pattern",!0);this.id=e.id||"";this.type=getStringOption(e.type,["crossHatch","crossDiagonal","diagonalLeft","diagonalRight","horizontal","vertical"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[fl](e){e=e?e[fl]():"#FFFFFF";const t=this.color?this.color[fl]():"#000000",a="repeating-linear-gradient",n=`${e},${e} 5px,${t} 5px,${t} 10px`;switch(this.type){case"crossHatch":return`${a}(to top,${n}) ${a}(to right,${n})`;case"crossDiagonal":return`${a}(45deg,${n}) ${a}(-45deg,${n})`;case"diagonalLeft":return`${a}(45deg,${n})`;case"diagonalRight":return`${a}(-45deg,${n})`;case"horizontal":return`${a}(to top,${n})`;case"vertical":return`${a}(to right,${n})`}return""}}class Picture extends StringObject{constructor(e){super(Kl,"picture");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Proto extends XFAObject{constructor(e){super(Kl,"proto",!0);this.appearanceFilter=new XFAObjectArray;this.arc=new XFAObjectArray;this.area=new XFAObjectArray;this.assist=new XFAObjectArray;this.barcode=new XFAObjectArray;this.bindItems=new XFAObjectArray;this.bookend=new XFAObjectArray;this.boolean=new XFAObjectArray;this.border=new XFAObjectArray;this.break=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.button=new XFAObjectArray;this.calculate=new XFAObjectArray;this.caption=new XFAObjectArray;this.certificate=new XFAObjectArray;this.certificates=new XFAObjectArray;this.checkButton=new XFAObjectArray;this.choiceList=new XFAObjectArray;this.color=new XFAObjectArray;this.comb=new XFAObjectArray;this.connect=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.corner=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.dateTimeEdit=new XFAObjectArray;this.decimal=new XFAObjectArray;this.defaultUi=new XFAObjectArray;this.desc=new XFAObjectArray;this.digestMethod=new XFAObjectArray;this.digestMethods=new XFAObjectArray;this.draw=new XFAObjectArray;this.edge=new XFAObjectArray;this.encoding=new XFAObjectArray;this.encodings=new XFAObjectArray;this.encrypt=new XFAObjectArray;this.encryptData=new XFAObjectArray;this.encryption=new XFAObjectArray;this.encryptionMethod=new XFAObjectArray;this.encryptionMethods=new XFAObjectArray;this.event=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.execute=new XFAObjectArray;this.extras=new XFAObjectArray;this.field=new XFAObjectArray;this.fill=new XFAObjectArray;this.filter=new XFAObjectArray;this.float=new XFAObjectArray;this.font=new XFAObjectArray;this.format=new XFAObjectArray;this.handler=new XFAObjectArray;this.hyphenation=new XFAObjectArray;this.image=new XFAObjectArray;this.imageEdit=new XFAObjectArray;this.integer=new XFAObjectArray;this.issuers=new XFAObjectArray;this.items=new XFAObjectArray;this.keep=new XFAObjectArray;this.keyUsage=new XFAObjectArray;this.line=new XFAObjectArray;this.linear=new XFAObjectArray;this.lockDocument=new XFAObjectArray;this.manifest=new XFAObjectArray;this.margin=new XFAObjectArray;this.mdp=new XFAObjectArray;this.medium=new XFAObjectArray;this.message=new XFAObjectArray;this.numericEdit=new XFAObjectArray;this.occur=new XFAObjectArray;this.oid=new XFAObjectArray;this.oids=new XFAObjectArray;this.overflow=new XFAObjectArray;this.pageArea=new XFAObjectArray;this.pageSet=new XFAObjectArray;this.para=new XFAObjectArray;this.passwordEdit=new XFAObjectArray;this.pattern=new XFAObjectArray;this.picture=new XFAObjectArray;this.radial=new XFAObjectArray;this.reason=new XFAObjectArray;this.reasons=new XFAObjectArray;this.rectangle=new XFAObjectArray;this.ref=new XFAObjectArray;this.script=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.signData=new XFAObjectArray;this.signature=new XFAObjectArray;this.signing=new XFAObjectArray;this.solid=new XFAObjectArray;this.speak=new XFAObjectArray;this.stipple=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray;this.subjectDN=new XFAObjectArray;this.subjectDNs=new XFAObjectArray;this.submit=new XFAObjectArray;this.text=new XFAObjectArray;this.textEdit=new XFAObjectArray;this.time=new XFAObjectArray;this.timeStamp=new XFAObjectArray;this.toolTip=new XFAObjectArray;this.traversal=new XFAObjectArray;this.traverse=new XFAObjectArray;this.ui=new XFAObjectArray;this.validate=new XFAObjectArray;this.value=new XFAObjectArray;this.variables=new XFAObjectArray}}class Radial extends XFAObject{constructor(e){super(Kl,"radial",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toEdge","toCenter"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[fl](e){e=e?e[fl]():"#FFFFFF";const t=this.color?this.color[fl]():"#000000";return`radial-gradient(circle at center, ${"toEdge"===this.type?`${e},${t}`:`${t},${e}`})`}}class Reason extends StringObject{constructor(e){super(Kl,"reason");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Reasons extends XFAObject{constructor(e){super(Kl,"reasons",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.reason=new XFAObjectArray}}class Rectangle extends XFAObject{constructor(e){super(Kl,"rectangle",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.fill=null}[ol](){const e=this.edge.children.length?this.edge.children[0]:new Edge({}),t=e[fl](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[fl]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;const n=(this.corner.children.length?this.corner.children[0]:new Corner({}))[fl](),s={name:"svg",children:[{name:"rect",attributes:{xmlns:Gl,width:"100%",height:"100%",x:0,y:0,rx:n.radius,ry:n.radius,style:a}}],attributes:{xmlns:Gl,style:{overflow:"visible"},width:"100%",height:"100%"}};if(hasMargin(this[So]()[So]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[s]});s.attributes.style.position="absolute";return HTMLResult.success(s)}}class RefElement extends StringObject{constructor(e){super(Kl,"ref");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Script extends StringObject{constructor(e){super(Kl,"script");this.binding=e.binding||"";this.contentType=e.contentType||"";this.id=e.id||"";this.name=e.name||"";this.runAt=getStringOption(e.runAt,["client","both","server"]);this.use=e.use||"";this.usehref=e.usehref||""}}class SetProperty extends XFAObject{constructor(e){super(Kl,"setProperty");this.connection=e.connection||"";this.ref=e.ref||"";this.target=e.target||""}}class SignData extends XFAObject{constructor(e){super(Kl,"signData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["sign","clear","verify"]);this.ref=e.ref||"";this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Signature extends XFAObject{constructor(e){super(Kl,"signature",!0);this.id=e.id||"";this.type=getStringOption(e.type,["PDF1.3","PDF1.6"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.filter=null;this.manifest=null;this.margin=null}}class Signing extends XFAObject{constructor(e){super(Kl,"signing",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Solid extends XFAObject{constructor(e){super(Kl,"solid",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[fl](e){return e?e[fl]():"#FFFFFF"}}class Speak extends StringObject{constructor(e){super(Kl,"speak");this.disable=getInteger({data:e.disable,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.priority=getStringOption(e.priority,["custom","caption","name","toolTip"]);this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Stipple extends XFAObject{constructor(e){super(Kl,"stipple",!0);this.id=e.id||"";this.rate=getInteger({data:e.rate,defaultValue:50,validate:e=>e>=0&&e<=100});this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[fl](e){const t=this.rate/100;return Util.makeHexColor(Math.round(e.value.r*(1-t)+this.value.r*t),Math.round(e.value.g*(1-t)+this.value.g*t),Math.round(e.value.b*(1-t)+this.value.b*t))}}class Subform extends XFAObject{constructor(e){super(Kl,"subform",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.allowMacro=getInteger({data:e.allowMacro,defaultValue:0,validate:e=>1===e});this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.columnWidths=(e.columnWidths||"").trim().split(/\s+/).map(e=>"-1"===e?-1:getMeasurement(e));this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.mergeMode=getStringOption(e.mergeMode,["consumeData","matchTemplate"]);this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.restoreState=getStringOption(e.restoreState,["manual","auto"]);this.scope=getStringOption(e.scope,["name","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.bookend=null;this.border=null;this.break=null;this.calculate=null;this.desc=null;this.extras=null;this.keep=null;this.margin=null;this.occur=null;this.overflow=null;this.pageSet=null;this.para=null;this.traversal=null;this.validate=null;this.variables=null;this.area=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.connect=new XFAObjectArray;this.draw=new XFAObjectArray;this.event=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.proto=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}[vo](){const e=this[So]();return e instanceof SubformSet?e[vo]():e}[Ho](){return!0}[Po](){return this.layout.endsWith("-tb")&&0===this[oo].attempt&&this[oo].numberInLine>0||this[So]()[Po]()}*[yo](){yield*getContainedChildren(this)}[fo](){return flushHTML(this)}[Yi](e,t){addHTML(this,e,t)}[uo](){return getAvailableSpace(this)}[No](){const e=this[vo]();if(!e[No]())return!1;if(void 0!==this[oo]._isSplittable)return this[oo]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[oo]._isSplittable=!1;return!1}if(this.keep&&"none"!==this.keep.intact){this[oo]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[oo].numberInLine)return!1;this[oo]._isSplittable=!0;return!0}[ol](e){setTabIndex(this);if(this.break){if("auto"!==this.break.after||""!==this.break.afterTarget){const e=new BreakAfter({targetType:this.break.after,target:this.break.afterTarget,startNew:this.break.startNew.toString()});e[xo]=this[xo];this[Ji](e);this.breakAfter.push(e)}if("auto"!==this.break.before||""!==this.break.beforeTarget){const e=new BreakBefore({targetType:this.break.before,target:this.break.beforeTarget,startNew:this.break.startNew.toString()});e[xo]=this[xo];this[Ji](e);this.breakBefore.push(e)}if(""!==this.break.overflowTarget){const e=new Overflow({target:this.break.overflowTarget,leader:this.break.overflowLeader,trailer:this.break.overflowTrailer});e[xo]=this[xo];this[Ji](e);this.overflow.push(e)}this[Jo](this.break);this.break=null}if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;(this.breakBefore.children.length>1||this.breakAfter.children.length>1)&&warn("XFA - Several breakBefore or breakAfter in subforms: please file a bug.");if(this.breakBefore.children.length>=1){const e=this.breakBefore.children[0];if(handleBreak(e))return HTMLResult.breakNode(e)}if(this[oo]?.afterBreakAfter)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[cl],class:[]};setAccess(this,a.class);this[oo]||=Object.create(null);Object.assign(this[oo],{children:t,line:null,attributes:a,attempt:0,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const n=this[Ao](),s=n[oo].noLayoutFailure,r=this[No]();r||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const i=new Set(["area","draw","exclGroup","field","subform","subformSet"]);if(this.layout.includes("row")){const e=this[vo]().columnWidths;if(Array.isArray(e)&&e.length>0){this[oo].columnWidths=e;this[oo].currentColumn=0}}const o=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),l=["xfaSubform"],f=layoutClass(this);f&&l.push(f);a.style=o;a.class=l;this.name&&(a.xfaName=this.name);if(this.overflow){const t=this.overflow[wo]();if(t.addLeader){t.addLeader=!1;handleOverflow(this,t.leader,e)}}this[Yo]();const c="lr-tb"===this.layout||"rl-tb"===this.layout,h=c?2:1;for(;this[oo].attempt=1){const e=this.breakAfter.children[0];if(handleBreak(e)){this[oo].afterBreakAfter=w;return HTMLResult.breakNode(e)}}delete this[oo];return w}}class SubformSet extends XFAObject{constructor(e){super(Kl,"subformSet",!0);this.id=e.id||"";this.name=e.name||"";this.relation=getStringOption(e.relation,["ordered","choice","unordered"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.bookend=null;this.break=null;this.desc=null;this.extras=null;this.occur=null;this.overflow=null;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[yo](){yield*getContainedChildren(this)}[vo](){let e=this[So]();for(;!(e instanceof Subform);)e=e[So]();return e}[Ho](){return!0}}class SubjectDN extends ContentObject{constructor(e){super(Kl,"subjectDN");this.delimiter=e.delimiter||",";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){this[so]=new Map(this[so].split(this.delimiter).map(e=>{(e=e.split("=",2))[0]=e[0].trim();return e}))}}class SubjectDNs extends XFAObject{constructor(e){super(Kl,"subjectDNs",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.subjectDN=new XFAObjectArray}}class Submit extends XFAObject{constructor(e){super(Kl,"submit",!0);this.embedPDF=getInteger({data:e.embedPDF,defaultValue:0,validate:e=>1===e});this.format=getStringOption(e.format,["xdp","formdata","pdf","urlencoded","xfd","xml"]);this.id=e.id||"";this.target=e.target||"";this.textEncoding=getKeyword({data:e.textEncoding?e.textEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.use=e.use||"";this.usehref=e.usehref||"";this.xdpContent=e.xdpContent||"";this.encrypt=null;this.encryptData=new XFAObjectArray;this.signData=new XFAObjectArray}}class Template extends XFAObject{constructor(e){super(Kl,"template",!0);this.baseProfile=getStringOption(e.baseProfile,["full","interactiveForms"]);this.extras=null;this.subform=new XFAObjectArray}[lo](){0===this.subform.children.length&&warn("XFA - No subforms in template node.");this.subform.children.length>=2&&warn("XFA - Several subforms in template node: please file a bug.");this[sl]=5e3}[No](){return!0}[el](e,t){return e.startsWith("#")?[this[Io].get(e.slice(1))]:searchNode(this,t,e,!0,!0)}*[il](){if(!this.subform.children.length)return HTMLResult.success({name:"div",children:[]});this[oo]={overflowNode:null,firstUnsplittable:null,currentContentArea:null,currentPageArea:null,noLayoutFailure:!1,pageNumber:1,pagePosition:"first",oddOrEven:"odd",blankOrNotBlank:"nonBlank",paraStack:[]};const e=this.subform.children[0];e.pageSet[eo]();const t=e.pageSet.pageArea.children,a={name:"div",children:[]};let n=null,s=null,r=null;if(e.breakBefore.children.length>=1){s=e.breakBefore.children[0];r=s.target}else if(e.subform.children.length>=1&&e.subform.children[0].breakBefore.children.length>=1){s=e.subform.children[0].breakBefore.children[0];r=s.target}else if(e.break?.beforeTarget){s=e.break;r=s.beforeTarget}else if(e.subform.children.length>=1&&e.subform.children[0].break?.beforeTarget){s=e.subform.children[0].break;r=s.beforeTarget}if(s){const e=this[el](r,s[So]());if(e instanceof PageArea){n=e;s[oo]={}}}n||=t[0];n[oo]={numberOfUse:1};const i=n[So]();i[oo]={numberOfUse:1,pageIndex:i.pageArea.children.indexOf(n),pageSetIndex:0};let o,l=null,f=null,c=!0,h=0,u=0;for(;;){if(c)h=0;else{a.children.pop();if(3===++h){warn("XFA - Something goes wrong: please file a bug.");return a}}o=null;this[oo].currentPageArea=n;const t=n[ol]().html;a.children.push(t);if(l){this[oo].noLayoutFailure=!0;t.children.push(l[ol](n[oo].space).html);l=null}if(f){this[oo].noLayoutFailure=!0;t.children.push(f[ol](n[oo].space).html);f=null}const s=n.contentArea.children,r=t.children.filter(e=>e.attributes.class.includes("xfaContentarea"));c=!1;this[oo].firstUnsplittable=null;this[oo].noLayoutFailure=!1;const flush=t=>{const a=e[fo]();if(a){c||=a.children?.length>0;r[t].children.push(a)}};for(let t=u,n=s.length;t0;r[t].children.push(h.html)}else!c&&a.children.length>1&&a.children.pop();return a}if(h.isBreak()){const e=h.breakNode;flush(t);if("auto"===e.targetType)continue;if(e.leader){l=this[el](e.leader,e[So]());l=l?l[0]:null}if(e.trailer){f=this[el](e.trailer,e[So]());f=f?f[0]:null}if("pageArea"===e.targetType){o=e[oo].target;t=1/0}else if(e[oo].target){o=e[oo].target;u=e[oo].index+1;t=1/0}else t=e[oo].index;continue}if(this[oo].overflowNode){const e=this[oo].overflowNode;this[oo].overflowNode=null;const a=e[wo](),n=a.target;a.addLeader=null!==a.leader;a.addTrailer=null!==a.trailer;flush(t);const r=t;t=1/0;if(n instanceof PageArea)o=n;else if(n instanceof ContentArea){const e=s.indexOf(n);if(-1!==e)e>r?t=e-1:u=e;else{o=n[So]();u=o.contentArea.children.indexOf(n)}}continue}flush(t)}this[oo].pageNumber+=1;o&&(o[_o]()?o[oo].numberOfUse+=1:o=null);n=o||n[qo]();yield null}}}class Text extends ContentObject{constructor(e){super(Kl,"text");this.id=e.id||"";this.maxChars=getInteger({data:e.maxChars,defaultValue:0,validate:e=>e>=0});this.name=e.name||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}[$i](){return!0}[Wo](e){if(e[Lo]===ul.xhtml.id){this[so]=e;return!0}warn(`XFA - Invalid content in Text: ${e[Uo]}.`);return!1}[Go](e){this[so]instanceof XFAObject||super[Go](e)}[lo](){"string"==typeof this[so]&&(this[so]=this[so].replaceAll("\r\n","\n"))}[wo](){return"string"==typeof this[so]?this[so].split(/[\u2029\u2028\n]/).filter(e=>!!e).join("\n"):this[so][rl]()}[ol](e){if("string"==typeof this[so]){const e=valueToHtml(this[so]).html;if(this[so].includes("\u2029")){e.name="div";e.children=[];this[so].split("\u2029").map(e=>e.split(/[\u2028\n]/).flatMap(e=>[{name:"span",value:e},{name:"br"}])).forEach(t=>{e.children.push({name:"p",children:t})})}else if(/[\u2028\n]/.test(this[so])){e.name="div";e.children=[];this[so].split(/[\u2028\n]/).forEach(t=>{e.children.push({name:"span",value:t},{name:"br"})})}return HTMLResult.success(e)}return this[so][ol](e)}}class TextEdit extends XFAObject{constructor(e){super(Kl,"textEdit",!0);this.allowRichText=getInteger({data:e.allowRichText,defaultValue:0,validate:e=>1===e});this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.multiLine=getInteger({data:e.multiLine,defaultValue:"",validate:e=>0===e||1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.vScrollPolicy=getStringOption(e.vScrollPolicy,["auto","off","on"]);this.border=null;this.comb=null;this.extras=null;this.margin=null}[ol](e){const t=toStyle(this,"border","font","margin");let a;const n=this[So]()[So]();""===this.multiLine&&(this.multiLine=n instanceof Draw?1:0);a=1===this.multiLine?{name:"textarea",attributes:{dataId:n[ro]?.[cl]||n[cl],fieldId:n[cl],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(n),"aria-required":!1}}:{name:"input",attributes:{type:"text",dataId:n[ro]?.[cl]||n[cl],fieldId:n[cl],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(n),"aria-required":!1}};if(isRequired(n)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Time extends StringObject{constructor(e){super(Kl,"time");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[lo](){const e=this[so].trim();this[so]=e?new Date(e):null}[ol](e){return valueToHtml(this[so]?this[so].toString():"")}}class TimeStamp extends XFAObject{constructor(e){super(Kl,"timeStamp");this.id=e.id||"";this.server=e.server||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class ToolTip extends StringObject{constructor(e){super(Kl,"toolTip");this.id=e.id||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Traversal extends XFAObject{constructor(e){super(Kl,"traversal",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.traverse=new XFAObjectArray}}class Traverse extends XFAObject{constructor(e){super(Kl,"traverse",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["next","back","down","first","left","right","up"]);this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.script=null}get name(){return this.operation}[Eo](){return!1}}class Ui extends XFAObject{constructor(e){super(Kl,"ui",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null;this.barcode=null;this.button=null;this.checkButton=null;this.choiceList=null;this.dateTimeEdit=null;this.defaultUi=null;this.imageEdit=null;this.numericEdit=null;this.passwordEdit=null;this.signature=null;this.textEdit=null}[wo](){if(void 0===this[oo]){for(const e of Object.getOwnPropertyNames(this)){if("extras"===e||"picture"===e)continue;const t=this[e];if(t instanceof XFAObject){this[oo]=t;return t}}this[oo]=null}return this[oo]}[ol](e){const t=this[wo]();return t?t[ol](e):HTMLResult.EMPTY}}class Validate extends XFAObject{constructor(e){super(Kl,"validate",!0);this.formatTest=getStringOption(e.formatTest,["warning","disabled","error"]);this.id=e.id||"";this.nullTest=getStringOption(e.nullTest,["disabled","error","warning"]);this.scriptTest=getStringOption(e.scriptTest,["error","disabled","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.picture=null;this.script=null}}class Value extends XFAObject{constructor(e){super(Kl,"value",!0);this.id=e.id||"";this.override=getInteger({data:e.override,defaultValue:0,validate:e=>1===e});this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.arc=null;this.boolean=null;this.date=null;this.dateTime=null;this.decimal=null;this.exData=null;this.float=null;this.image=null;this.integer=null;this.line=null;this.rectangle=null;this.text=null;this.time=null}[nl](e){const t=this[So]();if(t instanceof Field&&t.ui?.imageEdit){if(!this.image){this.image=new Image({});this[Ji](this.image)}this.image[so]=e[so];return}const a=e[Uo];if(null===this[a]){for(const e of Object.getOwnPropertyNames(this)){const t=this[e];if(t instanceof XFAObject){this[e]=null;this[Jo](t)}}this[e[Uo]]=e;this[Ji](e)}else this[a][so]=e[so]}[rl](){if(this.exData)return"string"==typeof this.exData[so]?this.exData[so].trim():this.exData[so][rl]().trim();for(const e of Object.getOwnPropertyNames(this)){if("image"===e)continue;const t=this[e];if(t instanceof XFAObject)return(t[so]||"").toString().trim()}return null}[ol](e){for(const t of Object.getOwnPropertyNames(this)){const a=this[t];if(a instanceof XFAObject)return a[ol](e)}return HTMLResult.EMPTY}}class Variables extends XFAObject{constructor(e){super(Kl,"variables",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.manifest=new XFAObjectArray;this.script=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[Eo](){return!0}}class TemplateNamespace{static[hl](e,t){if(Object.hasOwn(TemplateNamespace,e)){const a=TemplateNamespace[e](t);a[al](t);return a}}static appearanceFilter(e){return new AppearanceFilter(e)}static arc(e){return new Arc(e)}static area(e){return new Area(e)}static assist(e){return new Assist(e)}static barcode(e){return new Barcode(e)}static bind(e){return new Bind(e)}static bindItems(e){return new BindItems(e)}static bookend(e){return new Bookend(e)}static boolean(e){return new BooleanElement(e)}static border(e){return new Border(e)}static break(e){return new Break(e)}static breakAfter(e){return new BreakAfter(e)}static breakBefore(e){return new BreakBefore(e)}static button(e){return new Button(e)}static calculate(e){return new Calculate(e)}static caption(e){return new Caption(e)}static certificate(e){return new Certificate(e)}static certificates(e){return new Certificates(e)}static checkButton(e){return new CheckButton(e)}static choiceList(e){return new ChoiceList(e)}static color(e){return new Color(e)}static comb(e){return new Comb(e)}static connect(e){return new Connect(e)}static contentArea(e){return new ContentArea(e)}static corner(e){return new Corner(e)}static date(e){return new DateElement(e)}static dateTime(e){return new DateTime(e)}static dateTimeEdit(e){return new DateTimeEdit(e)}static decimal(e){return new Decimal(e)}static defaultUi(e){return new DefaultUi(e)}static desc(e){return new Desc(e)}static digestMethod(e){return new DigestMethod(e)}static digestMethods(e){return new DigestMethods(e)}static draw(e){return new Draw(e)}static edge(e){return new Edge(e)}static encoding(e){return new Encoding(e)}static encodings(e){return new Encodings(e)}static encrypt(e){return new Encrypt(e)}static encryptData(e){return new EncryptData(e)}static encryption(e){return new Encryption(e)}static encryptionMethod(e){return new EncryptionMethod(e)}static encryptionMethods(e){return new EncryptionMethods(e)}static event(e){return new Event(e)}static exData(e){return new ExData(e)}static exObject(e){return new ExObject(e)}static exclGroup(e){return new ExclGroup(e)}static execute(e){return new Execute(e)}static extras(e){return new Extras(e)}static field(e){return new Field(e)}static fill(e){return new Fill(e)}static filter(e){return new Filter(e)}static float(e){return new Float(e)}static font(e){return new template_Font(e)}static format(e){return new Format(e)}static handler(e){return new Handler(e)}static hyphenation(e){return new Hyphenation(e)}static image(e){return new Image(e)}static imageEdit(e){return new ImageEdit(e)}static integer(e){return new Integer(e)}static issuers(e){return new Issuers(e)}static items(e){return new Items(e)}static keep(e){return new Keep(e)}static keyUsage(e){return new KeyUsage(e)}static line(e){return new Line(e)}static linear(e){return new Linear(e)}static lockDocument(e){return new LockDocument(e)}static manifest(e){return new Manifest(e)}static margin(e){return new Margin(e)}static mdp(e){return new Mdp(e)}static medium(e){return new Medium(e)}static message(e){return new Message(e)}static numericEdit(e){return new NumericEdit(e)}static occur(e){return new Occur(e)}static oid(e){return new Oid(e)}static oids(e){return new Oids(e)}static overflow(e){return new Overflow(e)}static pageArea(e){return new PageArea(e)}static pageSet(e){return new PageSet(e)}static para(e){return new Para(e)}static passwordEdit(e){return new PasswordEdit(e)}static pattern(e){return new template_Pattern(e)}static picture(e){return new Picture(e)}static proto(e){return new Proto(e)}static radial(e){return new Radial(e)}static reason(e){return new Reason(e)}static reasons(e){return new Reasons(e)}static rectangle(e){return new Rectangle(e)}static ref(e){return new RefElement(e)}static script(e){return new Script(e)}static setProperty(e){return new SetProperty(e)}static signData(e){return new SignData(e)}static signature(e){return new Signature(e)}static signing(e){return new Signing(e)}static solid(e){return new Solid(e)}static speak(e){return new Speak(e)}static stipple(e){return new Stipple(e)}static subform(e){return new Subform(e)}static subformSet(e){return new SubformSet(e)}static subjectDN(e){return new SubjectDN(e)}static subjectDNs(e){return new SubjectDNs(e)}static submit(e){return new Submit(e)}static template(e){return new Template(e)}static text(e){return new Text(e)}static textEdit(e){return new TextEdit(e)}static time(e){return new Time(e)}static timeStamp(e){return new TimeStamp(e)}static toolTip(e){return new ToolTip(e)}static traversal(e){return new Traversal(e)}static traverse(e){return new Traverse(e)}static ui(e){return new Ui(e)}static validate(e){return new Validate(e)}static value(e){return new Value(e)}static variables(e){return new Variables(e)}}const Jl=ul.datasets.id;function createText(e){const t=new Text({});t[so]=e;return t}class Binder{constructor(e){this.root=e;this.datasets=e.datasets;this.data=e.datasets?.data||new XmlObject(ul.datasets.id,"data");this.emptyMerge=0===this.data[ko]().length;this.root.form=this.form=e.template[ao]()}_isConsumeData(){return!this.emptyMerge&&this._mergeMode}_isMatchTemplate(){return!this._isConsumeData()}bind(){this._bindElement(this.form,this.data);return this.form}getData(){return this.data}_bindValue(e,t,a){e[ro]=t;if(e[Co]())if(t[Bo]()){const a=t[bo]();e[nl](createText(a))}else if(e instanceof Field&&"multiSelect"===e.ui?.choiceList?.open){const a=t[ko]().map(e=>e[so].trim()).join("\n");e[nl](createText(a))}else this._isConsumeData()&&warn("XFA - Nodes haven't the same type.");else!t[Bo]()||this._isMatchTemplate()?this._bindElement(e,t):warn("XFA - Nodes haven't the same type.")}_findDataByNameToConsume(e,t,a,n){if(!e)return null;let s,r;for(let n=0;n<3;n++){s=a[jo](e,!1,!0);for(;;){r=s.next().value;if(!r)break;if(t===r[Bo]())return r}if(a[Lo]===ul.datasets.id&&"data"===a[Uo])break;a=a[So]()}if(!n)return null;s=this.data[jo](e,!0,!1);r=s.next().value;if(r)return r;s=this.data[co](e,!0);r=s.next().value;return r?.[Bo]()?r:null}_setProperties(e,t){if(Object.hasOwn(e,"setProperty"))for(const{ref:a,target:n,connection:s}of e.setProperty.children){if(s)continue;if(!a)continue;const r=searchNode(this.root,t,a,!1,!1);if(!r){warn(`XFA - Invalid reference: ${a}.`);continue}const[i]=r;if(!i[Do](this.data)){warn("XFA - Invalid node: must be a data node.");continue}const o=searchNode(this.root,e,n,!1,!1);if(!o){warn(`XFA - Invalid target: ${n}.`);continue}const[l]=o;if(!l[Do](e)){warn("XFA - Invalid target: must be a property or subproperty.");continue}const f=l[So]();if(l instanceof SetProperty||f instanceof SetProperty){warn("XFA - Invalid target: cannot be a setProperty or one of its properties.");continue}if(l instanceof BindItems||f instanceof BindItems){warn("XFA - Invalid target: cannot be a bindItems or one of its properties.");continue}const c=i[rl](),h=l[Uo];if(l instanceof XFAAttribute){const e=Object.create(null);e[h]=c;const t=Reflect.construct(Object.getPrototypeOf(f).constructor,[e]);f[h]=t[h];continue}if(Object.hasOwn(l,so)){l[ro]=i;l[so]=c;l[lo]()}else warn("XFA - Invalid node to use in setProperty")}}_bindItems(e,t){if(!Object.hasOwn(e,"items")||!Object.hasOwn(e,"bindItems")||e.bindItems.isEmpty())return;for(const t of e.items.children)e[Jo](t);e.items.clear();const a=new Items({}),n=new Items({});e[Ji](a);e.items.push(a);e[Ji](n);e.items.push(n);for(const{ref:s,labelRef:r,valueRef:i,connection:o}of e.bindItems.children){if(o)continue;if(!s)continue;const e=searchNode(this.root,t,s,!1,!1);if(e)for(const t of e){if(!t[Do](this.datasets)){warn(`XFA - Invalid ref (${s}): must be a datasets child.`);continue}const e=searchNode(this.root,t,r,!0,!1);if(!e){warn(`XFA - Invalid label: ${r}.`);continue}const[o]=e;if(!o[Do](this.datasets)){warn("XFA - Invalid label: must be a datasets child.");continue}const l=searchNode(this.root,t,i,!0,!1);if(!l){warn(`XFA - Invalid value: ${i}.`);continue}const[f]=l;if(!f[Do](this.datasets)){warn("XFA - Invalid value: must be a datasets child.");continue}const c=createText(o[rl]()),h=createText(f[rl]());a[Ji](c);a.text.push(c);n[Ji](h);n.text.push(h)}else warn(`XFA - Invalid reference: ${s}.`)}}_bindOccurrences(e,t,a){let n;if(t.length>1){n=e[ao]();n[Jo](n.occur);n.occur=null}this._bindValue(e,t[0],a);this._setProperties(e,t[0]);this._bindItems(e,t[0]);if(1===t.length)return;const s=e[So](),r=e[Uo],i=s[Fo](e);for(let e=1,o=t.length;et.name===e.name).length:a[n].children.length;const r=a[Fo](e)+1,i=t.initial-s;if(i){const t=e[ao]();t[Jo](t.occur);t.occur=null;a[n].push(t);a[Oo](r,t);for(let e=1;e0)this._bindOccurrences(n,[e[0]],null);else if(this.emptyMerge){const e=t[Lo]===Jl?-1:t[Lo],a=n[ro]=new XmlObject(e,n.name||"root");t[Ji](a);this._bindElement(n,a)}continue}if(!n[Ho]())continue;let e=!1,s=null,r=null,i=null;if(n.bind){switch(n.bind.match){case"none":this._setAndBind(n,t);continue;case"global":e=!0;break;case"dataRef":if(!n.bind.ref){warn(`XFA - ref is empty in node ${n[Uo]}.`);this._setAndBind(n,t);continue}r=n.bind.ref}n.bind.picture&&(s=n.bind.picture[so])}const[o,l]=this._getOccurInfo(n);if(r){i=searchNode(this.root,t,r,!0,!1);if(null===i){i=createDataNode(this.data,t,r);if(!i)continue;this._isConsumeData()&&(i[no]=!0);this._setAndBind(n,i);continue}this._isConsumeData()&&(i=i.filter(e=>!e[no]));i.length>l?i=i.slice(0,l):0===i.length&&(i=null);i&&this._isConsumeData()&&i.forEach(e=>{e[no]=!0})}else{if(!n.name){this._setAndBind(n,t);continue}if(this._isConsumeData()){const a=[];for(;a.length0?a:null}else{i=t[jo](n.name,!1,this.emptyMerge).next().value;if(!i){if(0===o){a.push(n);continue}const e=t[Lo]===Jl?-1:t[Lo];i=n[ro]=new XmlObject(e,n.name);this.emptyMerge&&(i[no]=!0);t[Ji](i);this._setAndBind(n,i);continue}this.emptyMerge&&(i[no]=!0);i=[i]}}i?this._bindOccurrences(n,i,s):o>0?this._setAndBind(n,t):a.push(n)}a.forEach(e=>e[So]()[Jo](e))}}class DataHandler{constructor(e,t){this.data=t;this.dataset=e.datasets||null}serialize(e){const t=[[-1,this.data[ko]()]];for(;t.length>0;){const a=t.at(-1),[n,s]=a;if(n+1===s.length){t.pop();continue}const r=s[++a[0]],i=e.get(r[cl]);if(i)r[nl](i);else{const t=r[ho]();for(const a of t.values()){const t=e.get(a[cl]);if(t){a[nl](t);break}}}const o=r[ko]();o.length>0&&t.push([-1,o])}const a=[''];if(this.dataset)for(const e of this.dataset[ko]())"data"!==e[Uo]&&e[ll](a);this.data[ll](a);a.push("");return a.join("")}}const Ql=ul.config.id;class Acrobat extends XFAObject{constructor(e){super(Ql,"acrobat",!0);this.acrobat7=null;this.autoSave=null;this.common=null;this.validate=null;this.validateApprovalSignatures=null;this.submitUrl=new XFAObjectArray}}class Acrobat7 extends XFAObject{constructor(e){super(Ql,"acrobat7",!0);this.dynamicRender=null}}class ADBE_JSConsole extends OptionObject{constructor(e){super(Ql,"ADBE_JSConsole",["delegate","Enable","Disable"])}}class ADBE_JSDebugger extends OptionObject{constructor(e){super(Ql,"ADBE_JSDebugger",["delegate","Enable","Disable"])}}class AddSilentPrint extends Option01{constructor(e){super(Ql,"addSilentPrint")}}class AddViewerPreferences extends Option01{constructor(e){super(Ql,"addViewerPreferences")}}class AdjustData extends Option10{constructor(e){super(Ql,"adjustData")}}class AdobeExtensionLevel extends IntegerObject{constructor(e){super(Ql,"adobeExtensionLevel",0,e=>e>=1&&e<=8)}}class Agent extends XFAObject{constructor(e){super(Ql,"agent",!0);this.name=e.name?e.name.trim():"";this.common=new XFAObjectArray}}class AlwaysEmbed extends ContentObject{constructor(e){super(Ql,"alwaysEmbed")}}class Amd extends StringObject{constructor(e){super(Ql,"amd")}}class config_Area extends XFAObject{constructor(e){super(Ql,"area");this.level=getInteger({data:e.level,defaultValue:0,validate:e=>e>=1&&e<=3});this.name=getStringOption(e.name,["","barcode","coreinit","deviceDriver","font","general","layout","merge","script","signature","sourceSet","templateCache"])}}class Attributes extends OptionObject{constructor(e){super(Ql,"attributes",["preserve","delegate","ignore"])}}class AutoSave extends OptionObject{constructor(e){super(Ql,"autoSave",["disabled","enabled"])}}class Base extends StringObject{constructor(e){super(Ql,"base")}}class BatchOutput extends XFAObject{constructor(e){super(Ql,"batchOutput");this.format=getStringOption(e.format,["none","concat","zip","zipCompress"])}}class BehaviorOverride extends ContentObject{constructor(e){super(Ql,"behaviorOverride")}[lo](){this[so]=new Map(this[so].trim().split(/\s+/).filter(e=>e.includes(":")).map(e=>e.split(":",2)))}}class Cache extends XFAObject{constructor(e){super(Ql,"cache",!0);this.templateCache=null}}class Change extends Option01{constructor(e){super(Ql,"change")}}class Common extends XFAObject{constructor(e){super(Ql,"common",!0);this.data=null;this.locale=null;this.localeSet=null;this.messaging=null;this.suppressBanner=null;this.template=null;this.validationMessaging=null;this.versionControl=null;this.log=new XFAObjectArray}}class Compress extends XFAObject{constructor(e){super(Ql,"compress");this.scope=getStringOption(e.scope,["imageOnly","document"])}}class CompressLogicalStructure extends Option01{constructor(e){super(Ql,"compressLogicalStructure")}}class CompressObjectStream extends Option10{constructor(e){super(Ql,"compressObjectStream")}}class Compression extends XFAObject{constructor(e){super(Ql,"compression",!0);this.compressLogicalStructure=null;this.compressObjectStream=null;this.level=null;this.type=null}}class Config extends XFAObject{constructor(e){super(Ql,"config",!0);this.acrobat=null;this.present=null;this.trace=null;this.agent=new XFAObjectArray}}class Conformance extends OptionObject{constructor(e){super(Ql,"conformance",["A","B"])}}class ContentCopy extends Option01{constructor(e){super(Ql,"contentCopy")}}class Copies extends IntegerObject{constructor(e){super(Ql,"copies",1,e=>e>=1)}}class Creator extends StringObject{constructor(e){super(Ql,"creator")}}class CurrentPage extends IntegerObject{constructor(e){super(Ql,"currentPage",0,e=>e>=0)}}class Data extends XFAObject{constructor(e){super(Ql,"data",!0);this.adjustData=null;this.attributes=null;this.incrementalLoad=null;this.outputXSL=null;this.range=null;this.record=null;this.startNode=null;this.uri=null;this.window=null;this.xsl=null;this.excludeNS=new XFAObjectArray;this.transform=new XFAObjectArray}}class Debug extends XFAObject{constructor(e){super(Ql,"debug",!0);this.uri=null}}class DefaultTypeface extends ContentObject{constructor(e){super(Ql,"defaultTypeface");this.writingScript=getStringOption(e.writingScript,["*","Arabic","Cyrillic","EastEuropeanRoman","Greek","Hebrew","Japanese","Korean","Roman","SimplifiedChinese","Thai","TraditionalChinese","Vietnamese"])}}class Destination extends OptionObject{constructor(e){super(Ql,"destination",["pdf","pcl","ps","webClient","zpl"])}}class DocumentAssembly extends Option01{constructor(e){super(Ql,"documentAssembly")}}class Driver extends XFAObject{constructor(e){super(Ql,"driver",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class DuplexOption extends OptionObject{constructor(e){super(Ql,"duplexOption",["simplex","duplexFlipLongEdge","duplexFlipShortEdge"])}}class DynamicRender extends OptionObject{constructor(e){super(Ql,"dynamicRender",["forbidden","required"])}}class Embed extends Option01{constructor(e){super(Ql,"embed")}}class config_Encrypt extends Option01{constructor(e){super(Ql,"encrypt")}}class config_Encryption extends XFAObject{constructor(e){super(Ql,"encryption",!0);this.encrypt=null;this.encryptionLevel=null;this.permissions=null}}class EncryptionLevel extends OptionObject{constructor(e){super(Ql,"encryptionLevel",["40bit","128bit"])}}class Enforce extends StringObject{constructor(e){super(Ql,"enforce")}}class Equate extends XFAObject{constructor(e){super(Ql,"equate");this.force=getInteger({data:e.force,defaultValue:1,validate:e=>0===e});this.from=e.from||"";this.to=e.to||""}}class EquateRange extends XFAObject{constructor(e){super(Ql,"equateRange");this.from=e.from||"";this.to=e.to||"";this._unicodeRange=e.unicodeRange||""}get unicodeRange(){const e=[],t=/U\+([0-9a-fA-F]+)/,a=this._unicodeRange;for(let n of a.split(",").map(e=>e.trim()).filter(e=>!!e)){n=n.split("-",2).map(e=>{const a=e.match(t);return a?parseInt(a[1],16):0});1===n.length&&n.push(n[0]);e.push(n)}return shadow(this,"unicodeRange",e)}}class Exclude extends ContentObject{constructor(e){super(Ql,"exclude")}[lo](){this[so]=this[so].trim().split(/\s+/).filter(e=>e&&["calculate","close","enter","exit","initialize","ready","validate"].includes(e))}}class ExcludeNS extends StringObject{constructor(e){super(Ql,"excludeNS")}}class FlipLabel extends OptionObject{constructor(e){super(Ql,"flipLabel",["usePrinterSetting","on","off"])}}class config_FontInfo extends XFAObject{constructor(e){super(Ql,"fontInfo",!0);this.embed=null;this.map=null;this.subsetBelow=null;this.alwaysEmbed=new XFAObjectArray;this.defaultTypeface=new XFAObjectArray;this.neverEmbed=new XFAObjectArray}}class FormFieldFilling extends Option01{constructor(e){super(Ql,"formFieldFilling")}}class GroupParent extends StringObject{constructor(e){super(Ql,"groupParent")}}class IfEmpty extends OptionObject{constructor(e){super(Ql,"ifEmpty",["dataValue","dataGroup","ignore","remove"])}}class IncludeXDPContent extends StringObject{constructor(e){super(Ql,"includeXDPContent")}}class IncrementalLoad extends OptionObject{constructor(e){super(Ql,"incrementalLoad",["none","forwardOnly"])}}class IncrementalMerge extends Option01{constructor(e){super(Ql,"incrementalMerge")}}class Interactive extends Option01{constructor(e){super(Ql,"interactive")}}class Jog extends OptionObject{constructor(e){super(Ql,"jog",["usePrinterSetting","none","pageSet"])}}class LabelPrinter extends XFAObject{constructor(e){super(Ql,"labelPrinter",!0);this.name=getStringOption(e.name,["zpl","dpl","ipl","tcpl"]);this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class Layout extends OptionObject{constructor(e){super(Ql,"layout",["paginate","panel"])}}class Level extends IntegerObject{constructor(e){super(Ql,"level",0,e=>e>0)}}class Linearized extends Option01{constructor(e){super(Ql,"linearized")}}class Locale extends StringObject{constructor(e){super(Ql,"locale")}}class LocaleSet extends StringObject{constructor(e){super(Ql,"localeSet")}}class Log extends XFAObject{constructor(e){super(Ql,"log",!0);this.mode=null;this.threshold=null;this.to=null;this.uri=null}}class MapElement extends XFAObject{constructor(e){super(Ql,"map",!0);this.equate=new XFAObjectArray;this.equateRange=new XFAObjectArray}}class MediumInfo extends XFAObject{constructor(e){super(Ql,"mediumInfo",!0);this.map=null}}class config_Message extends XFAObject{constructor(e){super(Ql,"message",!0);this.msgId=null;this.severity=null}}class Messaging extends XFAObject{constructor(e){super(Ql,"messaging",!0);this.message=new XFAObjectArray}}class Mode extends OptionObject{constructor(e){super(Ql,"mode",["append","overwrite"])}}class ModifyAnnots extends Option01{constructor(e){super(Ql,"modifyAnnots")}}class MsgId extends IntegerObject{constructor(e){super(Ql,"msgId",1,e=>e>=1)}}class NameAttr extends StringObject{constructor(e){super(Ql,"nameAttr")}}class NeverEmbed extends ContentObject{constructor(e){super(Ql,"neverEmbed")}}class NumberOfCopies extends IntegerObject{constructor(e){super(Ql,"numberOfCopies",null,e=>e>=2&&e<=5)}}class OpenAction extends XFAObject{constructor(e){super(Ql,"openAction",!0);this.destination=null}}class Output extends XFAObject{constructor(e){super(Ql,"output",!0);this.to=null;this.type=null;this.uri=null}}class OutputBin extends StringObject{constructor(e){super(Ql,"outputBin")}}class OutputXSL extends XFAObject{constructor(e){super(Ql,"outputXSL",!0);this.uri=null}}class Overprint extends OptionObject{constructor(e){super(Ql,"overprint",["none","both","draw","field"])}}class Packets extends StringObject{constructor(e){super(Ql,"packets")}[lo](){"*"!==this[so]&&(this[so]=this[so].trim().split(/\s+/).filter(e=>["config","datasets","template","xfdf","xslt"].includes(e)))}}class PageOffset extends XFAObject{constructor(e){super(Ql,"pageOffset");this.x=getInteger({data:e.x,defaultValue:"useXDCSetting",validate:e=>!0});this.y=getInteger({data:e.y,defaultValue:"useXDCSetting",validate:e=>!0})}}class PageRange extends StringObject{constructor(e){super(Ql,"pageRange")}[lo](){const e=this[so].trim().split(/\s+/).map(e=>parseInt(e,10)),t=[];for(let a=0,n=e.length;a!1)}}class Pcl extends XFAObject{constructor(e){super(Ql,"pcl",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.pageOffset=null;this.staple=null;this.xdc=null}}class Pdf extends XFAObject{constructor(e){super(Ql,"pdf",!0);this.name=e.name||"";this.adobeExtensionLevel=null;this.batchOutput=null;this.compression=null;this.creator=null;this.encryption=null;this.fontInfo=null;this.interactive=null;this.linearized=null;this.openAction=null;this.pdfa=null;this.producer=null;this.renderPolicy=null;this.scriptModel=null;this.silentPrint=null;this.submitFormat=null;this.tagged=null;this.version=null;this.viewerPreferences=null;this.xdc=null}}class Pdfa extends XFAObject{constructor(e){super(Ql,"pdfa",!0);this.amd=null;this.conformance=null;this.includeXDPContent=null;this.part=null}}class Permissions extends XFAObject{constructor(e){super(Ql,"permissions",!0);this.accessibleContent=null;this.change=null;this.contentCopy=null;this.documentAssembly=null;this.formFieldFilling=null;this.modifyAnnots=null;this.plaintextMetadata=null;this.print=null;this.printHighQuality=null}}class PickTrayByPDFSize extends Option01{constructor(e){super(Ql,"pickTrayByPDFSize")}}class config_Picture extends StringObject{constructor(e){super(Ql,"picture")}}class PlaintextMetadata extends Option01{constructor(e){super(Ql,"plaintextMetadata")}}class Presence extends OptionObject{constructor(e){super(Ql,"presence",["preserve","dissolve","dissolveStructure","ignore","remove"])}}class Present extends XFAObject{constructor(e){super(Ql,"present",!0);this.behaviorOverride=null;this.cache=null;this.common=null;this.copies=null;this.destination=null;this.incrementalMerge=null;this.layout=null;this.output=null;this.overprint=null;this.pagination=null;this.paginationOverride=null;this.script=null;this.validate=null;this.xdp=null;this.driver=new XFAObjectArray;this.labelPrinter=new XFAObjectArray;this.pcl=new XFAObjectArray;this.pdf=new XFAObjectArray;this.ps=new XFAObjectArray;this.submitUrl=new XFAObjectArray;this.webClient=new XFAObjectArray;this.zpl=new XFAObjectArray}}class Print extends Option01{constructor(e){super(Ql,"print")}}class PrintHighQuality extends Option01{constructor(e){super(Ql,"printHighQuality")}}class PrintScaling extends OptionObject{constructor(e){super(Ql,"printScaling",["appdefault","noScaling"])}}class PrinterName extends StringObject{constructor(e){super(Ql,"printerName")}}class Producer extends StringObject{constructor(e){super(Ql,"producer")}}class Ps extends XFAObject{constructor(e){super(Ql,"ps",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.staple=null;this.xdc=null}}class Range extends ContentObject{constructor(e){super(Ql,"range")}[lo](){this[so]=this[so].split(",",2).map(e=>e.split("-").map(e=>parseInt(e.trim(),10))).filter(e=>e.every(e=>!isNaN(e))).map(e=>{1===e.length&&e.push(e[0]);return e})}}class Record extends ContentObject{constructor(e){super(Ql,"record")}[lo](){this[so]=this[so].trim();const e=parseInt(this[so],10);!isNaN(e)&&e>=0&&(this[so]=e)}}class Relevant extends ContentObject{constructor(e){super(Ql,"relevant")}[lo](){this[so]=this[so].trim().split(/\s+/)}}class Rename extends ContentObject{constructor(e){super(Ql,"rename")}[lo](){this[so]=this[so].trim();(this[so].toLowerCase().startsWith("xml")||new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*","u").test(this[so]))&&warn("XFA - Rename: invalid XFA name")}}class RenderPolicy extends OptionObject{constructor(e){super(Ql,"renderPolicy",["server","client"])}}class RunScripts extends OptionObject{constructor(e){super(Ql,"runScripts",["both","client","none","server"])}}class config_Script extends XFAObject{constructor(e){super(Ql,"script",!0);this.currentPage=null;this.exclude=null;this.runScripts=null}}class ScriptModel extends OptionObject{constructor(e){super(Ql,"scriptModel",["XFA","none"])}}class Severity extends OptionObject{constructor(e){super(Ql,"severity",["ignore","error","information","trace","warning"])}}class SilentPrint extends XFAObject{constructor(e){super(Ql,"silentPrint",!0);this.addSilentPrint=null;this.printerName=null}}class Staple extends XFAObject{constructor(e){super(Ql,"staple");this.mode=getStringOption(e.mode,["usePrinterSetting","on","off"])}}class StartNode extends StringObject{constructor(e){super(Ql,"startNode")}}class StartPage extends IntegerObject{constructor(e){super(Ql,"startPage",0,e=>!0)}}class SubmitFormat extends OptionObject{constructor(e){super(Ql,"submitFormat",["html","delegate","fdf","xml","pdf"])}}class SubmitUrl extends StringObject{constructor(e){super(Ql,"submitUrl")}}class SubsetBelow extends IntegerObject{constructor(e){super(Ql,"subsetBelow",100,e=>e>=0&&e<=100)}}class SuppressBanner extends Option01{constructor(e){super(Ql,"suppressBanner")}}class Tagged extends Option01{constructor(e){super(Ql,"tagged")}}class config_Template extends XFAObject{constructor(e){super(Ql,"template",!0);this.base=null;this.relevant=null;this.startPage=null;this.uri=null;this.xsl=null}}class Threshold extends OptionObject{constructor(e){super(Ql,"threshold",["trace","error","information","warning"])}}class To extends OptionObject{constructor(e){super(Ql,"to",["null","memory","stderr","stdout","system","uri"])}}class TemplateCache extends XFAObject{constructor(e){super(Ql,"templateCache");this.maxEntries=getInteger({data:e.maxEntries,defaultValue:5,validate:e=>e>=0})}}class Trace extends XFAObject{constructor(e){super(Ql,"trace",!0);this.area=new XFAObjectArray}}class Transform extends XFAObject{constructor(e){super(Ql,"transform",!0);this.groupParent=null;this.ifEmpty=null;this.nameAttr=null;this.picture=null;this.presence=null;this.rename=null;this.whitespace=null}}class Type extends OptionObject{constructor(e){super(Ql,"type",["none","ascii85","asciiHex","ccittfax","flate","lzw","runLength","native","xdp","mergedXDP"])}}class Uri extends StringObject{constructor(e){super(Ql,"uri")}}class config_Validate extends OptionObject{constructor(e){super(Ql,"validate",["preSubmit","prePrint","preExecute","preSave"])}}class ValidateApprovalSignatures extends ContentObject{constructor(e){super(Ql,"validateApprovalSignatures")}[lo](){this[so]=this[so].trim().split(/\s+/).filter(e=>["docReady","postSign"].includes(e))}}class ValidationMessaging extends OptionObject{constructor(e){super(Ql,"validationMessaging",["allMessagesIndividually","allMessagesTogether","firstMessageOnly","noMessages"])}}class Version extends OptionObject{constructor(e){super(Ql,"version",["1.7","1.6","1.5","1.4","1.3","1.2"])}}class VersionControl extends XFAObject{constructor(e){super(Ql,"VersionControl");this.outputBelow=getStringOption(e.outputBelow,["warn","error","update"]);this.sourceAbove=getStringOption(e.sourceAbove,["warn","error"]);this.sourceBelow=getStringOption(e.sourceBelow,["update","maintain"])}}class ViewerPreferences extends XFAObject{constructor(e){super(Ql,"viewerPreferences",!0);this.ADBE_JSConsole=null;this.ADBE_JSDebugger=null;this.addViewerPreferences=null;this.duplexOption=null;this.enforce=null;this.numberOfCopies=null;this.pageRange=null;this.pickTrayByPDFSize=null;this.printScaling=null}}class WebClient extends XFAObject{constructor(e){super(Ql,"webClient",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class Whitespace extends OptionObject{constructor(e){super(Ql,"whitespace",["preserve","ltrim","normalize","rtrim","trim"])}}class Window extends ContentObject{constructor(e){super(Ql,"window")}[lo](){const e=this[so].split(",",2).map(e=>parseInt(e.trim(),10));if(e.some(e=>isNaN(e)))this[so]=[0,0];else{1===e.length&&e.push(e[0]);this[so]=e}}}class Xdc extends XFAObject{constructor(e){super(Ql,"xdc",!0);this.uri=new XFAObjectArray;this.xsl=new XFAObjectArray}}class Xdp extends XFAObject{constructor(e){super(Ql,"xdp",!0);this.packets=null}}class Xsl extends XFAObject{constructor(e){super(Ql,"xsl",!0);this.debug=null;this.uri=null}}class Zpl extends XFAObject{constructor(e){super(Ql,"zpl",!0);this.name=e.name?e.name.trim():"";this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class ConfigNamespace{static[hl](e,t){if(Object.hasOwn(ConfigNamespace,e))return ConfigNamespace[e](t)}static acrobat(e){return new Acrobat(e)}static acrobat7(e){return new Acrobat7(e)}static ADBE_JSConsole(e){return new ADBE_JSConsole(e)}static ADBE_JSDebugger(e){return new ADBE_JSDebugger(e)}static addSilentPrint(e){return new AddSilentPrint(e)}static addViewerPreferences(e){return new AddViewerPreferences(e)}static adjustData(e){return new AdjustData(e)}static adobeExtensionLevel(e){return new AdobeExtensionLevel(e)}static agent(e){return new Agent(e)}static alwaysEmbed(e){return new AlwaysEmbed(e)}static amd(e){return new Amd(e)}static area(e){return new config_Area(e)}static attributes(e){return new Attributes(e)}static autoSave(e){return new AutoSave(e)}static base(e){return new Base(e)}static batchOutput(e){return new BatchOutput(e)}static behaviorOverride(e){return new BehaviorOverride(e)}static cache(e){return new Cache(e)}static change(e){return new Change(e)}static common(e){return new Common(e)}static compress(e){return new Compress(e)}static compressLogicalStructure(e){return new CompressLogicalStructure(e)}static compressObjectStream(e){return new CompressObjectStream(e)}static compression(e){return new Compression(e)}static config(e){return new Config(e)}static conformance(e){return new Conformance(e)}static contentCopy(e){return new ContentCopy(e)}static copies(e){return new Copies(e)}static creator(e){return new Creator(e)}static currentPage(e){return new CurrentPage(e)}static data(e){return new Data(e)}static debug(e){return new Debug(e)}static defaultTypeface(e){return new DefaultTypeface(e)}static destination(e){return new Destination(e)}static documentAssembly(e){return new DocumentAssembly(e)}static driver(e){return new Driver(e)}static duplexOption(e){return new DuplexOption(e)}static dynamicRender(e){return new DynamicRender(e)}static embed(e){return new Embed(e)}static encrypt(e){return new config_Encrypt(e)}static encryption(e){return new config_Encryption(e)}static encryptionLevel(e){return new EncryptionLevel(e)}static enforce(e){return new Enforce(e)}static equate(e){return new Equate(e)}static equateRange(e){return new EquateRange(e)}static exclude(e){return new Exclude(e)}static excludeNS(e){return new ExcludeNS(e)}static flipLabel(e){return new FlipLabel(e)}static fontInfo(e){return new config_FontInfo(e)}static formFieldFilling(e){return new FormFieldFilling(e)}static groupParent(e){return new GroupParent(e)}static ifEmpty(e){return new IfEmpty(e)}static includeXDPContent(e){return new IncludeXDPContent(e)}static incrementalLoad(e){return new IncrementalLoad(e)}static incrementalMerge(e){return new IncrementalMerge(e)}static interactive(e){return new Interactive(e)}static jog(e){return new Jog(e)}static labelPrinter(e){return new LabelPrinter(e)}static layout(e){return new Layout(e)}static level(e){return new Level(e)}static linearized(e){return new Linearized(e)}static locale(e){return new Locale(e)}static localeSet(e){return new LocaleSet(e)}static log(e){return new Log(e)}static map(e){return new MapElement(e)}static mediumInfo(e){return new MediumInfo(e)}static message(e){return new config_Message(e)}static messaging(e){return new Messaging(e)}static mode(e){return new Mode(e)}static modifyAnnots(e){return new ModifyAnnots(e)}static msgId(e){return new MsgId(e)}static nameAttr(e){return new NameAttr(e)}static neverEmbed(e){return new NeverEmbed(e)}static numberOfCopies(e){return new NumberOfCopies(e)}static openAction(e){return new OpenAction(e)}static output(e){return new Output(e)}static outputBin(e){return new OutputBin(e)}static outputXSL(e){return new OutputXSL(e)}static overprint(e){return new Overprint(e)}static packets(e){return new Packets(e)}static pageOffset(e){return new PageOffset(e)}static pageRange(e){return new PageRange(e)}static pagination(e){return new Pagination(e)}static paginationOverride(e){return new PaginationOverride(e)}static part(e){return new Part(e)}static pcl(e){return new Pcl(e)}static pdf(e){return new Pdf(e)}static pdfa(e){return new Pdfa(e)}static permissions(e){return new Permissions(e)}static pickTrayByPDFSize(e){return new PickTrayByPDFSize(e)}static picture(e){return new config_Picture(e)}static plaintextMetadata(e){return new PlaintextMetadata(e)}static presence(e){return new Presence(e)}static present(e){return new Present(e)}static print(e){return new Print(e)}static printHighQuality(e){return new PrintHighQuality(e)}static printScaling(e){return new PrintScaling(e)}static printerName(e){return new PrinterName(e)}static producer(e){return new Producer(e)}static ps(e){return new Ps(e)}static range(e){return new Range(e)}static record(e){return new Record(e)}static relevant(e){return new Relevant(e)}static rename(e){return new Rename(e)}static renderPolicy(e){return new RenderPolicy(e)}static runScripts(e){return new RunScripts(e)}static script(e){return new config_Script(e)}static scriptModel(e){return new ScriptModel(e)}static severity(e){return new Severity(e)}static silentPrint(e){return new SilentPrint(e)}static staple(e){return new Staple(e)}static startNode(e){return new StartNode(e)}static startPage(e){return new StartPage(e)}static submitFormat(e){return new SubmitFormat(e)}static submitUrl(e){return new SubmitUrl(e)}static subsetBelow(e){return new SubsetBelow(e)}static suppressBanner(e){return new SuppressBanner(e)}static tagged(e){return new Tagged(e)}static template(e){return new config_Template(e)}static templateCache(e){return new TemplateCache(e)}static threshold(e){return new Threshold(e)}static to(e){return new To(e)}static trace(e){return new Trace(e)}static transform(e){return new Transform(e)}static type(e){return new Type(e)}static uri(e){return new Uri(e)}static validate(e){return new config_Validate(e)}static validateApprovalSignatures(e){return new ValidateApprovalSignatures(e)}static validationMessaging(e){return new ValidationMessaging(e)}static version(e){return new Version(e)}static versionControl(e){return new VersionControl(e)}static viewerPreferences(e){return new ViewerPreferences(e)}static webClient(e){return new WebClient(e)}static whitespace(e){return new Whitespace(e)}static window(e){return new Window(e)}static xdc(e){return new Xdc(e)}static xdp(e){return new Xdp(e)}static xsl(e){return new Xsl(e)}static zpl(e){return new Zpl(e)}}const Zl=ul.connectionSet.id;class ConnectionSet extends XFAObject{constructor(e){super(Zl,"connectionSet",!0);this.wsdlConnection=new XFAObjectArray;this.xmlConnection=new XFAObjectArray;this.xsdConnection=new XFAObjectArray}}class EffectiveInputPolicy extends XFAObject{constructor(e){super(Zl,"effectiveInputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EffectiveOutputPolicy extends XFAObject{constructor(e){super(Zl,"effectiveOutputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Operation extends StringObject{constructor(e){super(Zl,"operation");this.id=e.id||"";this.input=e.input||"";this.name=e.name||"";this.output=e.output||"";this.use=e.use||"";this.usehref=e.usehref||""}}class RootElement extends StringObject{constructor(e){super(Zl,"rootElement");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAction extends StringObject{constructor(e){super(Zl,"soapAction");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAddress extends StringObject{constructor(e){super(Zl,"soapAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class connection_set_Uri extends StringObject{constructor(e){super(Zl,"uri");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlAddress extends StringObject{constructor(e){super(Zl,"wsdlAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlConnection extends XFAObject{constructor(e){super(Zl,"wsdlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.effectiveInputPolicy=null;this.effectiveOutputPolicy=null;this.operation=null;this.soapAction=null;this.soapAddress=null;this.wsdlAddress=null}}class XmlConnection extends XFAObject{constructor(e){super(Zl,"xmlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.uri=null}}class XsdConnection extends XFAObject{constructor(e){super(Zl,"xsdConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.rootElement=null;this.uri=null}}class ConnectionSetNamespace{static[hl](e,t){if(Object.hasOwn(ConnectionSetNamespace,e))return ConnectionSetNamespace[e](t)}static connectionSet(e){return new ConnectionSet(e)}static effectiveInputPolicy(e){return new EffectiveInputPolicy(e)}static effectiveOutputPolicy(e){return new EffectiveOutputPolicy(e)}static operation(e){return new Operation(e)}static rootElement(e){return new RootElement(e)}static soapAction(e){return new SoapAction(e)}static soapAddress(e){return new SoapAddress(e)}static uri(e){return new connection_set_Uri(e)}static wsdlAddress(e){return new WsdlAddress(e)}static wsdlConnection(e){return new WsdlConnection(e)}static xmlConnection(e){return new XmlConnection(e)}static xsdConnection(e){return new XsdConnection(e)}}const ef=ul.datasets.id;class datasets_Data extends XmlObject{constructor(e){super(ef,"data",e)}[Mo](){return!0}}class Datasets extends XFAObject{constructor(e){super(ef,"datasets",!0);this.data=null;this.Signature=null}[Wo](e){const t=e[Uo];("data"===t&&e[Lo]===ef||"Signature"===t&&e[Lo]===ul.signature.id)&&(this[t]=e);this[Ji](e)}}class DatasetsNamespace{static[hl](e,t){if(Object.hasOwn(DatasetsNamespace,e))return DatasetsNamespace[e](t)}static datasets(e){return new Datasets(e)}static data(e){return new datasets_Data(e)}}const tf=ul.localeSet.id;class CalendarSymbols extends XFAObject{constructor(e){super(tf,"calendarSymbols",!0);this.name="gregorian";this.dayNames=new XFAObjectArray(2);this.eraNames=null;this.meridiemNames=null;this.monthNames=new XFAObjectArray(2)}}class CurrencySymbol extends StringObject{constructor(e){super(tf,"currencySymbol");this.name=getStringOption(e.name,["symbol","isoname","decimal"])}}class CurrencySymbols extends XFAObject{constructor(e){super(tf,"currencySymbols",!0);this.currencySymbol=new XFAObjectArray(3)}}class DatePattern extends StringObject{constructor(e){super(tf,"datePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class DatePatterns extends XFAObject{constructor(e){super(tf,"datePatterns",!0);this.datePattern=new XFAObjectArray(4)}}class DateTimeSymbols extends ContentObject{constructor(e){super(tf,"dateTimeSymbols")}}class Day extends StringObject{constructor(e){super(tf,"day")}}class DayNames extends XFAObject{constructor(e){super(tf,"dayNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.day=new XFAObjectArray(7)}}class Era extends StringObject{constructor(e){super(tf,"era")}}class EraNames extends XFAObject{constructor(e){super(tf,"eraNames",!0);this.era=new XFAObjectArray(2)}}class locale_set_Locale extends XFAObject{constructor(e){super(tf,"locale",!0);this.desc=e.desc||"";this.name="isoname";this.calendarSymbols=null;this.currencySymbols=null;this.datePatterns=null;this.dateTimeSymbols=null;this.numberPatterns=null;this.numberSymbols=null;this.timePatterns=null;this.typeFaces=null}}class locale_set_LocaleSet extends XFAObject{constructor(e){super(tf,"localeSet",!0);this.locale=new XFAObjectArray}}class Meridiem extends StringObject{constructor(e){super(tf,"meridiem")}}class MeridiemNames extends XFAObject{constructor(e){super(tf,"meridiemNames",!0);this.meridiem=new XFAObjectArray(2)}}class Month extends StringObject{constructor(e){super(tf,"month")}}class MonthNames extends XFAObject{constructor(e){super(tf,"monthNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.month=new XFAObjectArray(12)}}class NumberPattern extends StringObject{constructor(e){super(tf,"numberPattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class NumberPatterns extends XFAObject{constructor(e){super(tf,"numberPatterns",!0);this.numberPattern=new XFAObjectArray(4)}}class NumberSymbol extends StringObject{constructor(e){super(tf,"numberSymbol");this.name=getStringOption(e.name,["decimal","grouping","percent","minus","zero"])}}class NumberSymbols extends XFAObject{constructor(e){super(tf,"numberSymbols",!0);this.numberSymbol=new XFAObjectArray(5)}}class TimePattern extends StringObject{constructor(e){super(tf,"timePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class TimePatterns extends XFAObject{constructor(e){super(tf,"timePatterns",!0);this.timePattern=new XFAObjectArray(4)}}class TypeFace extends XFAObject{constructor(e){super(tf,"typeFace",!0);this.name=""|e.name}}class TypeFaces extends XFAObject{constructor(e){super(tf,"typeFaces",!0);this.typeFace=new XFAObjectArray}}class LocaleSetNamespace{static[hl](e,t){if(Object.hasOwn(LocaleSetNamespace,e))return LocaleSetNamespace[e](t)}static calendarSymbols(e){return new CalendarSymbols(e)}static currencySymbol(e){return new CurrencySymbol(e)}static currencySymbols(e){return new CurrencySymbols(e)}static datePattern(e){return new DatePattern(e)}static datePatterns(e){return new DatePatterns(e)}static dateTimeSymbols(e){return new DateTimeSymbols(e)}static day(e){return new Day(e)}static dayNames(e){return new DayNames(e)}static era(e){return new Era(e)}static eraNames(e){return new EraNames(e)}static locale(e){return new locale_set_Locale(e)}static localeSet(e){return new locale_set_LocaleSet(e)}static meridiem(e){return new Meridiem(e)}static meridiemNames(e){return new MeridiemNames(e)}static month(e){return new Month(e)}static monthNames(e){return new MonthNames(e)}static numberPattern(e){return new NumberPattern(e)}static numberPatterns(e){return new NumberPatterns(e)}static numberSymbol(e){return new NumberSymbol(e)}static numberSymbols(e){return new NumberSymbols(e)}static timePattern(e){return new TimePattern(e)}static timePatterns(e){return new TimePatterns(e)}static typeFace(e){return new TypeFace(e)}static typeFaces(e){return new TypeFaces(e)}}const af=ul.signature.id;class signature_Signature extends XFAObject{constructor(e){super(af,"signature",!0)}}class SignatureNamespace{static[hl](e,t){if(Object.hasOwn(SignatureNamespace,e))return SignatureNamespace[e](t)}static signature(e){return new signature_Signature(e)}}const nf=ul.stylesheet.id;class Stylesheet extends XFAObject{constructor(e){super(nf,"stylesheet",!0)}}class StylesheetNamespace{static[hl](e,t){if(Object.hasOwn(StylesheetNamespace,e))return StylesheetNamespace[e](t)}static stylesheet(e){return new Stylesheet(e)}}const sf=ul.xdp.id;class xdp_Xdp extends XFAObject{constructor(e){super(sf,"xdp",!0);this.uuid=e.uuid||"";this.timeStamp=e.timeStamp||"";this.config=null;this.connectionSet=null;this.datasets=null;this.localeSet=null;this.stylesheet=new XFAObjectArray;this.template=null}[Ko](e){const t=ul[e[Uo]];return t&&e[Lo]===t.id}}class XdpNamespace{static[hl](e,t){if(Object.hasOwn(XdpNamespace,e))return XdpNamespace[e](t)}static xdp(e){return new xdp_Xdp(e)}}const rf=ul.xhtml.id,of=Symbol(),lf=new Set(["color","font","font-family","font-size","font-stretch","font-style","font-weight","margin","margin-bottom","margin-left","margin-right","margin-top","letter-spacing","line-height","orphans","page-break-after","page-break-before","page-break-inside","tab-interval","tab-stop","text-align","text-decoration","text-indent","vertical-align","widows","kerning-mode","xfa-font-horizontal-scale","xfa-font-vertical-scale","xfa-spacerun","xfa-tab-stops"]),ff=new Map([["page-break-after","breakAfter"],["page-break-before","breakBefore"],["page-break-inside","breakInside"],["kerning-mode",e=>"none"===e?"none":"normal"],["xfa-font-horizontal-scale",e=>`scaleX(${Math.max(0,parseInt(e,10)/100).toFixed(2)})`],["xfa-font-vertical-scale",e=>`scaleY(${Math.max(0,parseInt(e,10)/100).toFixed(2)})`],["xfa-spacerun",""],["xfa-tab-stops",""],["font-size",(e,t)=>measureToString(.99*(e=t.fontSize=Math.abs(getMeasurement(e))))],["letter-spacing",e=>measureToString(getMeasurement(e))],["line-height",e=>measureToString(getMeasurement(e))],["margin",e=>measureToString(getMeasurement(e))],["margin-bottom",e=>measureToString(getMeasurement(e))],["margin-left",e=>measureToString(getMeasurement(e))],["margin-right",e=>measureToString(getMeasurement(e))],["margin-top",e=>measureToString(getMeasurement(e))],["text-indent",e=>measureToString(getMeasurement(e))],["font-family",e=>e],["vertical-align",e=>measureToString(getMeasurement(e))]]),cf=/\s+/g,hf=/[\r\n]+/g,uf=/\r\n?/g;function mapStyle(e,t,a){const n=Object.create(null);if(!e)return n;const s=Object.create(null);for(const[t,a]of e.split(";").map(e=>e.split(":",2))){const e=ff.get(t);if(""===e)continue;let r=a;e&&(r="string"==typeof e?e:e(a,s));t.endsWith("scale")?n.transform=n.transform?`${n[t]} ${r}`:r:n[t.replaceAll(/-([a-zA-Z])/g,(e,t)=>t.toUpperCase())]=r}n.fontFamily&&setFontFamily({typeface:n.fontFamily,weight:n.fontWeight||"normal",posture:n.fontStyle||"normal",size:s.fontSize||0},t,t[xo].fontFinder,n);if(a&&n.verticalAlign&&"0px"!==n.verticalAlign&&n.fontSize){const e=.583,t=.333,a=getMeasurement(n.fontSize);n.fontSize=measureToString(a*e);n.verticalAlign=measureToString(Math.sign(getMeasurement(n.verticalAlign))*a*t)}a&&n.fontSize&&(n.fontSize=`calc(${n.fontSize} * var(--total-scale-factor))`);fixTextIndent(n);return n}const mf=new Set(["body","html"]);class XhtmlObject extends XmlObject{constructor(e,t){super(rf,t);this[of]=!1;this.style=e.style||""}[Zi](e){super[Zi](e);this.style=function checkStyle(e){return e.style?e.style.split(";").filter(e=>!!e.trim()).map(e=>e.split(":",2).map(e=>e.trim())).filter(([t,a])=>{"font-family"===t&&e[xo].usedTypefaces.add(a);return lf.has(t)}).map(e=>e.join(":")).join(";"):""}(this)}[$i](){return!mf.has(this[Uo])}[Go](e,t=!1){if(t)this[of]=!0;else{e=e.replaceAll(hf,"");this.style.includes("xfa-spacerun:yes")||(e=e.replaceAll(cf," "))}e&&(this[so]+=e)}[Vo](e,t=!0){const a=Object.create(null),n={top:NaN,bottom:NaN,left:NaN,right:NaN};let s=null;for(const[e,t]of this.style.split(";").map(e=>e.split(":",2)))switch(e){case"font-family":a.typeface=stripQuotes(t);break;case"font-size":a.size=getMeasurement(t);break;case"font-weight":a.weight=t;break;case"font-style":a.posture=t;break;case"letter-spacing":a.letterSpacing=getMeasurement(t);break;case"margin":const e=t.split(/ \t/).map(e=>getMeasurement(e));switch(e.length){case 1:n.top=n.bottom=n.left=n.right=e[0];break;case 2:n.top=n.bottom=e[0];n.left=n.right=e[1];break;case 3:n.top=e[0];n.bottom=e[2];n.left=n.right=e[1];break;case 4:n.top=e[0];n.left=e[1];n.bottom=e[2];n.right=e[3]}break;case"margin-top":n.top=getMeasurement(t);break;case"margin-bottom":n.bottom=getMeasurement(t);break;case"margin-left":n.left=getMeasurement(t);break;case"margin-right":n.right=getMeasurement(t);break;case"line-height":s=getMeasurement(t)}e.pushData(a,n,s);if(this[so])e.addString(this[so]);else for(const t of this[ko]())"#text"!==t[Uo]?t[Vo](e):e.addString(t[so]);t&&e.popFont()}[ol](e){const t=[];this[oo]={children:t};this[Qi]({});if(0===t.length&&!this[so])return HTMLResult.EMPTY;let a;a=this[of]?this[so]?this[so].replaceAll(uf,"\n"):void 0:this[so]||void 0;return HTMLResult.success({name:this[Uo],attributes:{href:this.href,style:mapStyle(this.style,this,this[of])},children:t,value:a})}}class A extends XhtmlObject{constructor(e){super(e,"a");this.href=fixURL(e.href)||""}}class B extends XhtmlObject{constructor(e){super(e,"b")}[Vo](e){e.pushFont({weight:"bold"});super[Vo](e);e.popFont()}}class Body extends XhtmlObject{constructor(e){super(e,"body")}[ol](e){const t=super[ol](e),{html:a}=t;if(!a)return HTMLResult.EMPTY;a.name="div";a.attributes.class=["xfaRich"];return t}}class Br extends XhtmlObject{constructor(e){super(e,"br")}[rl](){return"\n"}[Vo](e){e.addString("\n")}[ol](e){return HTMLResult.success({name:"br"})}}class Html extends XhtmlObject{constructor(e){super(e,"html")}[ol](e){const t=[];this[oo]={children:t};this[Qi]({});if(0===t.length)return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},value:this[so]||""});if(1===t.length){const e=t[0];if(e.attributes?.class.includes("xfaRich"))return HTMLResult.success(e)}return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},children:t})}}class I extends XhtmlObject{constructor(e){super(e,"i")}[Vo](e){e.pushFont({posture:"italic"});super[Vo](e);e.popFont()}}class Li extends XhtmlObject{constructor(e){super(e,"li")}}class Ol extends XhtmlObject{constructor(e){super(e,"ol")}}class P extends XhtmlObject{constructor(e){super(e,"p")}[Vo](e){super[Vo](e,!1);e.addString("\n");e.addPara();e.popFont()}[rl](){return this[So]()[ko]().at(-1)===this?super[rl]():super[rl]()+"\n"}}class Span extends XhtmlObject{constructor(e){super(e,"span")}}class Sub extends XhtmlObject{constructor(e){super(e,"sub")}}class Sup extends XhtmlObject{constructor(e){super(e,"sup")}}class Ul extends XhtmlObject{constructor(e){super(e,"ul")}}class XhtmlNamespace{static[hl](e,t){if(Object.hasOwn(XhtmlNamespace,e))return XhtmlNamespace[e](t)}static a(e){return new A(e)}static b(e){return new B(e)}static body(e){return new Body(e)}static br(e){return new Br(e)}static html(e){return new Html(e)}static i(e){return new I(e)}static li(e){return new Li(e)}static ol(e){return new Ol(e)}static p(e){return new P(e)}static span(e){return new Span(e)}static sub(e){return new Sub(e)}static sup(e){return new Sup(e)}static ul(e){return new Ul(e)}}const pf={config:ConfigNamespace,connection:ConnectionSetNamespace,datasets:DatasetsNamespace,localeSet:LocaleSetNamespace,signature:SignatureNamespace,stylesheet:StylesheetNamespace,template:TemplateNamespace,xdp:XdpNamespace,xhtml:XhtmlNamespace};class UnknownNamespace{constructor(e){this.namespaceId=e}[hl](e,t){return new XmlObject(this.namespaceId,e,t)}}class Root extends XFAObject{constructor(e){super(-1,"root",Object.create(null));this.element=null;this[Io]=e}[Wo](e){this.element=e;return!0}[lo](){super[lo]();if(this.element.template instanceof Template){this[Io].set(Qo,this.element);this.element.template[Zo](this[Io]);this.element.template[Io]=this[Io]}}}class Empty extends XFAObject{constructor(){super(-1,"",Object.create(null))}[Wo](e){return!1}}class Builder{constructor(e=null){this._namespaceStack=[];this._nsAgnosticLevel=0;this._namespacePrefixes=new Map;this._namespaces=new Map;this._nextNsId=Math.max(...Object.values(ul).map(({id:e})=>e));this._currentNamespace=e||new UnknownNamespace(++this._nextNsId)}buildRoot(e){return new Root(e)}build({nsPrefix:e,name:t,attributes:a,namespace:n,prefixes:s}){const r=null!==n;if(r){this._namespaceStack.push(this._currentNamespace);this._currentNamespace=this._searchNamespace(n)}s&&this._addNamespacePrefix(s);if(Object.hasOwn(a,Xo)){const e=pf.datasets,t=a[Xo];let n=null;for(const[a,s]of Object.entries(t)){if(this._getNamespaceToUse(a)===e){n={xfa:s};break}}n?a[Xo]=n:delete a[Xo]}const i=this._getNamespaceToUse(e),o=i?.[hl](t,a)||new Empty;o[Mo]()&&this._nsAgnosticLevel++;(r||s||o[Mo]())&&(o[to]={hasNamespace:r,prefixes:s,nsAgnostic:o[Mo]()});return o}isNsAgnostic(){return this._nsAgnosticLevel>0}_searchNamespace(e){let t=this._namespaces.get(e);if(t)return t;for(const[a,{check:n}]of Object.entries(ul))if(n(e)){t=pf[a];if(t){this._namespaces.set(e,t);return t}break}t=new UnknownNamespace(++this._nextNsId);this._namespaces.set(e,t);return t}_addNamespacePrefix(e){for(const{prefix:t,value:a}of e){const e=this._searchNamespace(a);this._namespacePrefixes.getOrInsertComputed(t,makeArr).push(e)}}_getNamespaceToUse(e){if(!e)return this._currentNamespace;const t=this._namespacePrefixes.get(e);if(t?.length>0)return t.at(-1);warn(`Unknown namespace prefix: ${e}.`);return null}clean(e){const{hasNamespace:t,prefixes:a,nsAgnostic:n}=e;t&&(this._currentNamespace=this._namespaceStack.pop());a&&a.forEach(({prefix:e})=>{this._namespacePrefixes.get(e).pop()});n&&this._nsAgnosticLevel--}}class XFAParser extends XMLParserBase{constructor(e=null,t=!1){super();this._builder=new Builder(e);this._stack=[];this._globalData={usedTypefaces:new Set};this._ids=new Map;this._current=this._builder.buildRoot(this._ids);this._errorCode=Bi;this._whiteRegex=/^\s+$/;this._nbsps=/\xa0+/g;this._richText=t}parse(e){this.parseXml(e);if(this._errorCode===Bi){this._current[lo]();return this._current.element}}onText(e){e=e.replace(this._nbsps,e=>e.slice(1)+" ");this._richText||this._current[$i]()?this._current[Go](e,this._richText):this._whiteRegex.test(e)||this._current[Go](e.trim())}onCdata(e){this._current[Go](e)}_mkAttributes(e,t){let a=null,n=null;const s=Object.create({});for(const{name:r,value:i}of e)if("xmlns"===r)a?warn(`XFA - multiple namespace definition in <${t}>`):a=i;else if(r.startsWith("xmlns:")){const e=r.substring(6);n??=[];n.push({prefix:e,value:i})}else{const e=r.indexOf(":");if(-1===e)s[r]=i;else{const t=s[Xo]??=Object.create(null),[a,n]=[r.slice(0,e),r.slice(e+1)];(t[a]||=Object.create(null))[n]=i}}return[a,n,s]}_getNameAndPrefix(e,t){const a=e.indexOf(":");return-1===a?[e,null]:[e.substring(a+1),t?"":e.substring(0,a)]}onBeginElement(e,t,a){const[n,s,r]=this._mkAttributes(t,e),[i,o]=this._getNameAndPrefix(e,this._builder.isNsAgnostic()),l=this._builder.build({nsPrefix:o,name:i,attributes:r,namespace:n,prefixes:s});l[xo]=this._globalData;if(a){l[lo]();this._current[Wo](l)&&l[tl](this._ids);l[Zi](this._builder)}else{this._stack.push(this._current);this._current=l}}onEndElement(e){const t=this._current;if(t[Ro]()&&"string"==typeof t[so]){const e=new XFAParser;e._globalData=this._globalData;const a=e.parse(t[so]);t[so]=null;t[Wo](a)}t[lo]();this._current=this._stack.pop();this._current[Wo](t)&&t[tl](this._ids);t[Zi](this._builder)}onError(e){this._errorCode=e}}class XFAFactory{constructor(e){try{this.root=(new XFAParser).parse(XFAFactory._createDocument(e));const t=new Binder(this.root);this.form=t.bind();this.dataHandler=new DataHandler(this.root,t.getData());this.form[xo].template=this.form}catch(e){warn(`XFA - an error occurred during parsing and binding: ${e}`)}}isValid(){return!(!this.root||!this.form)}_createPagesHelper(){const e=this.form[il]();return new Promise((t,a)=>{const nextIteration=()=>{try{const a=e.next();a.done?t(a.value):setTimeout(nextIteration,0)}catch(e){a(e)}};setTimeout(nextIteration,0)})}async _createPages(){try{this.pages=await this._createPagesHelper();this.dims=this.pages.children.map(e=>{const{width:t,height:a}=e.attributes.style;return[0,0,parseInt(t,10),parseInt(a,10)]})}catch(e){warn(`XFA - an error occurred during layout: ${e}`)}}getBoundingBox(e){return this.dims[e]}async getNumPages(){this.pages||await this._createPages();return this.dims.length}setImages(e){this.form[xo].images=e}setFonts(e){this.form[xo].fontFinder=new FontFinder(e);const t=[];for(let e of this.form[xo].usedTypefaces){e=stripQuotes(e);this.form[xo].fontFinder.find(e)||t.push(e)}return t.length>0?t:null}appendFonts(e,t){this.form[xo].fontFinder.add(e,t)}async getPages(){this.pages||await this._createPages();const e=this.pages;this.pages=null;return e}serializeData(e){return this.dataHandler.serialize(e)}static _createDocument(e){return e["/xdp:xdp"]?Object.values(e).join(""):e["xdp:xdp"]}static getRichTextAsHtml(e){if(!e||"string"!=typeof e)return null;try{let t=new XFAParser(XhtmlNamespace,!0).parse(e);if(!["body","xhtml"].includes(t[Uo])){const e=XhtmlNamespace.body({});e[Ji](t);t=e}const a=t[ol]();if(!a.success)return null;const{html:n}=a,{attributes:s}=n;if(s){s.class&&(s.class=s.class.filter(e=>!e.startsWith("xfa")));s.dir="auto"}return{html:n,str:t[rl]()}}catch(e){warn(`XFA - an error occurred during parsing of rich text: ${e}`)}return null}}class AnnotationFactory{static createGlobals(e){return Promise.all([e.ensureCatalog("acroForm"),e.ensureDoc("xfaDatasets"),e.ensureCatalog("structTreeRoot"),e.ensureCatalog("baseUrl"),e.ensureCatalog("attachments"),e.ensureCatalog("globalColorSpaceCache")]).then(([t,a,n,s,r,i])=>({pdfManager:e,acroForm:t instanceof Dict?t:Dict.empty,xfaDatasets:a,structTreeRoot:n,baseUrl:s,attachments:r,globalColorSpaceCache:i}),e=>{warn(`createGlobals: "${e}".`);return null})}static async create(e,t,a,n,s,r,i,o){const l=s?await this._getPageIndex(e,t,a.pdfManager):null;return a.pdfManager.ensure(this,"_create",[e,t,a,n,s,r,i,l,o])}static _create(e,t,a,n,s=!1,r=null,i=null,o=null,l=null){const f=e.fetchIfRef(t);if(!(f instanceof Dict))return;let c=f.get("Subtype");c=c instanceof Name?c.name:null;if(i&&!i.has(R[c?.toUpperCase()]))return null;const{acroForm:h,pdfManager:u}=a,m=t instanceof Ref?t.toString():`annot_${n.createObjId()}`,p={xref:e,ref:t,dict:f,subtype:c,id:m,annotationGlobals:a,collectFields:s,orphanFields:r,needAppearances:!s&&!0===h.get("NeedAppearances"),pageIndex:o,evaluatorOptions:u.evaluatorOptions,pageRef:l};switch(c){case"Link":return new LinkAnnotation(p);case"Text":return new TextAnnotation(p);case"Widget":let e=getInheritableProperty({dict:f,key:"FT"});e=e instanceof Name?e.name:null;switch(e){case"Tx":return new TextWidgetAnnotation(p);case"Btn":return new ButtonWidgetAnnotation(p);case"Ch":return new ChoiceWidgetAnnotation(p);case"Sig":return new SignatureWidgetAnnotation(p)}warn(`Unimplemented widget field type "${e}", falling back to base field type.`);return new WidgetAnnotation(p);case"Popup":return new PopupAnnotation(p);case"FreeText":return new FreeTextAnnotation(p);case"Line":return new LineAnnotation(p);case"Square":return new SquareAnnotation(p);case"Circle":return new CircleAnnotation(p);case"PolyLine":return new PolylineAnnotation(p);case"Polygon":return new PolygonAnnotation(p);case"Caret":return new CaretAnnotation(p);case"Ink":return new InkAnnotation(p);case"Highlight":return new HighlightAnnotation(p);case"Underline":return new UnderlineAnnotation(p);case"Squiggly":return new SquigglyAnnotation(p);case"StrikeOut":return new StrikeOutAnnotation(p);case"Stamp":return new StampAnnotation(p);case"FileAttachment":return new FileAttachmentAnnotation(p);default:s||warn(c?`Unimplemented annotation type "${c}", falling back to base annotation.`:"Annotation is missing the required /Subtype.");return new Annotation(p)}}static async _getPageIndex(e,t,a){try{const n=await e.fetchIfRefAsync(t);if(!(n instanceof Dict))return-1;const s=n.getRaw("P");if(s instanceof Ref)try{return await a.ensureCatalog("getPageIndex",[s])}catch(e){info(`_getPageIndex -- not a valid page reference: "${e}".`)}if(n.has("Kids"))return-1;const r=await a.ensureDoc("numPages");for(let e=0;ee/255)||t}function getQuadPoints(e,t){const a=e.getArray("QuadPoints");if(!isNumberArray(a,null)||0===a.length||a.length%8>0)return null;const n=new Float32Array(a.length);for(let e=0,s=a.length;et[2]||pt[3]))return null;n.set([u,d,m,d,u,p,m,p],e)}return n}function getTransformMatrix(e,t,n){const s=a.slice();Util.axialAlignedBoundingBox(t,n,s);const[r,i,o,l]=s;if(r===o||i===l)return[1,0,0,1,e[0],e[1]];const f=(e[2]-e[0])/(o-r),c=(e[3]-e[1])/(l-i);return[f,0,0,c,e[0]-r*f,e[1]-i*c]}class Annotation{constructor(e){const{annotationGlobals:t,dict:a,orphanFields:n,ref:s,subtype:r,xref:i}=e,o=n?.get(s);o&&a.set("Parent",o);this.setTitle(a.get("T"));this.setContents(a.get("Contents"));this.setModificationDate(a.get("M"));this.setFlags(a.get("F"));this.setRectangle(a.getArray("Rect"));this.setColor(a.getArray("C"));this.setBorderStyle(a);this.setAppearance(a);this.setOptionalContent(a);const l=a.get("MK");this.setBorderAndBackgroundColors(l);this.setRotation(l,a);this.ref=e.ref instanceof Ref?e.ref:null;this._streams=[];this.appearance&&this._streams.push(this.appearance);const f=!!(this.flags&L),c=!!(this.flags&U);this.data={annotationType:R[r?.toUpperCase()],annotationFlags:this.flags,borderStyle:this.borderStyle,color:this.color,backgroundColor:this.backgroundColor,borderColor:this.borderColor,rotation:this.rotation,contentsObj:this._contents,hasAppearance:!!this.appearance,id:e.id,modificationDate:this.modificationDate,rect:this.rectangle,subtype:r,hasOwnCanvas:!1,noRotate:!!(this.flags&_),noHTML:f&&c,isEditable:!1,structParent:-1};if(t.structTreeRoot){let n=a.get("StructParent");this.data.structParent=n=Number.isInteger(n)&&n>=0?n:-1;t.structTreeRoot.addAnnotationIdToPage(e.pageRef,n)}if(e.collectFields){const t=a.get("Kids");if(Array.isArray(t)){const e=[];for(const a of t)a instanceof Ref&&e.push(a.toString());0!==e.length&&(this.data.kidIds=e)}this.data.actions=collectActions(i,a,ie);this.data.fieldName=this._constructFieldName(a);this.data.pageIndex=e.pageIndex}const h=a.get("IT");h instanceof Name&&(this.data.it=h.name);this._isOffscreenCanvasSupported=e.evaluatorOptions.isOffscreenCanvasSupported;this._fallbackFontDict=null;this._needAppearances=!1}_hasFlag(e,t){return!!(e&t)}_buildFlags(e,t){let{flags:a}=this;if(void 0===e){if(void 0===t)return;return t?a&~E:a&~N|E}if(e){a|=E;return t?a&~z|N:a&~N|z}a&=~(N|z);return t?a&~E:a|E}_isViewable(e){return!this._hasFlag(e,M)&&!this._hasFlag(e,z)}_isPrintable(e){return this._hasFlag(e,E)&&!this._hasFlag(e,N)&&!this._hasFlag(e,M)}mustBeViewed(e,t){const a=e?.get(this.data.id)?.noView;return void 0!==a?!a:this.viewable&&!this._hasFlag(this.flags,N)}mustBePrinted(e){const t=e?.get(this.data.id)?.noPrint;return void 0!==t?!t:this.printable}mustBeViewedWhenEditing(e,t=null){return e?!this.data.isEditable:!t?.has(this.data.id)}get viewable(){return null!==this.data.quadPoints&&(0===this.flags||this._isViewable(this.flags))}get printable(){return null!==this.data.quadPoints&&(0!==this.flags&&this._isPrintable(this.flags))}_parseStringHelper(e){const t="string"==typeof e?stringToPDFString(e):"";return{str:t,dir:t&&"rtl"===bidi(t).dir?"rtl":"ltr"}}setDefaultAppearance(e){const{dict:t,annotationGlobals:a}=e,n=getInheritableProperty({dict:t,key:"DA"})||a.acroForm.get("DA");this._defaultAppearance="string"==typeof n?n:"";this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance)}setTitle(e){this._title=this._parseStringHelper(e)}setContents(e){this._contents=this._parseStringHelper(e)}setModificationDate(e){this.modificationDate="string"==typeof e?e:null}setFlags(e){this.flags=Number.isInteger(e)&&e>0?e:0;this.flags&M&&"Annotation"!==this.constructor.name&&(this.flags^=M)}hasFlag(e){return this._hasFlag(this.flags,e)}setRectangle(e){this.rectangle=lookupNormalRect(e,[0,0,0,0])}setColor(e){this.color=getRgbColor(e)}setLineEndings(e){this.lineEndings=["None","None"];if(Array.isArray(e)&&2===e.length)for(let t=0;t<2;t++){const a=e[t];if(a instanceof Name)switch(a.name){case"None":continue;case"Square":case"Circle":case"Diamond":case"OpenArrow":case"ClosedArrow":case"Butt":case"ROpenArrow":case"RClosedArrow":case"Slash":this.lineEndings[t]=a.name;continue}warn(`Ignoring invalid lineEnding: ${a}`)}}setRotation(e,t){this.rotation=0;let a=e instanceof Dict?e.get("R")||0:t.get("Rotate")||0;if(Number.isInteger(a)&&0!==a){a%=360;a<0&&(a+=360);a%90==0&&(this.rotation=a)}}setBorderAndBackgroundColors(e){if(e instanceof Dict){this.borderColor=getRgbColor(e.getArray("BC"),null);this.backgroundColor=getRgbColor(e.getArray("BG"),null)}else this.borderColor=this.backgroundColor=null}setBorderStyle(e){this.borderStyle=new AnnotationBorderStyle;if(e instanceof Dict)if(e.has("BS")){const t=e.get("BS");if(t instanceof Dict){const e=t.get("Type");if(!e||isName(e,"Border")){this.borderStyle.setWidth(t.get("W"),this.rectangle);this.borderStyle.setStyle(t.get("S"));this.borderStyle.setDashArray(t.getArray("D"))}}}else if(e.has("Border")){const t=e.getArray("Border");if(Array.isArray(t))if(t.length>=3){this.borderStyle.setHorizontalCornerRadius(t[0]);this.borderStyle.setVerticalCornerRadius(t[1]);this.borderStyle.setWidth(t[2],this.rectangle);4===t.length&&this.borderStyle.setDashArray(t[3],!0)}else 0===t.length&&this.borderStyle.setWidth(0)}else this.borderStyle.setWidth(0)}setAppearance(e){this.appearance=null;const t=e.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(a instanceof BaseStream){this.appearance=a;return}if(!(a instanceof Dict))return;const n=e.get("AS");if(!(n instanceof Name&&a.has(n.name)))return;const s=a.get(n.name);s instanceof BaseStream&&(this.appearance=s)}setOptionalContent(e){this.oc=null;const t=e.get("OC");t instanceof Name?warn("setOptionalContent: Support for /Name-entry is not implemented."):t instanceof Dict&&(this.oc=t)}async loadResources(e,t){const a=await t.dict.getAsync("Resources");a&&await ObjectLoader.load(a,e,a.xref);return a}async getOperatorList(e,t,a,n){const{hasOwnCanvas:s,id:r,rect:i}=this.data;let o=this.appearance;const f=!!(s&&a&l);if(f&&(0===this.width||0===this.height)){this.data.hasOwnCanvas=!1;return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}if(!o){if(!f)return{opList:new OperatorList,separateForm:!1,separateCanvas:!1};o=new StringStream("");o.dict=new Dict}const c=o.dict,h=await this.loadResources(da,o),u=lookupRect(c.getArray("BBox"),[0,0,1,1]),m=lookupMatrix(c.getArray("Matrix"),pa),p=getTransformMatrix(i,u,m),d=new OperatorList;let g;this.oc&&(g=await e.parseMarkedContentProps(this.oc,null));void 0!==g&&d.addOp(xt,["OC",g]);d.addOp(Bt,[r,i,p,m,f]);await e.getOperatorList({stream:o,task:t,resources:h,operatorList:d,fallbackFontDict:this._fallbackFontDict});d.addOp(Dt,[]);void 0!==g&&d.addOp(Ct,[]);this.reset();return{opList:d,separateForm:!1,separateCanvas:f}}async save(e,t,a,n){return null}get overlaysTextContent(){return!1}get hasTextContent(){return!1}async extractTextContent(e,t,a){if(!this.appearance)return;const n=await this.loadResources(ga,this.appearance),s=[],r=[];let i=null;const o={desiredSize:Math.Infinity,ready:!0,enqueue(e,t){for(const t of e.items)if(void 0!==t.str){i||=t.transform.slice(-2);r.push(t.str);if(t.hasEOL){s.push(r.join("").trimEnd());r.length=0}}}};await e.getTextContent({stream:this.appearance,task:t,resources:n,includeMarkedContent:!0,keepWhiteSpace:!0,sink:o,viewBox:a});this.reset();r.length&&s.push(r.join("").trimEnd());if(s.length>1||s[0]){const e=this.appearance.dict,t=lookupRect(e.getArray("BBox"),null),a=lookupMatrix(e.getArray("Matrix"),null);this.data.textPosition=this._transformPoint(i,t,a);this.data.textContent=s}}_transformPoint(e,t,a){const{rect:n}=this.data;t||=[0,0,1,1];a||=[1,0,0,1,0,0];const s=getTransformMatrix(n,t,a);s[4]-=n[0];s[5]-=n[1];const r=e.slice();Util.applyTransform(r,s);Util.applyTransform(r,a);return r}getFieldObject(){return this.data.kidIds?{id:this.data.id,actions:this.data.actions,name:this.data.fieldName,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,type:"",kidIds:this.data.kidIds,page:this.data.pageIndex,rotation:this.rotation}:null}reset(){for(const e of this._streams)e.reset()}_constructFieldName(e){if(!e.has("T")&&!e.has("Parent")){warn("Unknown field name, falling back to empty field name.");return""}if(!e.has("Parent"))return stringToPDFString(e.get("T"));const t=[];e.has("T")&&t.unshift(stringToPDFString(e.get("T")));let a=e;const n=new RefSet;e.objId&&n.put(e.objId);for(;a.has("Parent");){a=a.get("Parent");if(!(a instanceof Dict)||a.objId&&n.has(a.objId))break;a.objId&&n.put(a.objId);a.has("T")&&t.unshift(stringToPDFString(a.get("T")))}return t.join(".")}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class AnnotationBorderStyle{width=1;rawWidth=1;style=te;dashArray=[3];horizontalCornerRadius=0;verticalCornerRadius=0;setWidth(e,t=[0,0,0,0]){if(e instanceof Name)this.width=0;else if("number"==typeof e){if(e>0){this.rawWidth=e;const a=(t[2]-t[0])/2,n=(t[3]-t[1])/2;if(a>0&&n>0&&(e>a||e>n)){warn(`AnnotationBorderStyle.setWidth - ignoring width: ${e}`);e=1}}this.width=e}}setStyle(e){if(e instanceof Name)switch(e.name){case"S":this.style=te;break;case"D":this.style=ae;break;case"B":this.style=ne;break;case"I":this.style=se;break;case"U":this.style=re}}setDashArray(e,t=!1){if(Array.isArray(e)){let a=!0,n=!0;for(const t of e){if(!(+t>=0)){a=!1;break}t>0&&(n=!1)}if(0===e.length||a&&!n){this.dashArray=e;t&&this.setStyle(Name.get("D"))}else this.width=0}else e&&(this.width=0)}setHorizontalCornerRadius(e){Number.isInteger(e)&&(this.horizontalCornerRadius=e)}setVerticalCornerRadius(e){Number.isInteger(e)&&(this.verticalCornerRadius=e)}}class MarkupAnnotation extends Annotation{constructor(e){super(e);const{dict:t}=e;if(t.has("IRT")){const e=t.getRaw("IRT");this.data.inReplyTo=e instanceof Ref?e.toString():null;const a=t.get("RT");this.data.replyType=a instanceof Name?a.name:D}let a=null;if(this.data.replyType===H){const e=t.get("IRT");this.setTitle(e.get("T"));this.data.titleObj=this._title;this.setContents(e.get("Contents"));this.data.contentsObj=this._contents;if(e.has("CreationDate")){this.setCreationDate(e.get("CreationDate"));this.data.creationDate=this.creationDate}else this.data.creationDate=null;if(e.has("M")){this.setModificationDate(e.get("M"));this.data.modificationDate=this.modificationDate}else this.data.modificationDate=null;a=e.getRaw("Popup");if(e.has("C")){this.setColor(e.getArray("C"));this.data.color=this.color}else this.data.color=null}else{this.data.titleObj=this._title;this.setCreationDate(t.get("CreationDate"));this.data.creationDate=this.creationDate;a=t.getRaw("Popup");t.has("C")||(this.data.color=null)}this.data.popupRef=a instanceof Ref?a.toString():null;t.has("RC")&&(this.data.richText=XFAFactory.getRichTextAsHtml(t.get("RC")))}setCreationDate(e){this.creationDate="string"==typeof e?e:null}_setDefaultAppearance({xref:e,extra:a,strokeColor:n,fillColor:s,blendMode:r,strokeAlpha:i,fillAlpha:o,pointsCallback:l}){const f=this.data.rect=t.slice(),c=["q"];a&&c.push(a);n&&c.push(`${n[0]} ${n[1]} ${n[2]} RG`);s&&c.push(`${s[0]} ${s[1]} ${s[2]} rg`);const h=this.data.quadPoints||Float32Array.from([this.rectangle[0],this.rectangle[3],this.rectangle[2],this.rectangle[3],this.rectangle[0],this.rectangle[1],this.rectangle[2],this.rectangle[1]]);for(let e=0,t=h.length;ethis._decodeFormValue(e)).filter(e=>null!==e);return t.length>0?t:null}return e instanceof Name?e.name:"string"==typeof e?stringToPDFString(e):null}hasFieldFlag(e){return!!(this.data.fieldFlags&e)}_isViewable(e){return!0}mustBeViewed(e,t){return t?this.viewable:super.mustBeViewed(e,t)&&!this._hasFlag(this.flags,z)}getRotationMatrix(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);return 0===t?pa:getRotationMatrix(t,this.width,this.height)}getBorderAndBackgroundAppearances(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(!this.backgroundColor&&!this.borderColor)return"";const a=0===t||180===t?`0 0 ${this.width} ${this.height} re`:`0 0 ${this.height} ${this.width} re`;let n="";this.backgroundColor&&(n=`${getPdfColor(this.backgroundColor,!0)} ${a} f `);if(this.borderColor){n+=`${this.borderStyle.width||1} w ${getPdfColor(this.borderColor,!1)} ${a} S `}return n}async getOperatorList(e,t,a,n){if(a&h&&!(this instanceof SignatureWidgetAnnotation)&&!this.data.noHTML&&!this.data.hasOwnCanvas)return{opList:new OperatorList,separateForm:!0,separateCanvas:!1};if(!this._hasText)return super.getOperatorList(e,t,a,n);const s=await this._getAppearance(e,t,a,n);if(this.appearance&&null===s)return super.getOperatorList(e,t,a,n);const r=new OperatorList;if(!this._defaultAppearance||null===s)return{opList:r,separateForm:!1,separateCanvas:!1};const i=!!(this.data.hasOwnCanvas&&a&l),o=[0,0,this.width,this.height],f=getTransformMatrix(this.data.rect,o,[1,0,0,1,0,0]);let c;this.oc&&(c=await e.parseMarkedContentProps(this.oc,null));void 0!==c&&r.addOp(xt,["OC",c]);r.addOp(Bt,[this.data.id,this.data.rect,f,this.getRotationMatrix(n),i]);const u=new StringStream(s);await e.getOperatorList({stream:u,task:t,resources:this._fieldResources.mergedResources,operatorList:r});r.addOp(Dt,[]);void 0!==c&&r.addOp(Ct,[]);return{opList:r,separateForm:!1,separateCanvas:i}}_getMKDict(e){const t=new Dict(null);e&&t.set("R",e);t.setIfArray("BC",getPdfColorArray(this.borderColor));t.setIfArray("BG",getPdfColorArray(this.backgroundColor));return t.size>0?t:null}amendSavedDict(e,t){}setValue(e,t,a,n){const{dict:s,ref:r}=function getParentToUpdate(e,t,a){const n=new RefSet,s=e,r={dict:null,ref:null};for(;e instanceof Dict&&!n.has(t);){n.put(t);if(e.has("T"))break;if(!((t=e.getRaw("Parent"))instanceof Ref))return r;e=a.fetch(t)}if(e instanceof Dict&&e!==s){r.dict=e;r.ref=t}return r}(e,this.ref,a);if(s){if(!n.has(r)){const e=s.clone();e.set("V",t);n.put(r,{data:e});return e}}else e.set("V",t);return null}async save(e,t,a,n){const s=a?.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let i=s?.value,o=s?.rotation;if(i===this.data.fieldValue||void 0===i){if(!this._hasValueFromXFA&&void 0===o&&void 0===r)return;i||=this.data.fieldValue}if(void 0===o&&!this._hasValueFromXFA&&Array.isArray(i)&&Array.isArray(this.data.fieldValue)&&isArrayEqual(i,this.data.fieldValue)&&void 0===r)return;void 0===o&&(o=this.rotation);let l=null;if(!this._needAppearances){l=await this._getAppearance(e,t,c,a);if(null===l&&void 0===r)return}let f=!1;if(l?.needAppearances){f=!0;l=null}const{xref:h}=e,u=h.fetchIfRef(this.ref);if(!(u instanceof Dict))return;const m=new Dict(h);for(const[e,t]of u.getRawEntries())"AP"!==e&&m.set(e,t);if(void 0!==r){m.set("F",r);if(null===l&&!f){const e=u.getRaw("AP");e&&m.set("AP",e)}}const p={path:this.data.fieldName,value:i},d=this.setValue(m,Array.isArray(i)?i.map(stringToAsciiOrUTF16BE):stringToAsciiOrUTF16BE(i),h,n);this.amendSavedDict(a,d||m);const g=this._getMKDict(o);g&&m.set("MK",g);n.put(this.ref,{data:m,xfa:p,needAppearances:f});if(null!==l){const e=h.getNewTemporaryRef(),t=new Dict(h);m.set("AP",t);t.set("N",e);const s=this._getSaveFieldResources(h),r=new StringStream(l),i=r.dict=new Dict(h);i.setIfName("Subtype","Form");i.set("Resources",s);const f=o%180==0?[0,0,this.width,this.height]:[0,0,this.height,this.width];i.set("BBox",f);const c=this.getRotationMatrix(a);c!==pa&&i.set("Matrix",c);n.put(e,{data:r,xfa:null,needAppearances:!1})}m.set("M",`D:${getModificationDate()}`)}async _getAppearance(e,t,a,n){if(this.data.password)return null;const s=n?.get(this.data.id);let r,o;if(s){r=s.formattedValue||s.value;o=s.rotation}if(void 0===o&&void 0===r&&!this._needAppearances&&(!this._hasValueFromXFA||this.appearance))return null;const l=this.getBorderAndBackgroundAppearances(n);if(void 0===r){r=this.data.fieldValue;if(!r)return`/Tx BMC q ${l}Q EMC`}Array.isArray(r)&&1===r.length&&(r=r[0]);assert("string"==typeof r,"Expected `value` to be a string.");r=r.trimEnd();if(this.data.combo){const e=this.data.options.find(({exportValue:e})=>r===e);r=e?.displayValue||r}if(""===r)return`/Tx BMC q ${l}Q EMC`;void 0===o&&(o=this.rotation);let f,h=-1;if(this.data.multiLine){f=r.split(/\r\n?|\n/).map(e=>e.normalize("NFC"));h=f.length}else f=[r.replace(/\r\n?|\n/,"").normalize("NFC")];let{width:u,height:m}=this;90!==o&&270!==o||([u,m]=[m,u]);this._defaultAppearance||(this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance="/Helvetica 0 Tf 0 g"));let p,d,g,b=await WidgetAnnotation._getFontData(e,t,this.data.defaultAppearanceData,this._fieldResources.mergedResources);const w=[];let j=!1;for(const e of f){const t=b.encodeString(e);t.length>1&&(j=!0);w.push(t.join(""))}if(j&&a&c)return{needAppearances:!0};if(j&&this._isOffscreenCanvasSupported){const a=this.data.comb?"monospace":"sans-serif",n=new FakeUnicodeFont(e.xref,a),s=n.createFontResources(f.join("")),i=s.getRaw("Font");if(this._fieldResources.mergedResources.has("Font")){const e=this._fieldResources.mergedResources.get("Font");for(const[t,a]of i.getRawEntries())e.set(t,a)}else this._fieldResources.mergedResources.set("Font",i);const o=n.fontName.name;b=await WidgetAnnotation._getFontData(e,t,{fontName:o,fontSize:0},s);for(let e=0,t=w.length;e2)return`/Tx BMC q ${l}BT `+p+` 1 0 0 1 ${numberToString(2)} ${numberToString(v)} Tm (${escapeString(w[0])}) Tj ET Q EMC`;return`/Tx BMC q ${l}BT `+p+` 1 0 0 1 0 0 Tm ${this._renderText(w[0],b,d,u,q,{shift:0},2,v)} ET Q EMC`}static async _getFontData(e,t,a,n){const s=new OperatorList,r={font:null,clone(){return this}},{fontName:i,fontSize:o}=a;await e.handleSetFont(n,[i&&Name.get(i),o],null,s,t,r,null);return r.font}_getTextWidth(e,t){return Math.sumPrecise(t.charsToGlyphs(e).map(e=>e.width))/1e3}_computeFontSize(e,t,a,n,r){let{fontSize:i}=this.data.defaultAppearanceData,o=(i||12)*s,l=Math.round(e/o);if(!i){const roundWithTwoDigits=e=>Math.floor(100*e)/100;if(-1===r){const r=this._getTextWidth(a,n);i=roundWithTwoDigits(Math.min(e/s,t/r));l=1}else{const f=a.split(/\r\n?|\n/),c=[];for(const e of f){const t=n.encodeString(e).join(""),a=n.charsToGlyphs(t),s=n.getCharPositions(t);c.push({line:t,glyphs:a,positions:s})}const isTooBig=a=>{let s=0;for(const r of c){s+=this._splitLine(null,n,a,t,r).length*a;if(s>e)return!0}return!1};l=Math.max(l,r);for(;;){o=e/l;i=roundWithTwoDigits(o/s);if(!isTooBig(i))break;l++}}const{fontName:f,fontColor:c}=this.data.defaultAppearanceData;this._defaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:a}){return`/${escapePDFName(t)} ${e} Tf ${getPdfColor(a,!0)}`}({fontSize:i,fontName:f,fontColor:c})}return[this._defaultAppearance,i,e/l]}_renderText(e,t,a,n,s,r,i,o){let l;if(1===s){l=(n-this._getTextWidth(e,t)*a)/2}else if(2===s){l=n-this._getTextWidth(e,t)*a-i}else l=i;const f=numberToString(l-r.shift);r.shift=l;return`${f} ${o=numberToString(o)} Td (${escapeString(e)}) Tj`}_getSaveFieldResources(e){const{localResources:t,appearanceResources:a,acroFormResources:n}=this._fieldResources,s=this.data.defaultAppearanceData?.fontName;if(!s)return t||Dict.empty;for(const e of[t,a])if(e instanceof Dict){const t=e.get("Font");if(t instanceof Dict&&t.has(s))return e}if(n instanceof Dict){const a=n.get("Font");if(a instanceof Dict&&a.has(s)){const n=new Dict(e);n.set(s,a.getRaw(s));const r=new Dict(e);r.set("Font",n);return Dict.merge({xref:e,dictArray:[r,t],mergeSubDicts:!0})}}return t||Dict.empty}getFieldObject(){return null}}class TextWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t}=e;if(t.has("PMD")){this.flags|=N;this.data.hidden=!0;warn("Barcodes are not supported")}this.data.hasOwnCanvas=this.data.readOnly&&!this.data.noHTML;this._hasText=!0;"string"!=typeof this.data.fieldValue&&(this.data.fieldValue="");let a=getInheritableProperty({dict:t,key:"Q"});(!Number.isInteger(a)||a<0||a>2)&&(a=null);this.data.textAlignment=a;let n=getInheritableProperty({dict:t,key:"MaxLen"});(!Number.isInteger(n)||n<0)&&(n=0);this.data.maxLen=n;this.data.multiLine=this.hasFieldFlag(K);this.data.comb=this.hasFieldFlag(ee)&&!this.data.multiLine&&!this.data.password&&!this.hasFieldFlag(J)&&0!==this.data.maxLen;this.data.doNotScroll=this.hasFieldFlag(Z);const{data:{actions:s}}=this;if(!s)return;const r=/^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/;let i=!1;(1===s.Format?.length&&1===s.Keystroke?.length&&r.test(s.Format[0])&&r.test(s.Keystroke[0])||0===s.Format?.length&&1===s.Keystroke?.length&&r.test(s.Keystroke[0])||0===s.Keystroke?.length&&1===s.Format?.length&&r.test(s.Format[0]))&&(i=!0);const o=[];s.Format&&o.push(...s.Format);s.Keystroke&&o.push(...s.Keystroke);if(i){delete s.Keystroke;s.Format=o}for(const e of o){const t=e.match(r);if(!t)continue;const a="Date"===t[1];let n=t[2];const s=parseInt(n,10);isNaN(s)||Math.floor(Math.log10(s))+1!==t[2].length||(n=(a?Ri:Hi)[s]??n);this.data.datetimeFormat=n;if(!i)break;if(a){if(/HH|MM|ss|h/.test(n)){this.data.datetimeType="datetime-local";this.data.timeStep=/ss/.test(n)?1:60}else this.data.datetimeType="date";break}this.data.datetimeType="time";this.data.timeStep=/ss/.test(n)?1:60;break}}get hasTextContent(){return!!this.appearance&&!this._needAppearances}_getCombAppearance(e,t,a,n,s,r,i,o,l,f,c,h,u){const m=s/this.data.maxLen,p=this.getBorderAndBackgroundAppearances(u),d=[],g=t.getCharPositions(a);for(const[e,t]of g)d.push(`(${escapeString(a.substring(e,t))}) Tj`);h&&d.reverse();const b=m*g.length;let w=i;1===c?w+=Math.floor((s-b)/(2*m))*m:2===c&&(w+=s-b);const j=d.join(` ${numberToString(m)} 0 Td `);return`/Tx BMC q ${p}BT `+e+` 1 0 0 1 ${numberToString(w)} ${numberToString(o+l)} Tm ${j} ET Q EMC`}_getMultilineAppearance(e,t,a,n,s,r,i,o,l,f,c,h){const u=[],m=s-2*o,p={shift:0};for(let e=0,r=t.length;en){l.push(e.substring(u,a));u=a;m=d;f=-1;h=-1}else{m+=d;f=a;c=s;h=t}else if(m+d>n)if(-1!==f){l.push(e.substring(u,c));u=c;t=h+1;f=-1;m=0}else{l.push(e.substring(u,a));u=a;m=d}else m+=d}ut?`\\${t}`:"\\s+");new RegExp(`^\\s*${r}\\s*$`).test(this.data.fieldValue)&&(this.data.textContent=this.data.fieldValue.split("\n"))}getFieldObject(){return{id:this.data.id,value:this.data.fieldValue,defaultValue:this.data.defaultFieldValue||"",multiline:this.data.multiLine,password:this.data.password,charLimit:this.data.maxLen,comb:this.data.comb,editable:!this.data.readOnly,hidden:this.data.hidden,name:this.data.fieldName,rect:this.data.rect,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,datetimeFormat:this.data.datetimeFormat,hasDatetimeHTML:!!this.data.datetimeType,type:"text"}}}class ButtonWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);this.checkedAppearance=null;this.uncheckedAppearance=null;const t=this.hasFieldFlag(V),a=this.hasFieldFlag($);this.data.checkBox=!t&&!a;this.data.radioButton=t&&!a;this.data.pushButton=a;this.data.isTooltipOnly=!1;if(this.data.checkBox)this._processCheckBox(e);else if(this.data.radioButton)this._processRadioButton(e);else if(this.data.pushButton){this.data.hasOwnCanvas=!0;this.data.noHTML=!1;this._processPushButton(e)}else warn("Invalid field flags for button widget annotation")}async getOperatorList(e,t,a,n){if(this.data.pushButton)return super.getOperatorList(e,t,a,!1,n);let s=null,r=null;if(n){const e=n.get(this.data.id);s=e?e.value:null;r=e?e.rotation:null}if(null===s&&this.appearance)return super.getOperatorList(e,t,a,n);null==s&&(s=this.data.checkBox?this.data.fieldValue===this.data.exportValue:this.data.fieldValue===this.data.buttonValue);const i=s?this.checkedAppearance:this.uncheckedAppearance;if(i){const s=this.appearance,o=lookupMatrix(i.dict.getArray("Matrix"),pa);r&&i.dict.set("Matrix",this.getRotationMatrix(n));this.appearance=i;const l=super.getOperatorList(e,t,a,n);this.appearance=s;i.dict.set("Matrix",o);return l}return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}async save(e,t,a,n){this.data.checkBox?this._saveCheckbox(e,t,a,n):this.data.radioButton&&this._saveRadioButton(e,t,a,n)}async _saveCheckbox(e,t,a,n){if(!a)return;const s=a.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let i=s?.rotation,o=s?.value;if(void 0===i&&void 0===r){if(void 0===o)return;if(this.data.fieldValue===this.data.exportValue===o)return}let l=e.xref.fetchIfRef(this.ref);if(!(l instanceof Dict))return;l=l.clone();void 0===i&&(i=this.rotation);void 0===o&&(o=this.data.fieldValue===this.data.exportValue);const f={path:this.data.fieldName,value:o?this.data.exportValue:""},c=Name.get(o?this.data.exportValue:"Off");this.setValue(l,c,e.xref,n);l.set("AS",c);l.set("M",`D:${getModificationDate()}`);void 0!==r&&l.set("F",r);const h=this._getMKDict(i);h&&l.set("MK",h);n.put(this.ref,{data:l,xfa:f,needAppearances:!1})}async _saveRadioButton(e,t,a,n){if(!a)return;const s=a.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let i=s?.rotation,o=s?.value;if(void 0===i&&void 0===r){if(void 0===o)return;if(this.data.fieldValue===this.data.buttonValue===o)return}let l=e.xref.fetchIfRef(this.ref);if(!(l instanceof Dict))return;l=l.clone();void 0===o&&(o=this.data.fieldValue===this.data.buttonValue);void 0===i&&(i=this.rotation);const f={path:this.data.fieldName,value:o?this.data.buttonValue:""},c=Name.get(o?this.data.buttonValue:"Off");o&&this.setValue(l,c,e.xref,n);l.set("AS",c);l.set("M",`D:${getModificationDate()}`);void 0!==r&&l.set("F",r);const h=this._getMKDict(i);h&&l.set("MK",h);n.put(this.ref,{data:l,xfa:f,needAppearances:!1})}_getDefaultCheckedAppearance(e,t){const{width:a,height:n}=this,s=[0,0,a,n],r=.8*Math.min(a,n);let i,o;if("check"===t){i={width:.755*r,height:.705*r};o="3"}else if("disc"===t){i={width:.791*r,height:.705*r};o="l"}else unreachable(`_getDefaultCheckedAppearance - unsupported type: ${t}`);const l=`q BT /PdfJsZaDb ${r} Tf 0 g ${numberToString((a-i.width)/2)} ${numberToString((n-i.height)/2)} Td (${o}) Tj ET Q`,f=new Dict(e.xref);f.set("FormType",1);f.setIfName("Subtype","Form");f.setIfName("Type","XObject");f.set("BBox",s);f.set("Matrix",[1,0,0,1,0,0]);f.set("Length",l.length);const c=new Dict(e.xref),h=new Dict(e.xref);h.set("PdfJsZaDb",this.fallbackFontDict);c.set("Font",h);f.set("Resources",c);this.checkedAppearance=new StringStream(l);this.checkedAppearance.dict=f;this._streams.push(this.checkedAppearance)}_processCheckBox(e){const t=e.dict.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(!(a instanceof Dict))return;const n=this._decodeFormValue(e.dict.get("AS"));"string"==typeof n&&(this.data.fieldValue=n);const s=null!==this.data.fieldValue&&"Off"!==this.data.fieldValue?this.data.fieldValue:"Yes",r=[...a.getKeys()];if(0===r.length)r.push("Off",s);else if(1===r.length)"Off"===r[0]?r.push(s):r.unshift("Off");else if(r.includes(s)){r.length=0;r.push("Off",s)}else{const e=r.find(e=>"Off"!==e);r.length=0;r.push("Off",e)}r.includes(this.data.fieldValue)||(this.data.fieldValue="Off");this.data.exportValue=r[1];const i=a.get(this.data.exportValue);this.checkedAppearance=i instanceof BaseStream?i:null;const o=a.get("Off");this.uncheckedAppearance=o instanceof BaseStream?o:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"check");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processRadioButton(e){this.data.buttonValue=null;const t=e.dict.get("Parent");if(t instanceof Dict){this.parent=e.dict.getRaw("Parent");const a=t.get("V");a instanceof Name&&(this.data.fieldValue=this._decodeFormValue(a))}const a=e.dict.get("AP");if(!(a instanceof Dict))return;const n=a.get("N");if(!(n instanceof Dict))return;for(const e of n.getKeys())if("Off"!==e){this.data.buttonValue=e;break}const s=n.get(this.data.buttonValue);this.checkedAppearance=s instanceof BaseStream?s:null;const r=n.get("Off");this.uncheckedAppearance=r instanceof BaseStream?r:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"disc");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processPushButton(e){const{dict:t,annotationGlobals:a}=e;if(t.has("A")||t.has("AA")||this.data.alternativeText){this.data.isTooltipOnly=!t.has("A")&&!t.has("AA");Catalog.parseDestDictionary({destDict:t,resultObj:this.data,docBaseUrl:a.baseUrl,docAttachments:a.attachments})}else warn("Push buttons without action dictionaries are not supported")}getFieldObject(){let e,t="button";if(this.data.checkBox){t="checkbox";e=this.data.exportValue}else if(this.data.radioButton){t="radiobutton";e=this.data.buttonValue}return{id:this.data.id,value:this.data.fieldValue||"Off",defaultValue:this.data.defaultFieldValue,exportValues:e,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,hidden:this.data.hidden,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:t}}get fallbackFontDict(){const e=new Dict;e.setIfName("BaseFont","ZapfDingbats");e.setIfName("Type","FallbackType");e.setIfName("Subtype","FallbackType");e.setIfName("Encoding","ZapfDingbatsEncoding");return shadow(this,"fallbackFontDict",e)}}class ChoiceWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.indices=t.getArray("I");this.hasIndices=Array.isArray(this.indices)&&this.indices.length>0;this.data.options=[];const n=getInheritableProperty({dict:t,key:"Opt"});if(Array.isArray(n))for(let e=0,t=n.length;e=0&&t0&&(this.data.options=this.data.fieldValue.map(e=>({exportValue:e,displayValue:e})));this.data.combo=this.hasFieldFlag(Y);this.data.multiSelect=this.hasFieldFlag(Q);this._hasText=!0}getFieldObject(){const e=this.data.combo?"combobox":"listbox",t=this.data.fieldValue.length>0?this.data.fieldValue[0]:null;return{id:this.data.id,value:t,defaultValue:this.data.defaultFieldValue,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,numItems:this.data.fieldValue.length,multipleSelection:this.data.multiSelect,hidden:this.data.hidden,actions:this.data.actions,items:this.data.options,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:e}}amendSavedDict(e,t){if(!this.hasIndices)return;let a=e?.get(this.data.id)?.value;Array.isArray(a)||(a=[a]);const n=[],{options:s}=this.data;for(let e=0,t=0,r=s.length;ea){a=n;t=e}}[m,p]=this._computeFontSize(e,l-4,t,u,-1)}const d=p*s,g=(d-p)/2,b=Math.floor(f/d);let w=0;if(h.length>0){const e=Math.min(...h),t=Math.max(...h);w=Math.max(0,t-b+1);w>e&&(w=e)}const j=Math.min(w+b+1,c),k=["/Tx BMC q",`1 1 ${l} ${f} re W n`];if(h.length){k.push("0.600006 0.756866 0.854904 rg");for(const e of h)w<=e&&ee.trimEnd());const{coords:e,bbox:t,matrix:a}=FakeUnicodeFont.getFirstPositionInfo(this.rectangle,this.rotation,n);this.data.textPosition=this._transformPoint(e,t,a)}if(this._isOffscreenCanvasSupported){const s=e.dict.get("CA"),r=new FakeUnicodeFont(a,"sans-serif");this.appearance=r.createAppearance(this._contents.str,this.rectangle,this.rotation,n,t,s);this._streams.push(this.appearance)}else warn("FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly.")}}get hasTextContent(){return this._hasAppearance}static createNewDict(e,t,{apRef:a,ap:n}){const{color:s,date:r,fontSize:i,oldAnnotation:o,rect:l,rotation:f,user:c,value:h}=e,u=o||new Dict(t);u.setIfNotExists("Type",Name.get("Annot"));u.setIfNotExists("Subtype",Name.get("FreeText"));u.set(o?"M":"CreationDate",`D:${getModificationDate(r)}`);o&&u.delete("RC");u.setIfArray("Rect",l);const m=`/Helv ${i} Tf ${getPdfColor(s,!0)}`;u.set("DA",m);u.setIfDefined("Contents",stringToAsciiOrUTF16BE(h));u.setIfNotExists("F",4);u.setIfNotExists("Border",[0,0,0]);u.setIfNumber("Rotate",f);u.setIfDefined("T",stringToAsciiOrUTF16BE(c));if(a||n){const e=new Dict(t);u.set("AP",e);e.set("N",a||n)}return u}static async createNewAppearanceStream(e,t,a){const{baseFontRef:n,evaluator:r,task:i}=a,{color:o,fontSize:l,rect:f,rotation:c,value:h}=e;if(!o)return null;const u=new Dict(t),m=new Dict(t);if(n)m.set("Helv",n);else{const e=new Dict(t);e.setIfName("BaseFont","Helvetica");e.setIfName("Type","Font");e.setIfName("Subtype","Type1");e.setIfName("Encoding","WinAnsiEncoding");m.set("Helv",e)}u.set("Font",m);const p=await WidgetAnnotation._getFontData(r,i,{fontName:"Helv",fontSize:l},u),[d,g,b,w]=f;let j=b-d,k=w-g;c%180!=0&&([j,k]=[k,j]);const y=h.split("\n"),q=l/1e3;let v=-1/0;const S=[];for(let e of y){const t=p.encodeString(e);if(t.length>1)return null;e=t.join("");S.push(e);let a=0;const n=p.charsToGlyphs(e);for(const e of n)a+=e.width*q;v=Math.max(v,a)}let x=1;v>j&&(x=j/v);let C=1;const F=s*l,T=1*l,O=F*y.length;O>k&&(C=k/O);const R=l*Math.min(x,C);let H,D,M;switch(c){case 0:M=[1,0,0,1];D=[f[0],f[1],j,k];H=[f[0],f[3]-T];break;case 90:M=[0,1,-1,0];D=[f[1],-f[2],j,k];H=[f[1],-f[0]-T];break;case 180:M=[-1,0,0,-1];D=[-f[2],-f[3],j,k];H=[-f[2],-f[1]-T];break;case 270:M=[0,-1,1,0];D=[-f[3],f[0],j,k];H=[-f[3],f[2]-T]}const N=["q",`${M.join(" ")} 0 0 cm`,`${D.join(" ")} re W n`,"BT",`${getPdfColor(o,!0)}`,`0 Tc /Helv ${numberToString(R)} Tf`];N.push(`${H.join(" ")} Td (${escapeString(S[0])}) Tj`);const E=numberToString(F);for(let e=1,t=S.length;e{e.push(`${n[0]} ${n[1]} m`,`${n[2]} ${n[3]} l`,"S");return[t[0]-o,t[7]-o,t[2]+o,t[3]+o]}})}}}class SquareAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;if(!this.appearance){const e=getPdfColorArray(this.color,[0,0,0]),n=t.get("CA"),s=getPdfColorArray(getRgbColor(t.getArray("IC"),null)),r=s?n:null;if(0===this.borderStyle.width&&!s)return;this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:s,strokeAlpha:n,fillAlpha:r,pointsCallback:(e,t)=>{const a=t[4]+this.borderStyle.width/2,n=t[5]+this.borderStyle.width/2,r=t[6]-t[4]-this.borderStyle.width,i=t[3]-t[7]-this.borderStyle.width;e.push(`${a} ${n} ${r} ${i} re`);s?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class CircleAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;if(!this.appearance){const e=getPdfColorArray(this.color,[0,0,0]),n=t.get("CA"),s=getPdfColorArray(getRgbColor(t.getArray("IC"),null)),r=s?n:null;if(0===this.borderStyle.width&&!s)return;const i=4/3*Math.tan(Math.PI/8);this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:s,strokeAlpha:n,fillAlpha:r,pointsCallback:(e,t)=>{const a=t[0]+this.borderStyle.width/2,n=t[1]-this.borderStyle.width/2,r=t[6]-this.borderStyle.width/2,o=t[7]+this.borderStyle.width/2,l=a+(r-a)/2,f=n+(o-n)/2,c=(r-a)/2*i,h=(o-n)/2*i;e.push(`${l} ${o} m`,`${l+c} ${o} ${r} ${f+h} ${r} ${f} c`,`${r} ${f-h} ${l+c} ${n} ${l} ${n} c`,`${l-c} ${n} ${a} ${f-h} ${a} ${f} c`,`${a} ${f+h} ${l-c} ${o} ${l} ${o} c`,"h");s?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class PolylineAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:a,xref:n}=e;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;this.data.vertices=null;if(!(this instanceof PolygonAnnotation)){this.setLineEndings(a.getArray("LE"));this.data.lineEndings=this.lineEndings}const s=a.getArray("Vertices");if(!isNumberArray(s,null))return;const r=this.data.vertices=Float32Array.from(s);if(!this.appearance){const e=getPdfColorArray(this.color,[0,0,0]),s=a.get("CA");let i,o=getRgbColor(a.getArray("IC"),null);o&&(o=getPdfColorArray(o));i=o?this.color?o.every((t,a)=>t===e[a])?"f":"B":"f":"S";const l=this.borderStyle.width||1,f=2*l,c=t.slice();for(let e=0,t=r.length;e{for(let t=0,a=r.length;t{for(const t of this.data.inkLists){for(let a=0,n=t.length;a0){const e=new Dict(t);p.set("BS",e);e.set("W",u)}p.setIfArray("C",getPdfColorArray(r));p.setIfNumber("CA",o);if(n||a){const e=new Dict(t);p.set("AP",e);e.set("N",a||n)}return p}static async createNewAppearanceStream(e,t,a){if(e.outlines)return this.createNewAppearanceStreamForHighlight(e,t,a);const{color:n,rect:s,paths:r,thickness:i,opacity:o}=e;if(!n)return null;const l=[`${i} w 1 J 1 j`,`${getPdfColor(n,!1)}`];1!==o&&l.push("/R0 gs");for(const e of r.lines){l.push(`${numberToString(e[4])} ${numberToString(e[5])} m`);for(let t=6,a=e.length;t{e.push(`${t[0]} ${t[1]} m`,`${t[2]} ${t[3]} l`,`${t[6]} ${t[7]} l`,`${t[4]} ${t[5]} l`,"f");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}get overlaysTextContent(){return!0}static createNewDict(e,t,{apRef:a,ap:n}){const{color:s,date:r,oldAnnotation:i,opacity:o,rect:l,rotation:f,user:c,quadPoints:h}=e,u=i||new Dict(t);u.setIfNotExists("Type",Name.get("Annot"));u.setIfNotExists("Subtype",Name.get("Highlight"));u.set(i?"M":"CreationDate",`D:${getModificationDate(r)}`);u.setIfArray("Rect",l);u.setIfNotExists("F",4);u.setIfNotExists("Border",[0,0,0]);u.setIfNumber("Rotate",f);u.setIfArray("QuadPoints",h);u.setIfArray("C",getPdfColorArray(s));u.setIfNumber("CA",o);u.setIfDefined("T",stringToAsciiOrUTF16BE(c));if(a||n){const e=new Dict(t);u.set("AP",e);e.set("N",a||n)}return u}static async createNewAppearanceStream(e,t,a){const{color:n,rect:s,outlines:r,opacity:i}=e;if(!n)return null;const o=[`${getPdfColor(n,!0)}`,"/R0 gs"],l=[];for(const e of r){l.length=0;l.push(`${numberToString(e[0])} ${numberToString(e[1])} m`);for(let t=2,a=e.length;t{e.push(`${t[4]} ${t[5]+1.3} m`,`${t[6]} ${t[7]+1.3} l`,"S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}get overlaysTextContent(){return!0}}class SquigglyAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;if(this.data.quadPoints=getQuadPoints(t,null)){if(!this.appearance){const e=getPdfColorArray(this.color,[0,0,0]),n=t.get("CA");this._setDefaultAppearance({xref:a,extra:"[] 0 d 1 w",strokeColor:e,strokeAlpha:n,pointsCallback:(e,t)=>{const a=(t[1]-t[5])/6;let n=a,s=t[4];const r=t[5],i=t[6];e.push(`${s} ${r+n} m`);do{s+=2;n=0===n?a:0;e.push(`${s} ${r+n} l`)}while(s{e.push((t[0]+t[4])/2+" "+(t[1]+t[5])/2+" m",(t[2]+t[6])/2+" "+(t[3]+t[7])/2+" l","S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}get overlaysTextContent(){return!0}}class StampAnnotation extends MarkupAnnotation{#ot=null;constructor(e){super(e);this.data.hasOwnCanvas=this.data.noRotate;this.data.isEditable=!this.data.noHTML;this.data.noHTML=!1}mustBeViewedWhenEditing(e,t=null){if(e){if(!this.data.isEditable)return!0;this.#ot??=this.data.hasOwnCanvas;this.data.hasOwnCanvas=!0;return!0}if(null!==this.#ot){this.data.hasOwnCanvas=this.#ot;this.#ot=null}return!t?.has(this.data.id)}static async createImage(e,t){const{width:a,height:n}=e,s=new OffscreenCanvas(a,n),r=s.getContext("2d",{alpha:!0});r.drawImage(e,0,0);const i=r.getImageData(0,0,a,n).data,o=new Uint32Array(i.buffer),l=o.some(FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>!!(255&~e));if(l){r.fillStyle="white";r.fillRect(0,0,a,n);r.drawImage(e,0,0)}const f=s.convertToBlob({type:"image/jpeg",quality:1}).then(e=>e.bytes()),c=Name.get("XObject"),h=Name.get("Image"),u=new Dict(t);u.set("Type",c);u.set("Subtype",h);u.set("BitsPerComponent",8);u.setIfName("ColorSpace","DeviceRGB");u.setIfName("Filter","DCTDecode");u.set("BBox",[0,0,a,n]);u.set("Width",a);u.set("Height",n);let m=null;if(l){const e=new Uint8Array(o.length);if(FeatureTest.isLittleEndian)for(let t=0,a=o.length;t>>24;else for(let t=0,a=o.length;t=0&&s<=1?s:null}}const df={get r(){return shadow(this,"r",new Uint8Array([7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21]))},get k(){return shadow(this,"k",new Int32Array([-680876936,-389564586,606105819,-1044525330,-176418897,1200080426,-1473231341,-45705983,1770035416,-1958414417,-42063,-1990404162,1804603682,-40341101,-1502002290,1236535329,-165796510,-1069501632,643717713,-373897302,-701558691,38016083,-660478335,-405537848,568446438,-1019803690,-187363961,1163531501,-1444681467,-51403784,1735328473,-1926607734,-378558,-2022574463,1839030562,-35309556,-1530992060,1272893353,-155497632,-1094730640,681279174,-358537222,-722521979,76029189,-640364487,-421815835,530742520,-995338651,-198630844,1126891415,-1416354905,-57434055,1700485571,-1894986606,-1051523,-2054922799,1873313359,-30611744,-1560198380,1309151649,-145523070,-1120210379,718787259,-343485551]))}};function calculateMD5(e,t,a){let n=1732584193,s=-271733879,r=-1732584194,i=271733878;const o=a+72&-64,l=new Uint8Array(o);let f,c;for(f=0;f>5&255;l[f++]=a>>13&255;l[f++]=a>>21&255;l[f++]=a>>>29&255;f+=3;const u=new Int32Array(16),{k:m,r:p}=df;for(f=0;f>>32-r)|0;a=n}n=n+a|0;s=s+o|0;r=r+h|0;i=i+d|0}return new Uint8Array([255&n,n>>8&255,n>>16&255,n>>>24&255,255&s,s>>8&255,s>>16&255,s>>>24&255,255&r,r>>8&255,r>>16&255,r>>>24&255,255&i,i>>8&255,i>>16&255,i>>>24&255])}function decodeString(e){try{return stringToUTF8String(e)}catch(t){warn(`UTF-8 decoding failed: "${t}".`);return e}}class DatasetXMLParser extends SimpleXMLParser{node=null;onEndElement(e){const t=super.onEndElement(e);if(t&&"xfa:datasets"===e){this.node=t;throw new Error("Aborting DatasetXMLParser.")}}}class DatasetReader{constructor(e){if(e.datasets)this.node=new SimpleXMLParser({hasAttributes:!0}).parseFromString(e.datasets).documentElement;else{const t=new DatasetXMLParser({hasAttributes:!0});try{t.parseFromString(e["xdp:xdp"])}catch{}this.node=t.node}}getValue(e){if(!this.node||!e)return"";const t=this.node.searchNode(parseXFAPath(e),0);if(!t)return"";const a=t.firstChild;return"value"===a?.nodeName?t.children.map(e=>decodeString(e.textContent)):decodeString(t.textContent)}}class SingleIntersector{#ft;minX=1/0;minY=1/0;maxX=-1/0;maxY=-1/0;#ct=null;#ht=[];#ut=[];#mt=-1;#pt=!1;constructor(e){this.#ft=e;const t=e.data.quadPoints;if(t){for(let e=0,a=t.length;e8&&(this.#ct=t)}else[this.minX,this.minY,this.maxX,this.maxY]=e.data.rect}#dt(e,t){if(this.minX>=e||this.maxX<=e||this.minY>=t||this.maxY<=t)return!1;const a=this.#ct;if(!a)return!0;if(this.#mt>=0){const n=this.#mt;if(!(a[n]>=e||a[n+2]<=e||a[n+5]>=t||a[n+1]<=t))return!0;this.#mt=-1}for(let n=0,s=a.length;n=e||a[n+2]<=e||a[n+5]>=t||a[n+1]<=t)){this.#mt=n;return!0}return!1}addGlyph(e,t,a){if(!this.#dt(e,t)){this.disableExtraChars();return!1}if(this.#ut.length>0){this.#ht.push(this.#ut.join(""));this.#ut.length=0}this.#ht.push(a);this.#pt=!0;return!0}addExtraChar(e){this.#pt&&this.#ut.push(e)}disableExtraChars(){if(this.#pt){this.#pt=!1;this.#ut.length=0}}setText(){this.#ft.data.overlaidText=this.#ht.join("")}}const gf=64;class Intersector{#gt=[];#bt=[];#wt;#jt;#kt;#yt;#qt;#vt;constructor(e){let t=1/0,a=1/0,n=-1/0,s=-1/0;const r=this.#gt;for(const i of e){if(!i.data.quadPoints&&!i.data.rect)continue;const e=new SingleIntersector(i);r.push(e);t=Math.min(t,e.minX);a=Math.min(a,e.minY);n=Math.max(n,e.maxX);s=Math.max(s,e.maxY)}this.#wt=t;this.#kt=a;this.#jt=n;this.#yt=s;this.#qt=63/(n-t);this.#vt=63/(s-a);for(const e of r){const t=this.#St(e.minX,e.minY),a=this.#St(e.maxX,e.maxY),n=(a-t)%gf,s=Math.floor((a-t)/gf);for(let a=t;a<=t+s*gf;a+=gf)for(let t=0;t<=n;t++){let n=this.#bt[a+t];n||(this.#bt[a+t]=n=[]);n.push(e)}}}#St(e,t){return Math.floor((e-this.#wt)*this.#qt)+Math.floor((t-this.#kt)*this.#vt)*gf}addGlyph(e,t,a,n){const s=e[4]+t/2,r=e[5]+a/2;if(sthis.#jt||r>this.#yt)return;const i=this.#bt[this.#St(s,r)];if(i)for(const e of i)e.addGlyph(s,r,n)}addExtraChar(e){for(const t of this.#gt)t.addExtraChar(e)}setText(){for(const e of this.#gt)e.setText()}}class Word64{constructor(e,t){this.high=0|e;this.low=0|t}and(e){this.high&=e.high;this.low&=e.low}xor(e){this.high^=e.high;this.low^=e.low}shiftRight(e){if(e>=32){this.low=this.high>>>e-32|0;this.high=0}else{this.low=this.low>>>e|this.high<<32-e;this.high=this.high>>>e|0}}rotateRight(e){let t,a;if(32&e){a=this.low;t=this.high}else{t=this.low;a=this.high}e&=31;this.low=t>>>e|a<<32-e;this.high=a>>>e|t<<32-e}not(){this.high=~this.high;this.low=~this.low}add(e){const t=(this.low>>>0)+(e.low>>>0);let a=(this.high>>>0)+(e.high>>>0);t>4294967295&&(a+=1);this.low=0|t;this.high=0|a}copyTo(e,t){e[t]=this.high>>>24&255;e[t+1]=this.high>>16&255;e[t+2]=this.high>>8&255;e[t+3]=255&this.high;e[t+4]=this.low>>>24&255;e[t+5]=this.low>>16&255;e[t+6]=this.low>>8&255;e[t+7]=255&this.low}assign(e){this.high=e.high;this.low=e.low}}const bf={get k(){return shadow(this,"k",[new Word64(1116352408,3609767458),new Word64(1899447441,602891725),new Word64(3049323471,3964484399),new Word64(3921009573,2173295548),new Word64(961987163,4081628472),new Word64(1508970993,3053834265),new Word64(2453635748,2937671579),new Word64(2870763221,3664609560),new Word64(3624381080,2734883394),new Word64(310598401,1164996542),new Word64(607225278,1323610764),new Word64(1426881987,3590304994),new Word64(1925078388,4068182383),new Word64(2162078206,991336113),new Word64(2614888103,633803317),new Word64(3248222580,3479774868),new Word64(3835390401,2666613458),new Word64(4022224774,944711139),new Word64(264347078,2341262773),new Word64(604807628,2007800933),new Word64(770255983,1495990901),new Word64(1249150122,1856431235),new Word64(1555081692,3175218132),new Word64(1996064986,2198950837),new Word64(2554220882,3999719339),new Word64(2821834349,766784016),new Word64(2952996808,2566594879),new Word64(3210313671,3203337956),new Word64(3336571891,1034457026),new Word64(3584528711,2466948901),new Word64(113926993,3758326383),new Word64(338241895,168717936),new Word64(666307205,1188179964),new Word64(773529912,1546045734),new Word64(1294757372,1522805485),new Word64(1396182291,2643833823),new Word64(1695183700,2343527390),new Word64(1986661051,1014477480),new Word64(2177026350,1206759142),new Word64(2456956037,344077627),new Word64(2730485921,1290863460),new Word64(2820302411,3158454273),new Word64(3259730800,3505952657),new Word64(3345764771,106217008),new Word64(3516065817,3606008344),new Word64(3600352804,1432725776),new Word64(4094571909,1467031594),new Word64(275423344,851169720),new Word64(430227734,3100823752),new Word64(506948616,1363258195),new Word64(659060556,3750685593),new Word64(883997877,3785050280),new Word64(958139571,3318307427),new Word64(1322822218,3812723403),new Word64(1537002063,2003034995),new Word64(1747873779,3602036899),new Word64(1955562222,1575990012),new Word64(2024104815,1125592928),new Word64(2227730452,2716904306),new Word64(2361852424,442776044),new Word64(2428436474,593698344),new Word64(2756734187,3733110249),new Word64(3204031479,2999351573),new Word64(3329325298,3815920427),new Word64(3391569614,3928383900),new Word64(3515267271,566280711),new Word64(3940187606,3454069534),new Word64(4118630271,4000239992),new Word64(116418474,1914138554),new Word64(174292421,2731055270),new Word64(289380356,3203993006),new Word64(460393269,320620315),new Word64(685471733,587496836),new Word64(852142971,1086792851),new Word64(1017036298,365543100),new Word64(1126000580,2618297676),new Word64(1288033470,3409855158),new Word64(1501505948,4234509866),new Word64(1607167915,987167468),new Word64(1816402316,1246189591)])}};function ch(e,t,a,n,s){e.assign(t);e.and(a);s.assign(t);s.not();s.and(n);e.xor(s)}function maj(e,t,a,n,s){e.assign(t);e.and(a);s.assign(t);s.and(n);e.xor(s);s.assign(a);s.and(n);e.xor(s)}function sigma(e,t,a){e.assign(t);e.rotateRight(28);a.assign(t);a.rotateRight(34);e.xor(a);a.assign(t);a.rotateRight(39);e.xor(a)}function sigmaPrime(e,t,a){e.assign(t);e.rotateRight(14);a.assign(t);a.rotateRight(18);e.xor(a);a.assign(t);a.rotateRight(41);e.xor(a)}function littleSigma(e,t,a){e.assign(t);e.rotateRight(1);a.assign(t);a.rotateRight(8);e.xor(a);a.assign(t);a.shiftRight(7);e.xor(a)}function littleSigmaPrime(e,t,a){e.assign(t);e.rotateRight(19);a.assign(t);a.rotateRight(61);e.xor(a);a.assign(t);a.shiftRight(6);e.xor(a)}function calculateSHA512(e,t,a,n=!1){let s,r,i,o,l,f,c,h;if(n){s=new Word64(3418070365,3238371032);r=new Word64(1654270250,914150663);i=new Word64(2438529370,812702999);o=new Word64(355462360,4144912697);l=new Word64(1731405415,4290775857);f=new Word64(2394180231,1750603025);c=new Word64(3675008525,1694076839);h=new Word64(1203062813,3204075428)}else{s=new Word64(1779033703,4089235720);r=new Word64(3144134277,2227873595);i=new Word64(1013904242,4271175723);o=new Word64(2773480762,1595750129);l=new Word64(1359893119,2917565137);f=new Word64(2600822924,725511199);c=new Word64(528734635,4215389547);h=new Word64(1541459225,327033209)}const u=128*Math.ceil((a+17)/128),m=new Uint8Array(u);let p,d;for(p=0;p>>29&255;m[p++]=a>>21&255;m[p++]=a>>13&255;m[p++]=a>>5&255;m[p++]=a<<3&255;const b=new Array(80);for(p=0;p<80;p++)b[p]=new Word64(0,0);const{k:w}=bf;let j=new Word64(0,0),k=new Word64(0,0),y=new Word64(0,0),q=new Word64(0,0),v=new Word64(0,0),S=new Word64(0,0),x=new Word64(0,0),C=new Word64(0,0);const F=new Word64(0,0),T=new Word64(0,0),O=new Word64(0,0),R=new Word64(0,0);let H,D;for(p=0;p>>t|e<<32-t}function calculate_sha256_ch(e,t,a){return e&t^~e&a}function calculate_sha256_maj(e,t,a){return e&t^e&a^t&a}function calculate_sha256_sigma(e){return rotr(e,2)^rotr(e,13)^rotr(e,22)}function calculate_sha256_sigmaPrime(e){return rotr(e,6)^rotr(e,11)^rotr(e,25)}function calculate_sha256_littleSigma(e){return rotr(e,7)^rotr(e,18)^e>>>3}function calculate_sha256_littleSigmaPrime(e){return rotr(e,17)^rotr(e,19)^e>>>10}function calculateSHA256(e,t,a){let n=1779033703,s=3144134277,r=1013904242,i=2773480762,o=1359893119,l=2600822924,f=528734635,c=1541459225;const h=64*Math.ceil((a+9)/64),u=new Uint8Array(h);let m,p;for(m=0;m>>29&255;u[m++]=a>>21&255;u[m++]=a>>13&255;u[m++]=a>>5&255;u[m++]=a<<3&255;const g=new Uint32Array(64),{k:b}=wf;for(m=0;m>24&255,n>>16&255,n>>8&255,255&n,s>>24&255,s>>16&255,s>>8&255,255&s,r>>24&255,r>>16&255,r>>8&255,255&r,i>>24&255,i>>16&255,i>>8&255,255&i,o>>24&255,o>>16&255,o>>8&255,255&o,l>>24&255,l>>16&255,l>>8&255,255&l,f>>24&255,f>>16&255,f>>8&255,255&f,c>>24&255,c>>16&255,c>>8&255,255&c])}class DecryptStream extends DecodeStream{constructor(e,t,a){super(t);this.stream=e;this.dict=e.dict;this.decrypt=a;this.nextChunk=null;this.initialized=!1}readBlock(){let e;if(this.initialized)e=this.nextChunk;else{e=this.stream.getBytes(512);this.initialized=!0}if(!e?.length){this.eof=!0;return}this.nextChunk=this.stream.getBytes(512);const t=this.nextChunk?.length>0;e=(0,this.decrypt)(e,!t);const a=this.bufferLength,n=a+e.length;this.ensureBuffer(n).set(e,a);this.bufferLength=n}getOriginalStream(){return this}}class ARCFourCipher{a=0;b=0;constructor(e){const t=new Uint8Array(256),a=e.length;for(let e=0;e<256;++e)t[e]=e;for(let n=0,s=0;n<256;++n){const r=t[n];s=s+r+e[n%a]&255;t[n]=t[s];t[s]=r}this.s=t}encryptBlock(e){let t=this.a,a=this.b;const n=this.s,s=e.length,r=new Uint8Array(s);for(let i=0;it<128?t<<1:t<<1^27);constructor(){this.buffer=new Uint8Array(16);this.bufferPosition=0}_expandKey(e){unreachable("Cannot call `_expandKey` on the base class")}_decrypt(e,t){let a,n,s;const r=new Uint8Array(16);r.set(e);for(let e=0,a=this._keySize;e<16;++e,++a)r[e]^=t[a];for(let e=this._cyclesOfRepetition-1;e>=1;--e){a=r[13];r[13]=r[9];r[9]=r[5];r[5]=r[1];r[1]=a;a=r[14];n=r[10];r[14]=r[6];r[10]=r[2];r[6]=a;r[2]=n;a=r[15];n=r[11];s=r[7];r[15]=r[3];r[11]=a;r[7]=n;r[3]=s;for(let e=0;e<16;++e)r[e]=this._inv_s[r[e]];for(let a=0,n=16*e;a<16;++a,++n)r[a]^=t[n];for(let e=0;e<16;e+=4){const t=this._mix[r[e]],n=this._mix[r[e+1]],s=this._mix[r[e+2]],i=this._mix[r[e+3]];a=t^n>>>8^n<<24^s>>>16^s<<16^i>>>24^i<<8;r[e]=a>>>24&255;r[e+1]=a>>16&255;r[e+2]=a>>8&255;r[e+3]=255&a}}a=r[13];r[13]=r[9];r[9]=r[5];r[5]=r[1];r[1]=a;a=r[14];n=r[10];r[14]=r[6];r[10]=r[2];r[6]=a;r[2]=n;a=r[15];n=r[11];s=r[7];r[15]=r[3];r[11]=a;r[7]=n;r[3]=s;for(let e=0;e<16;++e){r[e]=this._inv_s[r[e]];r[e]^=t[e]}return r}_encrypt(e,t){const a=this._s;let n,s,r;const i=new Uint8Array(16);i.set(e);for(let e=0;e<16;++e)i[e]^=t[e];for(let e=1;e=n;--a)if(e[a]!==t){t=0;break}o-=t;r[r.length-1]=e.subarray(0,16-t)}}const l=new Uint8Array(o);for(let e=0,t=0,a=r.length;e=256&&(o=255&(27^o))}for(let t=0;t<4;++t){a[e]=n^=a[e-32];e++;a[e]=s^=a[e-32];e++;a[e]=r^=a[e-32];e++;a[e]=i^=a[e-32];e++}}return a}}class PDFBase{_hash(e,t,a){unreachable("Abstract method `_hash` called")}checkOwnerPassword(e,t,a,n){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(a,e.length+t.length);return isArrayEqual(this._hash(e,s,a),n)}checkUserPassword(e,t,a){const n=new Uint8Array(e.length+8);n.set(e,0);n.set(t,e.length);return isArrayEqual(this._hash(e,n,[]),a)}getOwnerKey(e,t,a,n){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(a,e.length+t.length);const r=this._hash(e,s,a);return new AES256Cipher(r).decryptBlock(n,!1,new Uint8Array(16))}getUserKey(e,t,a){const n=new Uint8Array(e.length+8);n.set(e,0);n.set(t,e.length);const s=this._hash(e,n,[]);return new AES256Cipher(s).decryptBlock(a,!1,new Uint8Array(16))}}class PDF17 extends PDFBase{_hash(e,t,a){return calculateSHA256(t,0,t.length)}}class PDF20 extends PDFBase{_hash(e,t,a){let n=calculateSHA256(t,0,t.length).subarray(0,32),s=[0],r=0;for(;r<64||s.at(-1)>r-32;){const t=e.length+n.length+a.length,i=new Uint8Array(t);let o=0;i.set(e,o);o+=e.length;i.set(n,o);o+=n.length;i.set(a,o);const l=new Uint8Array(64*t);for(let e=0,a=0;e<64;e++,a+=t)l.set(i,a);s=new AES128Cipher(n.subarray(0,16)).encrypt(l,n.subarray(16,32));const f=Math.sumPrecise(s.slice(0,16))%3;0===f?n=calculateSHA256(s,0,s.length):1===f?n=calculateSHA384(s,0,s.length):2===f&&(n=calculateSHA512(s,0,s.length));r++}return n.subarray(0,32)}}class CipherTransform{constructor(e,t){this.StringCipherConstructor=e;this.StreamCipherConstructor=t}createStream(e,t){const a=new this.StreamCipherConstructor;return new DecryptStream(e,t,function cipherTransformDecryptStream(e,t){return a.decryptBlock(e,t)})}decryptString(e){const t=new this.StringCipherConstructor;let a=stringToBytes(e);a=t.decryptBlock(a,!0);return bytesToString(a)}encryptString(e){const t=new this.StringCipherConstructor;if(t instanceof AESBaseCipher){const a=16-e.length%16;e+=String.fromCharCode(a).repeat(a);const n=new Uint8Array(16);crypto.getRandomValues(n);let s=stringToBytes(e);s=t.encrypt(s,n);const r=new Uint8Array(16+s.length);r.set(n);r.set(s,16);return bytesToString(r)}let a=stringToBytes(e);a=t.encrypt(a);return bytesToString(a)}}class CipherTransformFactory{static get _defaultPasswordBytes(){return shadow(this,"_defaultPasswordBytes",new Uint8Array([40,191,78,94,78,117,138,65,100,0,78,86,255,250,1,8,46,46,0,182,208,104,62,128,47,12,169,254,100,83,105,122]))}#At(e,t,a,n,s,r,i,o,l,f,c,h){if(t){const e=Math.min(127,t.length);t=t.subarray(0,e)}else t=[];const u=6===e?new PDF20:new PDF17;return u.checkUserPassword(t,o,i)?u.getUserKey(t,l,c):t.length&&u.checkOwnerPassword(t,n,r,a)?u.getOwnerKey(t,s,r,f):null}#xt(e,t,a,n,s,r,i,o){const l=40+a.length+e.length,f=new Uint8Array(l);let c,h,u=0;if(t){h=Math.min(32,t.length);for(;u>8&255;f[u++]=s>>16&255;f[u++]=s>>>24&255;f.set(e,u);u+=e.length;if(r>=4&&!o){f.fill(255,u,u+4);u+=4}let m=calculateMD5(f,0,u);const p=i>>3;if(r>=3)for(c=0;c<50;++c)m=calculateMD5(m,0,p);const d=m.subarray(0,p);let g,b;if(r>=3){u=0;f.set(CipherTransformFactory._defaultPasswordBytes,u);u+=32;f.set(e,u);u+=e.length;g=new ARCFourCipher(d);b=g.encryptBlock(calculateMD5(f,0,u));h=d.length;const t=new Uint8Array(h);for(c=1;c<=19;++c){for(let e=0;en[t]===e)?d:null}#Ct(e,t,a,n){const s=new Uint8Array(32);let r=0;const i=Math.min(32,e.length);for(;r>3;if(a>=3)for(o=0;o<50;++o)l=calculateMD5(l,0,l.length);let c,h;if(a>=3){h=t;const e=new Uint8Array(f);for(o=19;o>=0;o--){for(let t=0;t>8&255;r[i++]=e>>16&255;r[i++]=255&t;r[i++]=t>>8&255;if(n){r[i++]=115;r[i++]=65;r[i++]=108;r[i++]=84}return calculateMD5(r,0,i).subarray(0,Math.min(s+5,16))}#Ft(e,t,a,n,s){if(!(t instanceof Name))throw new FormatError("Invalid crypt filter name.");const r=this,i=e.get(t.name),o=i?.get("CFM");if(!o||"None"===o.name)return function(){return new NullCipher};if("V2"===o.name)return function(){return new ARCFourCipher(r.#It(a,n,s,!1))};if("AESV2"===o.name)return function(){return new AES128Cipher(r.#It(a,n,s,!0))};if("AESV3"===o.name)return function(){return new AES256Cipher(s)};throw new FormatError("Unknown crypto method")}constructor(e,t,a){const n=e.get("Filter");if(!isName(n,"Standard"))throw new FormatError("unknown encryption method");this.filterName=n.name;this.dict=e;const s=e.get("V");if(!Number.isInteger(s)||1!==s&&2!==s&&4!==s&&5!==s)throw new FormatError("unsupported encryption algorithm");this.algorithm=s;let r=e.get("Length");if(!r)if(s<=3)r=40;else{const t=e.get("CF"),a=e.get("StmF");if(t instanceof Dict&&a instanceof Name){t.suppressEncryption=!0;const e=t.get(a.name);r=e?.get("Length")||128;r<40&&(r<<=3)}}if(!Number.isInteger(r)||r<40||r%8!=0)throw new FormatError("invalid key length");const i=stringToBytes(e.get("O")),o=stringToBytes(e.get("U")),l=i.subarray(0,32),f=o.subarray(0,32),c=e.get("P"),h=e.get("R"),u=(4===s||5===s)&&!1!==e.get("EncryptMetadata");this.encryptMetadata=u;const m=stringToBytes(t);let p,d;if(a){if(6===h)try{a=utf8StringToString(a)}catch{warn("CipherTransformFactory: Unable to convert UTF8 encoded password.")}p=stringToBytes(a)}if(5!==s)d=this.#xt(m,p,l,f,c,h,r,u);else{const t=i.subarray(32,40),a=i.subarray(40,48),n=o.subarray(0,48),s=o.subarray(32,40),r=o.subarray(40,48),c=stringToBytes(e.get("OE")),u=stringToBytes(e.get("UE")),m=stringToBytes(e.get("Perms"));d=this.#At(h,p,l,t,a,n,f,s,r,c,u,m)}if(!d){if(!a)throw new PasswordException("No password given",Zt);const e=this.#Ct(p,l,h,r);d=this.#xt(m,e,l,f,c,h,r,u)}if(!d)throw new PasswordException("Incorrect Password",ea);if(4===s&&d.length<16){this.encryptionKey=new Uint8Array(16);this.encryptionKey.set(d)}else this.encryptionKey=d;if(s>=4){const t=e.get("CF");t instanceof Dict&&(t.suppressEncryption=!0);this.cf=t;this.stmf=e.get("StmF")||Name.get("Identity");this.strf=e.get("StrF")||Name.get("Identity");this.eff=e.get("EFF")||this.stmf}}createCipherTransform(e,t){if(4===this.algorithm||5===this.algorithm)return new CipherTransform(this.#Ft(this.cf,this.strf,e,t,this.encryptionKey),this.#Ft(this.cf,this.stmf,e,t,this.encryptionKey));const a=this.#It(e,t,this.encryptionKey,!1),cipherConstructor=function(){return new ARCFourCipher(a)};return new CipherTransform(cipherConstructor,cipherConstructor)}}class XRef{constructor(e,t){this.stream=e;this.pdfManager=t;this.entries=[];this._xrefStms=new Set;this._cacheMap=new Map;this._pendingRefs=new RefSet;this._newPersistentRefNum=null;this._newTemporaryRefNum=null;this._persistentRefsCache=null}getNewPersistentRef(e){null===this._newPersistentRefNum&&(this._newPersistentRefNum=this.entries.length||1);const t=this._newPersistentRefNum++;this._cacheMap.set(t,e);return Ref.get(t,0)}getNewTemporaryRef(){if(null===this._newTemporaryRefNum){this._newTemporaryRefNum=this.entries.length||1;if(this._newPersistentRefNum){this._persistentRefsCache=new Map;for(let e=this._newTemporaryRefNum;e0;){const[i,o]=r;if(!Number.isInteger(i)||!Number.isInteger(o))throw new FormatError(`Invalid XRef range fields: ${i}, ${o}`);if(!Number.isInteger(a)||!Number.isInteger(n)||!Number.isInteger(s))throw new FormatError(`Invalid XRef entry fields length: ${i}, ${o}`);for(let r=t.entryNum;r=e.length);){a+=String.fromCharCode(n);n=e[t]}return a}function skipUntil(e,t,a){const n=a.length,s=e.length;let r=0;for(;t=n)break;t++;r++}return r}const e=/\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/g,t=/\b(startxref|\d+\s+\d+\s+obj)\b/g,a=/^(\d+)\s+(\d+)\s+obj\b/,n=new Uint8Array([116,114,97,105,108,101,114]),s=new Uint8Array([115,116,97,114,116,120,114,101,102]),r=new Uint8Array([47,88,82,101,102]);this.entries.length=0;this._cacheMap.clear();const i=this.stream;i.pos=0;const o=i.getBytes(),l=bytesToString(o),f=o.length;let c=i.start;const h=[],u=[];for(;c=f)break;m=o[c]}while(10!==m&&13!==m);continue}const p=readToken(o,c);let d;if(p.startsWith("xref")&&(4===p.length||/\s/.test(p[4]))){c+=skipUntil(o,c,n);h.push(c);c+=skipUntil(o,c,s)}else if(d=a.exec(p)){const t=0|d[1],a=0|d[2],n=c+p.length;let s,h=!1;if(this.entries[t]){if(this.entries[t].gen===a)try{new Parser({lexer:new Lexer(i.makeSubStream(n))}).getObj();h=!0}catch(e){e instanceof ParserEOFException?warn(`indexObjects -- checking object (${p}): "${e}".`):h=!0}}else h=!0;h&&(this.entries[t]={offset:c-i.start,gen:a,uncompressed:!0});e.lastIndex=n;const m=e.exec(l);if(m){s=e.lastIndex+1-c;if("endobj"!==m[1]){warn(`indexObjects: Found "${m[1]}" inside of another "obj", caused by missing "endobj" -- trying to recover.`);s-=m[1].length+1}}else s=f-c;const g=o.subarray(c,c+s),b=skipUntil(g,0,r);if(b0&&t[3]-t[1]>0)return t;warn(`Empty, or invalid, /${e} entry.`)}return null}get mediaBox(){return shadow(this,"mediaBox",this.getBoundingBox("MediaBox")||jf)}get cropBox(){return shadow(this,"cropBox",this.getBoundingBox("CropBox")||this.mediaBox)}get userUnit(){const e=this.pageDict.get("UserUnit");return shadow(this,"userUnit","number"==typeof e&&e>0?e:1)}get view(){const{cropBox:e,mediaBox:t}=this;if(e!==t&&!isArrayEqual(e,t)){const a=Util.intersect(e,t);if(a&&a[2]-a[0]>0&&a[3]-a[1]>0)return shadow(this,"view",a);warn("Empty /CropBox and /MediaBox intersection.")}return shadow(this,"view",t)}get rotate(){let e=this.#Rt("Rotate")||0;e%90!=0?e=0:e>=360?e%=360:e<0&&(e=(e%360+360)%360);return shadow(this,"rotate",e)}#Ht(e,t){if(!this.evaluatorOptions.ignoreErrors)throw e;warn(`getContentStream - ignoring sub-stream (${t}): "${e}".`)}async getContentStream(){const e=await this.pdfManager.ensure(this,"content");if(e instanceof BaseStream&&!e.isImageStream){if(e.isAsync){const t=await e.asyncGetBytes();if(t)return new Stream(t,0,t.length,e.dict)}return e}if(Array.isArray(e)){const t=[];for(let a=0,n=e.length;a{t&&(e[a]=new Stream(t,0,t.length,n.dict))}))}t.length>0&&await Promise.all(t);return new StreamsSequenceStream(e,this.#Ht.bind(this))}return new NullStream}get xfaData(){return shadow(this,"xfaData",this.xfaFactory?{bbox:this.xfaFactory.getBoundingBox(this.pageIndex)}:null)}async#Bt(e,t,a){const n=[];for(const s of e)if(s.id){const e=Ref.fromString(s.id);if(!e){warn(`A non-linked annotation cannot be modified: ${s.id}`);continue}if(s.deleted){t.put(e,e);if(s.popupRef){const e=Ref.fromString(s.popupRef);e&&t.put(e,e)}continue}if(s.popup?.deleted){const e=Ref.fromString(s.popupRef);e&&t.put(e,e)}a?.put(e);s.ref=e;n.push(this.xref.fetchAsync(e).then(e=>{e instanceof Dict&&(s.oldAnnotation=e.clone())},()=>{warn(`Cannot fetch \`oldAnnotation\` for: ${e}.`)}));delete s.id}await Promise.all(n)}async saveNewAnnotations(e,t,a,n,s){if(this.xfaFactory)throw new Error("XFA: Cannot save new annotations.");const r=this.#Ot(e),i=new RefSetCache,o=new RefSet;await this.#Bt(a,i,o);const l=this.pageDict,f=this.annotations.filter(e=>!(e instanceof Ref&&i.has(e))),c=await AnnotationFactory.saveNewAnnotations(r,this.xref,t,a,n,s);for(const{ref:e}of c.annotations)e instanceof Ref&&!o.has(e)&&f.push(e);const h=l.clone();h.set("Annots",f);s.put(this.ref,{data:h});for(const e of i)s.put(e,{data:null})}async save(e,t,a,n){const s=this.#Ot(e),r=await this._parsedAnnotations,i=[];for(const e of r)i.push(e.save(s,t,a,n).catch(function(e){warn(`save - ignoring annotation data during "${t.name}" task: "${e}".`);return null}));return Promise.all(i)}async loadResources(e){await(this.#Tt??=this.pdfManager.ensure(this,"resources"));await ObjectLoader.load(this.resources,e,this.xref)}async#Dt(e,t){const a=e?.get("Resources");if(!(a instanceof Dict&&a.size))return this.resources;await ObjectLoader.load(a,t,this.xref);return Dict.merge({xref:this.xref,dictArray:[a,this.resources],mergeSubDicts:!0})}async getOperatorList({handler:e,sink:t,task:a,intent:n,cacheKey:s,pageIndex:r=this.pageIndex,annotationStorage:i=null,modifiedIds:c=null}){const p=this.getContentStream(),g=this.loadResources(da),b=this.#Ot(e,r),w=this.xfaFactory?null:getNewAnnotationsMap(i),j=w?.get(this.pageIndex);let k=Promise.resolve(null),y=null;if(j){const e=this.pdfManager.ensureDoc("annotationGlobals");let t;const n=new Set;for(const{bitmapId:e,bitmap:t}of j)!e||t||n.has(e)||n.add(e);const{isOffscreenCanvasSupported:s}=this.evaluatorOptions;if(n.size>0){const e=j.slice();for(const[t,a]of i)t.startsWith(d)&&a.bitmap&&n.has(a.bitmapId)&&e.push(a);t=AnnotationFactory.generateImages(e,this.xref,s)}else t=AnnotationFactory.generateImages(j,this.xref,s);y=new RefSet;k=Promise.all([e,this.#Bt(j,y,null)]).then(([e])=>e?AnnotationFactory.printNewAnnotations(e,b,a,j,t):null)}const q=Promise.all([p,g]).then(async([i])=>{const o=await this.#Dt(i.dict,da),l=new OperatorList(n,t);e.send("StartRenderPage",{transparency:b.hasBlendModes(o,this.nonBlendModesSet),pageIndex:r,cacheKey:s});await b.getOperatorList({stream:i,task:a,resources:o,operatorList:l});return l});let[v,S,x]=await Promise.all([q,this._parsedAnnotations,k]);if(x){S=S.filter(e=>!(e.ref&&y.has(e.ref)));for(let e=0,t=x.length;ee.ref&&isRefsEqual(e.ref,a.refToReplace));if(n>=0){S.splice(n,1,a);x.splice(e--,1);t--}}}S=S.concat(x)}if(0===S.length||n&u){v.flush(!0);return{length:v.totalLength}}const C=!!(n&h),F=!!(n&m),T=!!(n&o),O=!!(n&l),R=!!(n&f),H=[];for(const e of S)(T||O&&e.mustBeViewed(i,C)&&e.mustBeViewedWhenEditing(F,c)||R&&e.mustBePrinted(i))&&H.push(e.getOperatorList(b,a,n,i).catch(function(e){warn(`getOperatorList - ignoring annotation data during "${a.name}" task: "${e}".`);return{opList:null,separateForm:!1,separateCanvas:!1}}));const D=await Promise.all(H);let M=!1,N=!1;for(const{opList:e,separateForm:t,separateCanvas:a}of D){v.addOpList(e);M||=t;N||=a}v.flush(!0,{form:M,canvas:N});return{length:v.totalLength}}async extractTextContent({handler:e,task:t,includeMarkedContent:a,disableNormalization:n,sink:s,intersector:r=null}){const i=this.getContentStream(),o=this.loadResources(ga),l=this.pdfManager.ensureCatalog("lang"),[f,,c]=await Promise.all([i,o,l]),h=await this.#Dt(f.dict,ga);return this.#Ot(e).getTextContent({stream:f,task:t,resources:h,includeMarkedContent:a,disableNormalization:n,sink:s,viewBox:this.view,lang:c,intersector:r})}async getStructTree(){const e=await this.pdfManager.ensureCatalog("structTreeRoot");if(!e)return null;await this._parsedAnnotations;try{const t=await this.pdfManager.ensure(this,"_parseStructTree",[e]);return await this.pdfManager.ensure(t,"serializable")}catch(e){warn(`getStructTree: "${e}".`);return null}}_parseStructTree(e){const t=new StructTreePage(e,this.pageDict);t.parse(this.ref);return t}async getAnnotationsData(e,t,a){const n=await this._parsedAnnotations;if(0===n.length)return n;const s=[],r=[];let i;const c=!!(a&o),h=!!(a&l),u=!!(a&f),m=[];for(const a of n){const n=c||h&&a.viewable;(n||u&&a.printable)&&s.push(a.data);if(a.hasTextContent&&n){i??=this.#Ot(e);r.push(a.extractTextContent(i,t,[-1/0,-1/0,1/0,1/0]).catch(function(e){warn(`getAnnotationsData - ignoring textContent during "${t.name}" task: "${e}".`)}))}else a.overlaysTextContent&&n&&m.push(a)}if(m.length>0){const a=new Intersector(m);r.push(this.extractTextContent({handler:e,task:t,includeMarkedContent:!1,disableNormalization:!1,sink:null,viewBox:this.view,lang:null,intersector:a}).then(()=>{a.setText()}))}await Promise.all(r);return s}get annotations(){const e=this.#Rt("Annots");return shadow(this,"annotations",Array.isArray(e)?e:[])}get _parsedAnnotations(){return shadow(this,"_parsedAnnotations",this.pdfManager.ensure(this,"annotations").then(async e=>{if(0===e.length)return e;const[t,a]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureDoc("fieldObjects")]);if(!t)return[];const n=a?.orphanFields,s=[];for(const a of e)s.push(AnnotationFactory.create(this.xref,a,t,this._localIdFactory,!1,n,null,this.ref).catch(function(e){warn(`_parsedAnnotations: "${e}".`);return null}));const r=[];let i,o;for(const e of await Promise.all(s))e&&(e instanceof WidgetAnnotation?(o||=[]).push(e):e instanceof PopupAnnotation?(i||=[]).push(e):r.push(e));o&&r.push(...o);i&&r.push(...i);return r}))}get jsActions(){return shadow(this,"jsActions",collectActions(this.xref,this.pageDict,le))}async collectAnnotationsByType(e,t,a,n,s){const{pageIndex:r}=this;if(Object.hasOwn(this,"_parsedAnnotations")){const e=await this._parsedAnnotations;for(const{data:t}of e)if(!a||a.has(t.annotationType)){t.pageIndex=r;n.push(Promise.resolve(t))}return}const i=await this.pdfManager.ensure(this,"annotations");let o;for(const l of i)n.push(AnnotationFactory.create(this.xref,l,s,this._localIdFactory,!1,null,a,this.ref).then(async a=>{if(!a)return null;a.data.pageIndex=r;if(a.hasTextContent&&a.viewable){o??=this.#Ot(e);await a.extractTextContent(o,t,[-1/0,-1/0,1/0,1/0])}return a.data}).catch(function(e){warn(`collectAnnotationsByType: "${e}".`);return null}))}}const kf=new Uint8Array([37,80,68,70,45]),yf=new Uint8Array([115,116,97,114,116,120,114,101,102]),qf=new Uint8Array([101,110,100,111,98,106]);function find(e,t,a=1024,n=!1){const s=t.length,r=e.peekBytes(a),i=r.length-s;if(i<=0)return!1;if(n){const a=s-1;let n=r.length-1;for(;n>=a;){let i=0;for(;i=s){e.pos+=n-a;return!0}n--}}else{let a=0;for(;a<=i;){let n=0;for(;n=s){e.pos+=a;return!0}a++}}return!1}class PDFDocument{#Mt=new Map;#Nt=null;constructor(e,t){if(t.length<=0)throw new InvalidPDFException("The PDF file is empty, i.e. its size is zero bytes.");this.pdfManager=e;this.stream=t;this.xref=new XRef(t,e);const a={font:0};this._globalIdFactory=class{static getDocId(){return`g_${e.docId}`}static createFontId(){return"f"+ ++a.font}static createObjId(){unreachable("Abstract method `createObjId` called.")}static getPageObjId(){unreachable("Abstract method `getPageObjId` called.")}}}parse(e){this.xref.parse(e);this.catalog=new Catalog(this.pdfManager,this.xref)}get linearization(){let e=null;try{e=Linearization.create(this.stream)}catch(e){if(e instanceof MissingDataException)throw e;info(e)}return shadow(this,"linearization",e)}get startXRef(){const e=this.stream;let t=0;if(this.linearization){e.reset();if(find(e,qf)){e.skip(6);let a=e.peekByte();for(;isWhiteSpace(a);){e.pos++;a=e.peekByte()}t=e.pos-e.start}}else{const a=1024,n=yf.length;let s=!1,r=e.end;for(;!s&&r>0;){r-=a-n;r<0&&(r=0);e.pos=r;s=find(e,yf,a,!0)}if(s){e.skip(9);let a;do{a=e.getByte()}while(isWhiteSpace(a));let n="";for(;a>=32&&a<=57;){n+=String.fromCharCode(a);a=e.getByte()}t=parseInt(n,10);isNaN(t)&&(t=0)}}return shadow(this,"startXRef",t)}checkHeader(){const e=this.stream;e.reset();if(!find(e,kf))return;e.moveStart();e.skip(kf.length);let t,a="";for(;(t=e.getByte())>32&&a.length<7;)a+=String.fromCharCode(t);ua.test(a)?this.#Nt=a:warn(`Invalid PDF header version: ${a}`)}parseStartXRef(){this.xref.setStartXRef(this.startXRef)}get numPages(){let e=0;e=this.catalog.hasActualNumPages?this.catalog.numPages:this.xfaFactory?this.xfaFactory.getNumPages():this.linearization?this.linearization.numPages:this.catalog.numPages;return shadow(this,"numPages",e)}#Pt(e,t=0){return!!Array.isArray(e)&&e.every(e=>{if(!((e=this.xref.fetchIfRef(e))instanceof Dict))return!1;if(e.has("Kids")){if(++t>10){warn("#hasOnlyDocumentSignatures: maximum recursion depth reached");return!1}return this.#Pt(e.get("Kids"),t)}const a=isName(e.get("FT"),"Sig"),n=e.get("Rect"),s=Array.isArray(n)&&n.every(e=>0===e);return a&&s})}get _xfaStreams(){const{acroForm:e}=this.catalog;if(!e)return null;const t=e.get("XFA"),a=new Map(["xdp:xdp","template","datasets","config","connectionSet","localeSet","stylesheet","/xdp:xdp"].map(e=>[e,null]));if(t instanceof BaseStream&&!t.isEmpty){a.set("xdp:xdp",t);return a}if(!Array.isArray(t)||0===t.length)return null;for(let e=0,n=t.length;ef.handleSetFont(n,[Name.get(e),1],null,c,t,u,a,s).catch(e=>{warn(`loadXfaFonts: "${e}".`);return null}),m=[];for(const[e,t]of s){const a=t.get("FontDescriptor");if(!(a instanceof Dict))continue;let n=a.get("FontFamily");n=n.replaceAll(/[ ]+(\d)/g,"$1");const s={fontFamily:n,fontWeight:a.get("FontWeight"),italicAngle:-a.get("ItalicAngle")};validateCSSFont(s)&&m.push(parseFont(e,null,s))}await Promise.all(m);const p=this.xfaFactory.setFonts(h);if(!p)return;r.ignoreErrors=!0;m.length=0;h.length=0;const d=new Set;for(const e of p)getXfaFontName(`${e}-Regular`)||d.add(e);d.size&&p.push("PdfJS-Fallback");for(const e of p)if(!d.has(e))for(const t of[{name:"Regular",fontWeight:400,italicAngle:0},{name:"Bold",fontWeight:700,italicAngle:0},{name:"Italic",fontWeight:400,italicAngle:12},{name:"BoldItalic",fontWeight:700,italicAngle:12}]){const a=`${e}-${t.name}`;m.push(parseFont(a,getXfaFontDict(a),{fontFamily:e,fontWeight:t.fontWeight,italicAngle:t.italicAngle}))}await Promise.all(m);this.xfaFactory.appendFonts(h,d)}loadXfaResources(e,t){return Promise.all([this.#_t(e,t).catch(()=>{}),this.#Et()])}serializeXfaData(e){return this.xfaFactory?this.xfaFactory.serializeData(e):null}get version(){return this.catalog.version||this.#Nt}get formInfo(){const e={hasFields:!1,hasAcroForm:!1,hasXfa:!1,hasSignatures:!1},{acroForm:t}=this.catalog;if(!t)return shadow(this,"formInfo",e);try{const a=t.get("Fields"),n=Array.isArray(a)&&a.length>0;e.hasFields=n;const s=t.get("XFA");e.hasXfa=Array.isArray(s)&&s.length>0||s instanceof BaseStream&&!s.isEmpty;const r=!!(1&t.get("SigFlags")),i=r&&this.#Pt(a);e.hasAcroForm=n&&!i;e.hasSignatures=r}catch(e){if(e instanceof MissingDataException)throw e;warn(`Cannot fetch form information: "${e}".`)}return shadow(this,"formInfo",e)}get documentInfo(){const{catalog:e,formInfo:t,xref:a}=this,n={PDFFormatVersion:this.version,Language:e.lang,EncryptFilterName:a.encrypt?.filterName??null,IsLinearized:!!this.linearization,IsAcroFormPresent:t.hasAcroForm,IsXFAPresent:t.hasXfa,IsCollectionPresent:!!e.collection,IsSignaturesPresent:t.hasSignatures};let s;try{s=a.trailer.get("Info")}catch(e){if(e instanceof MissingDataException)throw e;info("The document information dictionary is invalid.")}if(!(s instanceof Dict))return shadow(this,"documentInfo",n);for(const[e,t]of s){switch(e){case"Title":case"Author":case"Subject":case"Keywords":case"Creator":case"Producer":case"CreationDate":case"ModDate":if("string"==typeof t){n[e]=stringToPDFString(t);continue}break;case"Trapped":if(t instanceof Name){n[e]=t;continue}break;default:let a;switch(typeof t){case"string":a=stringToPDFString(t);break;case"number":case"boolean":a=t;break;default:t instanceof Name&&(a=t)}if(void 0===a){warn(`Bad value, for custom key "${e}", in Info: ${t}.`);continue}n.Custom??=Object.create(null);n.Custom[e]=a;continue}warn(`Bad value, for key "${e}", in Info: ${t}.`)}return shadow(this,"documentInfo",n)}get fingerprints(){const e="\0".repeat(16);function validate(t){return"string"==typeof t&&16===t.length&&t!==e}const t=this.xref.trailer.get("ID");let a,n;if(Array.isArray(t)&&validate(t[0])){a=stringToBytes(t[0]);t[1]!==t[0]&&validate(t[1])&&(n=stringToBytes(t[1]))}else a=calculateMD5(this.stream.getByteRange(0,1024),0,1024);return shadow(this,"fingerprints",[a.toHex(),n?.toHex()??null])}async#zt(e){const{catalog:t,linearization:a,xref:n}=this,s=Ref.get(a.objectNumberFirst,0);try{const e=await n.fetchAsync(s);if(e instanceof Dict){let a=e.getRaw("Type");a instanceof Ref&&(a=await n.fetchAsync(a));if(isName(a,"Page")||!e.has("Type")&&!e.has("Kids")&&e.has("Contents")){t.pageKidsCountCache.has(s)||t.pageKidsCountCache.put(s,1);t.pageIndexCache.has(s)||t.pageIndexCache.put(s,0);return[e,s]}}throw new FormatError("The Linearization dictionary doesn't point to a valid Page dictionary.")}catch(a){warn(`_getLinearizationPage: "${a.message}".`);return t.getPageDict(e)}}getPage(e){const t=this.#Mt.get(e);if(t)return t;const{catalog:a,linearization:n,xfaFactory:s}=this;let r;r=s?Promise.resolve([Dict.empty,null]):n?.pageFirst===e?this.#zt(e):a.getPageDict(e);r=r.then(([t,n])=>new Page({pdfManager:this.pdfManager,xref:this.xref,pageIndex:e,pageDict:t,ref:n,globalIdFactory:this._globalIdFactory,fontCache:a.fontCache,builtInCMapCache:a.builtInCMapCache,standardFontDataCache:a.standardFontDataCache,globalColorSpaceCache:a.globalColorSpaceCache,globalImageCache:a.globalImageCache,systemFontCache:a.systemFontCache,nonBlendModesSet:a.nonBlendModesSet,xfaFactory:s}));this.#Mt.set(e,r);return r}async checkFirstPage(e=!1){if(!e)try{await this.getPage(0)}catch(e){if(e instanceof XRefEntryException){this.#Mt.delete(0);await this.cleanup();throw new XRefParseException}}}async checkLastPage(e=!1){const{catalog:t,pdfManager:a}=this;t.setActualNumPages();let n;try{await Promise.all([a.ensureDoc("xfaFactory"),a.ensureDoc("linearization"),a.ensureCatalog("numPages")]);if(this.xfaFactory)return;n=this.linearization?this.linearization.numPages:t.numPages;if(!Number.isInteger(n))throw new FormatError("Page count is not an integer.");if(n<=1)return;await this.getPage(n-1)}catch(s){this.#Mt.delete(n-1);await this.cleanup();if(s instanceof XRefEntryException&&!e)throw new XRefParseException;warn(`checkLastPage - invalid /Pages tree /Count: ${n}.`);let r;try{r=await t.getAllPageDicts(e)}catch(a){if(a instanceof XRefEntryException&&!e)throw new XRefParseException;t.setActualNumPages(1);return}for(const[e,[n,s]]of r){let r;if(n instanceof Error){r=Promise.reject(n);r.catch(()=>{})}else r=Promise.resolve(new Page({pdfManager:a,xref:this.xref,pageIndex:e,pageDict:n,ref:s,globalIdFactory:this._globalIdFactory,fontCache:t.fontCache,builtInCMapCache:t.builtInCMapCache,standardFontDataCache:t.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:t.globalImageCache,systemFontCache:t.systemFontCache,nonBlendModesSet:t.nonBlendModesSet,xfaFactory:null}));this.#Mt.set(e,r)}t.setActualNumPages(r.size)}}async fontFallback(e,t){const{catalog:a,pdfManager:n}=this;for(const s of await Promise.all(a.fontCache))if(s.loadedName===e){s.fallback(t,n.evaluatorOptions);return}}async cleanup(e=!1){return this.catalog?this.catalog.cleanup(e):clearGlobalCaches()}async#Lt(e,t,a,n,s,r,i){const{xref:o}=this;if(!(a instanceof Ref)||r.has(a))return;r.put(a);const l=await o.fetchAsync(a);if(!(l instanceof Dict))return;let f=await l.getAsync("Subtype");f=f instanceof Name?f.name:null;if("Link"===f)return;if(l.has("T")){const t=stringToPDFString(await l.getAsync("T"));e=""===e?t:`${e}.${t}`}else{let a=l;for(;;){a=a.getRaw("Parent")||t;if(a instanceof Ref){if(r.has(a))break;a=await o.fetchAsync(a)}if(!(a instanceof Dict))break;if(a.has("T")){const t=stringToPDFString(await a.getAsync("T"));e=""===e?t:`${e}.${t}`;break}}}t&&!l.has("Parent")&&isName(l.get("Subtype"),"Widget")&&i.put(a,t);n.getOrInsertComputed(e,makeArr).push(AnnotationFactory.create(o,a,s,null,!0,i,null,null).then(e=>e?.getFieldObject()).catch(function(e){warn(`#collectFieldObjects: "${e}".`);return null}));if(!l.has("Kids"))return;const c=await l.getAsync("Kids");if(Array.isArray(c))for(const t of c)await this.#Lt(e,a,t,n,s,r,i)}get fieldObjects(){return shadow(this,"fieldObjects",this.pdfManager.ensureDoc("formInfo").then(async e=>{if(!e.hasFields)return null;const t=await this.annotationGlobals;if(!t)return null;const{acroForm:a}=t,n=new RefSet,s=Object.create(null),r=new Map,i=new RefSetCache;for(const e of a.get("Fields"))await this.#Lt("",null,e,r,t,n,i);const o=[];for(const[e,t]of r)o.push(Promise.all(t).then(t=>{(t=t.filter(e=>!!e)).length>0&&(s[e]=t)}));await Promise.all(o);return{allFields:objectSize(s)>0?s:null,orphanFields:i}}))}get hasJSActions(){return shadow(this,"hasJSActions",this.pdfManager.ensureDoc("_parseHasJSActions"))}async _parseHasJSActions(){const[e,t]=await Promise.all([this.pdfManager.ensureCatalog("jsActions"),this.pdfManager.ensureDoc("fieldObjects")]);return!!e||!!t?.allFields&&Object.values(t.allFields).some(e=>e.some(e=>null!==e.actions))}get calculationOrderIds(){const e=this.catalog.acroForm?.get("CO");if(!Array.isArray(e)||0===e.length)return shadow(this,"calculationOrderIds",null);const t=[];for(const a of e)a instanceof Ref&&t.push(a.toString());return shadow(this,"calculationOrderIds",t.length?t:null)}get annotationGlobals(){return shadow(this,"annotationGlobals",AnnotationFactory.createGlobals(this.pdfManager))}async toJSObject(e,t=!0){throw new Error("Not implemented: toJSObject")}}class BasePdfManager{constructor({docBaseUrl:e,docId:t,enableXfa:a,evaluatorOptions:n,handler:s,password:r}){this._docBaseUrl=function parseDocBaseUrl(e){if(e){const t=createValidAbsoluteUrl(e);if(t)return t.href;warn(`Invalid absolute docBaseUrl: "${e}".`)}return null}(e);this._docId=t;this._password=r;this.enableXfa=a;n.isOffscreenCanvasSupported&&=FeatureTest.isOffscreenCanvasSupported;n.isImageDecoderSupported&&=FeatureTest.isImageDecoderSupported;this.evaluatorOptions=Object.freeze(n);ImageResizer.setOptions(n);JpegStream.setOptions(n);OperatorList.setOptions(n);const i={...n,handler:s};JpxImage.setOptions(i);IccColorSpace.setOptions(i);CmykICCBasedCS.setOptions(i);JBig2CCITTFaxImage.setOptions(i);PDFFunctionFactory.setOptions(i);Pattern.setOptions(i)}get docId(){return this._docId}get password(){return this._password}get docBaseUrl(){return this._docBaseUrl}ensureDoc(e,t){return this.ensure(this.pdfDocument,e,t)}ensureXRef(e,t){return this.ensure(this.pdfDocument.xref,e,t)}ensureCatalog(e,t){return this.ensure(this.pdfDocument.catalog,e,t)}getPage(e){return this.pdfDocument.getPage(e)}fontFallback(e,t){return this.pdfDocument.fontFallback(e,t)}cleanup(e=!1){return this.pdfDocument.cleanup(e)}async ensure(e,t,a){unreachable("Abstract method `ensure` called")}requestRange(e,t){unreachable("Abstract method `requestRange` called")}requestLoadedStream(e=!1){unreachable("Abstract method `requestLoadedStream` called")}sendProgressiveData(e){unreachable("Abstract method `sendProgressiveData` called")}updatePassword(e){this._password=e}terminate(e){unreachable("Abstract method `terminate` called")}}class LocalPdfManager extends BasePdfManager{constructor(e){super(e);const t=new Stream(e.source);this.pdfDocument=new PDFDocument(this,t);this._loadedStreamPromise=Promise.resolve(t)}async ensure(e,t,a){const n=e[t];return"function"==typeof n?n.apply(e,a):n}requestRange(e,t){return Promise.resolve()}requestLoadedStream(e=!1){return this._loadedStreamPromise}terminate(e){}}class NetworkPdfManager extends BasePdfManager{constructor(e){super(e);this.streamManager=new ChunkedStreamManager(e.source,{msgHandler:e.handler,length:e.length,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize});this.pdfDocument=new PDFDocument(this,this.streamManager.getStream())}async ensure(e,t,a){try{const n=e[t];return"function"==typeof n?await n.apply(e,a):n}catch(n){if(!(n instanceof MissingDataException))throw n;await this.requestRange(n.begin,n.end);return this.ensure(e,t,a)}}requestRange(e,t){return this.streamManager.requestRange(e,t)}requestLoadedStream(e=!1){return this.streamManager.requestAllChunks(e)}sendProgressiveData(e){this.streamManager.onReceiveData({chunk:e})}terminate(e){this.streamManager.abort(e)}}const vf=1,Sf=2,Af=1,xf=2,Cf=3,If=4,Ff=5,Tf=6,Of=7,Rf=8;function onFn(){}function wrapReason(e){if(e instanceof AbortException||e instanceof InvalidPDFException||e instanceof PasswordException||e instanceof ResponseException||e instanceof UnknownErrorException)return e;e instanceof Error||"object"==typeof e&&null!==e||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(e.name){case"AbortException":return new AbortException(e.message);case"InvalidPDFException":return new InvalidPDFException(e.message);case"PasswordException":return new PasswordException(e.message,e.code);case"ResponseException":return new ResponseException(e.message,e.status,e.missing);case"UnknownErrorException":return new UnknownErrorException(e.message,e.details)}return new UnknownErrorException(e.message,e.toString())}class MessageHandler{#Ut=new AbortController;constructor(e,t,a){this.sourceName=e;this.targetName=t;this.comObj=a;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);a.addEventListener("message",this.#Xt.bind(this),{signal:this.#Ut.signal})}#Xt({data:e}){if(e.targetName!==this.sourceName)return;if(e.stream){this.#Wt(e);return}if(e.callback){const t=e.callbackId,a=this.callbackCapabilities[t];if(!a)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===vf)a.resolve(e.data);else{if(e.callback!==Sf)throw new Error("Unexpected callback case");a.reject(wrapReason(e.reason))}return}const t=this.actionHandler[e.action];if(!t)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const a=this.sourceName,n=e.sourceName,s=this.comObj;Promise.try(t,e.data).then(function(t){s.postMessage({sourceName:a,targetName:n,callback:vf,callbackId:e.callbackId,data:t})},function(t){s.postMessage({sourceName:a,targetName:n,callback:Sf,callbackId:e.callbackId,reason:wrapReason(t)})});return}e.streamId?this.#Kt(e):t(e.data)}on(e,t){const a=this.actionHandler;if(a[e])throw new Error(`There is already an actionName called "${e}"`);a[e]=t}send(e,t,a){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,data:t},a)}sendWithPromise(e,t,a){const n=this.callbackId++,s=Promise.withResolvers();this.callbackCapabilities[n]=s;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,callbackId:n,data:t},a)}catch(e){s.reject(e)}return s.promise}sendWithStream(e,t,a,n){const s=this.streamId++,r=this.sourceName,i=this.targetName,o=this.comObj;return new ReadableStream({start:a=>{const l=Promise.withResolvers();this.streamControllers[s]={controller:a,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:r,targetName:i,action:e,streamId:s,data:t,desiredSize:a.desiredSize},n);return l.promise},pull:e=>{const t=Promise.withResolvers();this.streamControllers[s].pullCall=t;o.postMessage({sourceName:r,targetName:i,stream:Tf,streamId:s,desiredSize:e.desiredSize});return t.promise},cancel:e=>{assert(e instanceof Error,"cancel must have a valid reason");const t=Promise.withResolvers();this.streamControllers[s].cancelCall=t;this.streamControllers[s].isClosed=!0;o.postMessage({sourceName:r,targetName:i,stream:Af,streamId:s,reason:wrapReason(e)});return t.promise}},a)}#Kt(e){const t=e.streamId,a=this.sourceName,n=e.sourceName,s=this.comObj,r=this,i=this.actionHandler[e.action],o={enqueue(e,r=1,i){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=r;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}s.postMessage({sourceName:a,targetName:n,stream:If,streamId:t,chunk:e},i)},close(){if(!this.isCancelled){this.isCancelled=!0;s.postMessage({sourceName:a,targetName:n,stream:Cf,streamId:t});delete r.streamSinks[t]}},error(e){assert(e instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;s.postMessage({sourceName:a,targetName:n,stream:Ff,streamId:t,reason:wrapReason(e)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:e.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[t]=o;Promise.try(i,e.data,o).then(function(){s.postMessage({sourceName:a,targetName:n,stream:Rf,streamId:t,success:!0})},function(e){s.postMessage({sourceName:a,targetName:n,stream:Rf,streamId:t,reason:wrapReason(e)})})}#Wt(e){const t=e.streamId,a=this.sourceName,n=e.sourceName,s=this.comObj,r=this.streamControllers[t],i=this.streamSinks[t];switch(e.stream){case Rf:e.success?r.startCall.resolve():r.startCall.reject(wrapReason(e.reason));break;case Of:e.success?r.pullCall.resolve():r.pullCall.reject(wrapReason(e.reason));break;case Tf:if(!i){s.postMessage({sourceName:a,targetName:n,stream:Of,streamId:t,success:!0});break}i.desiredSize<=0&&e.desiredSize>0&&i.sinkCapability.resolve();i.desiredSize=e.desiredSize;Promise.try(i.onPull||onFn).then(function(){s.postMessage({sourceName:a,targetName:n,stream:Of,streamId:t,success:!0})},function(e){s.postMessage({sourceName:a,targetName:n,stream:Of,streamId:t,reason:wrapReason(e)})});break;case If:assert(r,"enqueue should have stream controller");if(r.isClosed)break;r.controller.enqueue(e.chunk);break;case Cf:assert(r,"close should have stream controller");if(r.isClosed)break;r.isClosed=!0;r.controller.close();this.#Gt(r,t);break;case Ff:assert(r,"error should have stream controller");r.controller.error(wrapReason(e.reason));this.#Gt(r,t);break;case xf:e.success?r.cancelCall.resolve():r.cancelCall.reject(wrapReason(e.reason));this.#Gt(r,t);break;case Af:if(!i)break;const o=wrapReason(e.reason);Promise.try(i.onCancel||onFn,o).then(function(){s.postMessage({sourceName:a,targetName:n,stream:xf,streamId:t,success:!0})},function(e){s.postMessage({sourceName:a,targetName:n,stream:xf,streamId:t,reason:wrapReason(e)})});i.sinkCapability.reject(o);i.isCancelled=!0;delete this.streamSinks[t];break;default:throw new Error("Unexpected stream case")}}async#Gt(e,t){await Promise.allSettled([e.startCall?.promise,e.pullCall?.promise,e.cancelCall?.promise]);delete this.streamControllers[t]}destroy(){this.#Ut?.abort();this.#Ut=null}}async function writeObject(e,t,a,{encrypt:n=null,encryptRef:s=null}){const r=n&&s!==e?n.createCipherTransform(e.num,e.gen):null;a.push(`${e.num} ${e.gen} obj\n`);await writeValue(t,a,r);a.push("\nendobj\n")}async function writeDict(e,t,a){t.push("<<");for(const[n,s]of e.getRawEntries()){t.push(` /${escapePDFName(n)} `);await writeValue(s,t,a)}t.push(">>")}async function writeValue(e,t,a){if(e instanceof Name)t.push(`/${escapePDFName(e.name)}`);else if(e instanceof Ref)t.push(`${e.num} ${e.gen} R`);else if(Array.isArray(e)||ArrayBuffer.isView(e))await async function writeArray(e,t,a){t.push("[");for(let n=0,s=e.length;n=256&&!o)try{const e=new CompressionStream("deflate"),t=e.writable.getWriter();await t.ready;t.write(n).then(async()=>{await t.ready;await t.close()}).catch(()=>{});n=await new Response(e.readable).bytes();let a,l;if(r){if(!o){a=Array.isArray(r)?[Name.get("FlateDecode"),...r]:[Name.get("FlateDecode"),r];i&&(l=Array.isArray(i)?[null,...i]:[null,i])}}else a=Name.get("FlateDecode");a&&s.set("Filter",a);l&&s.set("DecodeParms",l)}catch(e){info(`writeStream - cannot compress data: "${e}".`)}let l=bytesToString(n);a&&(l=a.encryptString(l));s.set("Length",l.length);await writeDict(s,t,a);t.push(" stream\n",l,"\nendstream")}(e,t,a):null===e?t.push("null"):warn(`Unhandled value in writer: ${typeof e}, please file a bug.`)}function writeInt(e,t,a,n){for(let s=t+a-1;s>a-1;s--){n[s]=255&e;e>>=8}return a+t}function writeString(e,t,a){const n=e.length;for(let s=0;s1&&(r=a.documentElement.searchNode([s.at(-1)],0));r?r.childNodes=Array.isArray(n)?n.map(e=>new SimpleDOMNode("value",e)):[new SimpleDOMNode("#text",n)]:warn(`Node not found for path: ${t}`)}const n=[];a.documentElement.dump(n);return n.join("")}(n.fetchIfRef(t).getString(),a)}const s=new StringStream(e);s.dict=new Dict(n);s.dict.setIfName("Type","EmbeddedFile");a.put(t,{data:s})}function getIndexes(e){const t=[];for(const{ref:a}of e)a.num===t.at(-2)+t.at(-1)?t[t.length-1]+=1:t.push(a.num,1);return t}function computeIDs(e,t,a){if(Array.isArray(t.fileIds)&&t.fileIds.length>0){const n=function computeMD5(e,t){const a=Math.floor(Date.now()/1e3),n=t.filename||"",s=[a.toString(),n,e.toString(),...t.infoMap.values()],r=Math.sumPrecise(s.map(e=>e.length)),i=new Uint8Array(r);let o=0;for(const e of s)o=writeString(e,o,i);return bytesToString(calculateMD5(i,0,i.length))}(e,t);a.set("ID",[t.fileIds[0]||n,n])}}async function incrementalUpdate({originalData:e,xrefInfo:t,changes:a,xref:n=null,hasXfa:s=!1,xfaDatasetsRef:r=null,hasXfaDatasetsEntry:i=!1,needAppearances:o,acroFormRef:l=null,acroForm:f=null,xfaData:c=null,useXrefStream:h=!1}){await async function updateAcroform({xref:e,acroForm:t,acroFormRef:a,hasXfa:n,hasXfaDatasetsEntry:s,xfaDatasetsRef:r,needAppearances:i,changes:o}){!n||s||r||warn("XFA - Cannot save it");if(!i&&(!n||!r||s))return;const l=t.clone();if(n&&!s){const e=t.get("XFA").slice();e.splice(2,0,"datasets");e.splice(3,0,r);l.set("XFA",e)}i&&l.set("NeedAppearances",!0);o.put(a,{data:l})}({xref:n,acroForm:f,acroFormRef:l,hasXfa:s,hasXfaDatasetsEntry:i,xfaDatasetsRef:r,needAppearances:o,changes:a});s&&updateXFA({xfaData:c,xfaDatasetsRef:r,changes:a,xref:n});const u=function getTrailerDict(e,t,a){const n=new Dict(null);n.setIfDefined("Prev",e?.startXRef);const s=e.newRef;if(a){t.put(s,{data:""});n.set("Size",s.num+1);n.setIfName("Type","XRef")}else n.set("Size",s.num);n.setIfDefined("Root",e?.rootRef);n.setIfDefined("Info",e?.infoRef);n.setIfDefined("Encrypt",e?.encryptRef);return n}(t,a,h),m=[],p=await async function writeChanges(e,t,a=[]){const n=[];for(const[s,{data:r,objStreamRef:i,index:o}]of e.items())if(i)n.push({ref:s,data:r,objStreamRef:i,index:o});else if(null!==r&&"string"!=typeof r){await writeObject(s,r,a,t);n.push({ref:s,data:a.join("")});a.length=0}else n.push({ref:s,data:r});return n.sort((e,t)=>e.ref.num-t.ref.num)}(a,n,m);let d=e.length;const g=e.at(-1);if(10!==g&&13!==g){m.push("\n");d+=1}for(const{data:e}of p)null!==e&&m.push(e);await(h?async function getXRefStreamTable(e,t,a,n,s){const r=[];let i=0,o=0;for(const{ref:e,data:n,objStreamRef:s,index:l}of a){let a;i=Math.max(i,t);if(s){a=l;r.push([2,s.num,a])}else if(null!==n){a=Math.min(e.gen,65535);r.push([1,t,a]);t+=n.length}else{a=Math.min(e.gen+1,65535);r.push([0,0,a])}o=Math.max(o,a)}n.set("Index",getIndexes(a));const l=[1,getSizeInBytes(i),getSizeInBytes(o)];n.set("W",l);computeIDs(t,e,n);const f=Math.sumPrecise(l),c=new Uint8Array(f*r.length),h=new Stream(c);h.dict=n;let u=0;for(const[e,t,a]of r){u=writeInt(e,l[0],u,c);u=writeInt(t,l[1],u,c);u=writeInt(a,l[2],u,c)}await writeObject(e.newRef,h,s,{});s.push("startxref\n",t.toString(),"\n%%EOF\n")}(t,d,p,u,m):async function getXRefTable(e,t,a,n,s){s.push("xref\n");const r=getIndexes(a);let i=0;for(const{ref:e,data:n}of a){if(e.num===r[i]){s.push(`${r[i]} ${r[i+1]}\n`);i+=2}if(null!==n){s.push(`${t.toString().padStart(10,"0")} ${Math.min(e.gen,65535).toString().padStart(5,"0")} n\r\n`);t+=n.length}else s.push(`0000000000 ${Math.min(e.gen+1,65535).toString().padStart(5,"0")} f\r\n`)}computeIDs(t,e,n);s.push("trailer\n");await writeDict(n,s,null);s.push("\nstartxref\n",t.toString(),"\n%%EOF\n")}(t,d,p,u,m));const b=e.length+Math.sumPrecise(m.map(e=>e.length)),w=new Uint8Array(b);w.set(e);let j=e.length;for(const e of m)j=writeString(e,j,w);return w}class PageData{constructor(e,t){this.page=e;this.documentData=t;this.annotations=null;this.pointingNamedDestinations=null;t.pagesMap.put(e.ref,this)}}class DocumentData{constructor(e){this.document=e;this.destinations=null;this.pageLabels=null;this.pagesMap=new RefSetCache;this.oldRefMapping=new RefSetCache;this.dedupNamedDestinations=new Map;this.usedNamedDestinations=new Set;this.postponedRefCopies=new RefSetCache;this.usedStructParents=new Set;this.oldStructParentMapping=new Map;this.structTreeRoot=null;this.parentTree=null;this.idTree=null;this.roleMap=null;this.classMap=null;this.namespaces=null;this.structTreeAF=null;this.structTreePronunciationLexicon=[];this.acroForm=null;this.acroFormDefaultAppearance="";this.acroFormDefaultResources=null;this.acroFormQ=0;this.hasSignatureAnnotations=!1;this.fieldToParent=new RefSetCache;this.outline=null;this.embeddedFiles=null}}class XRefWrapper{constructor(e,t){this.entries=e;this._getNewRef=t}fetch(e){return e instanceof Ref?this.entries[e.num]:e}fetchIfRefAsync(e){return Promise.resolve(this.fetch(e))}fetchIfRef(e){return this.fetch(e)}fetchAsync(e){return Promise.resolve(this.fetch(e))}getNewTemporaryRef(){return this._getNewRef()}}class PDFEditor{hasSingleFile=!1;isSingleFile=!1;#Vt=null;currentDocument=null;oldPages=[];newPages=[];xref=[null];xrefWrapper=new XRefWrapper(this.xref,()=>this.newRef);newRefCount=1;namesDict=null;version="1.7";pageLabels=null;namedDestinations=new Map;parentTree=new Map;structTreeKids=[];idTree=new Map;classMap=new Dict;roleMap=new Dict;namespaces=new Map;structTreeAF=[];structTreePronunciationLexicon=[];fields=[];acroFormDefaultAppearance="";acroFormDefaultResources=null;acroFormNeedAppearances=!1;acroFormSigFlags=0;acroFormCalculationOrder=null;acroFormQ=0;outlineItems=null;embeddedFiles=new Map;constructor({useObjectStreams:e=!0,title:t="",author:a=""}={}){[this.rootRef,this.rootDict]=this.newDict;[this.infoRef,this.infoDict]=this.newDict;[this.pagesRef,this.pagesDict]=this.newDict;this.useObjectStreams=e;this.objStreamRefs=e?new Set:null;this.title=t;this.author=a}get newRef(){return Ref.get(this.newRefCount++,0)}get newDict(){const e=this.newRef;return[e,this.xref[e.num]=new Dict]}async#$t(e,t){const a=this.newRef;this.xref[a.num]=await this.#Yt(e,!0,t);return a}cloneDict(e){const t=e.clone();t.xref=this.xrefWrapper;return t}async#Yt(e,t,a){if(e instanceof Ref){const{currentDocument:{oldRefMapping:t}}=this;let n=t.get(e);if(n)return n;const s=e;if("number"==typeof(e=await a.fetchAsync(s)))return e;n=this.newRef;t.put(s,n);this.xref[n.num]=await this.#Yt(e,!0,a);return n}const n=[],{currentDocument:{postponedRefCopies:s}}=this;if(Array.isArray(e)){t&&(e=e.slice());for(let t=0,r=e.length;te[t]=a):n.push(this.#Yt(e[t],!0,a).then(a=>e[t]=a))}await Promise.all(n);return e}let r;if(e instanceof BaseStream){({dict:r}=e=e.getOriginalStream().clone());r.xref=this.xrefWrapper}else if(e instanceof Dict){t&&((e=e.clone()).xref=this.xrefWrapper);r=e}if(r){for(const[e,t]of r.getRawEntries()){const i=t instanceof Ref&&s.get(t);i?i.push(t=>r.set(e,t)):n.push(this.#Yt(t,!0,a).then(t=>r.set(e,t)))}await Promise.all(n)}return e}async#Jt(e,t,a,n,s,r,i,o=new RefSet){const{currentDocument:{pagesMap:l,oldRefMapping:f}}=this,c=t.getRaw("Pg");if(c instanceof Ref&&!l.has(c))return null;let h;const u=h=t.getRaw("K");if(u instanceof Ref){if(o.has(u))return null;h=await a.fetchAsync(u);Array.isArray(h)||(h=[u])}h=Array.isArray(h)?h:[h];const m=[],p=[];for(let t of h){const c=t instanceof Ref?t:null;if(c){if(o.has(c))continue;o.put(c);t=await a.fetchAsync(c)}if("number"==typeof t){m.push(t);continue}if(!(t instanceof Dict))continue;const h=t.getRaw("Pg");if(h instanceof Ref&&!l.has(h))continue;const u=t.get("Type");if(!u||isName(u,"StructElem")){let e=!1;if(c&&n.has(c)){if(!isName(t.get("S"),"Link"))continue;e=!0}const l=await this.#Jt(c,t,a,n,s,r,i,o);if(l){p.push(m.length);m.push(l);c&&f.put(c,l);e&&this.xref[l.num].setIfName("S","Span")}continue}if(isName(u,"OBJR")){if(!c)continue;const t=f.get(c);if(!t)continue;const a=this.xref[t.num].getRaw("Obj");if(a instanceof Ref){const t=this.xref[a.num];if(t instanceof Dict&&!t.has("StructParent")&&e){const a=this.parentTree.size;this.parentTree.set(a,[f,e]);t.set("StructParent",a)}}m.push(t);continue}if(isName(u,"MCR")){const e=await this.#Yt(c||t,!0,a);m.push(e);continue}if(c){const e=await this.#Yt(c,!0,a);m.push(e)}}if(0!==h.length&&0===m.length)return null;const d=this.newRef,g=this.xref[d.num]=this.cloneDict(t);g.delete("ID");g.delete("C");g.delete("K");g.delete("P");g.delete("S");await this.#Yt(g,!1,a);const b=t.get("C");if(b instanceof Name){const e=r.get(b.name);e?g.set("C",Name.get(e)):g.set("C",b)}else if(Array.isArray(b)){const e=[];for(const t of b)if(t instanceof Name){const a=r.get(t.name);a?e.push(Name.get(a)):e.push(t)}g.set("C",e)}const w=t.get("S");if(w instanceof Name){const e=i.get(w.name);e?g.set("S",Name.get(e)):g.set("S",w)}const j=t.get("ID");if("string"==typeof j){const e=stringToPDFString(j,!1),t=s.get(e);t?g.set("ID",stringToAsciiOrUTF16BE(t)):g.set("ID",j)}let k=g.get("A");if(k){Array.isArray(k)||(k=[k]);for(let e of k){e=this.xrefWrapper.fetch(e);if(isName(e.get("O"),"Table")&&e.has("Headers")){const t=this.xrefWrapper.fetch(e.getRaw("Headers"));if(Array.isArray(t))for(let e=0,a=t.length;e1&&g.set("K",m);return d}#Qt({document:e,includePages:t,excludePages:a}){if(!e)return[];let n,s,r,i;for(const e of t||[])Array.isArray(e)?(s||=[]).push(e):(n||=new Set).add(e);for(const e of a||[])Array.isArray(e)?(i||=[]).push(e):(r||=new Set).add(e);const o=[];for(let t=0,a=e.numPages;t=a&&t<=n){e=!0;break}if(e)continue}let e=!1;n&&(e=n.has(t));if(!e&&s)for(const[a,n]of s)if(t>=a&&t<=n){e=!0;break}e||n||s||(e=!0);e&&o.push(t)}return o}#Zt(e){const t=[],a=[];for(let n=0;ne.insertAfter-t.insertAfter);if(a.length>0)for(const t of e){if(!t.document||!t.pageIndices)continue;const e=this.#Qt(t).length;if(t.pageIndices.length0&&e.some(e=>e.document&&e.pageIndices);if(0===t.length&&n){const t=e.slice();let n=-1;for(const t of e)if(t.document&&t.pageIndices)for(const e of t.pageIndices)e>n&&(n=e);let s=0;for(const{i:e,insertAfter:r,count:i}of a){const a=Math.min(Math.max(r,-1)+s,n);for(let e=0;ee<=a)&&(t[e]={...n,pageIndices:n.pageIndices.map(e=>e>a?e+i:e)})}const o=[];for(let e=0;e{if(!e.document||e.pageIndices)return e;const a={...e,pageIndices:r[t]||[]};delete a.insertAfter;return a})}async extractPages(e,t,a,n){e.some(e=>void 0!==e.insertAfter)&&(e=this.#Zt(e));const s=[];let r=0;this.isSingleFile=1===e.length||e.every(t=>t.document===e[0].document);this.hasSingleFile=1===e.length;const i=[];t&&(this.#Vt={handler:a,task:n,newAnnotationsByPage:getNewAnnotationsMap(t),imagesPromises:AnnotationFactory.generateImages(t.values(),this.xrefWrapper,!0)});for(const{document:t,includePages:a,excludePages:n,pageIndices:o}of e){if(!t)continue;o&&(r=-1);const e=new DocumentData(t);i.push(e);s.push(this.#ea(e));let l=0;for(const i of this.#Qt({document:t,includePages:a,excludePages:n})){let a;o&&(a=o[l++]);if(void 0===a)if(-1!==r)a=r++;else for(a=0;void 0!==this.oldPages[a];a++);this.oldPages[a]=null;s.push(t.getPage(i).then(t=>{this.oldPages[a]=new PageData(t,e)}))}}await Promise.all(s);s.length=0;this.#ta(i);this.#aa(i);this.#na();for(const e of this.oldPages)s.push(this.#sa(e));await Promise.all(s);this.#ra();this.#ia(i);for(let e=0,t=this.oldPages.length;ee.destinations=t),t.ensureCatalog("rawPageLabels").then(t=>e.pageLabels=t),t.ensureCatalog("structTreeRoot").then(t=>e.structTreeRoot=t),t.ensureCatalog("acroForm").then(t=>e.acroForm=t),t.ensureCatalog("documentOutlineForEditor").then(t=>e.outline=t),t.ensureCatalog("rawEmbeddedFiles").then(t=>e.embeddedFiles=t)]);const n=e.structTreeRoot;if(n){const t=n.dict,s=t.get("ParentTree");if(s){const t=new NumberTree(s,a);e.parentTree=t.getAll(!0)}const r=t.get("IDTree");if(r){const t=new NameTree(r,a);e.idTree=t.getAll(!0)}e.roleMap=t.get("RoleMap")||null;e.classMap=t.get("ClassMap")||null;let i=t.get("Namespaces")||null;i&&!Array.isArray(i)&&(i=[i]);e.namespaces=i;e.structTreeAF=t.get("AF")||null;e.structTreePronunciationLexicon=t.get("PronunciationLexicon")||null}}async#sa(e){const{page:{xref:t,annotations:a},documentData:{pagesMap:n,destinations:s,usedNamedDestinations:r,fieldToParent:i}}=e;if(!a)return;const o=[];let l=[],f=0,{hasSignatureAnnotations:c}=e.documentData;for(const e of a){const a=f++;o.push(t.fetchIfRefAsync(e).then(async t=>{if(!isName(t.get("Subtype"),"Link")){if(isName(t.get("Subtype"),"Widget")){c||=isName(t.get("FT"),"Sig");const a=t.get("Parent")||null;t.delete("Parent");i.put(e,a)}l[a]=e;return}const o=t.get("A"),f=o instanceof Dict?o.get("D"):t.get("Dest");if(f&&(!Array.isArray(f)||f[0]instanceof Ref&&!n.has(f[0]))){if("string"==typeof f){const t=stringToPDFString(f,!0);if(s.has(t)){l[a]=e;r.add(t)}}}else l[a]=e}))}await Promise.all(o);l=l.filter(e=>!!e);e.annotations=l.length>0?l:null;e.documentData.hasSignatureAnnotations||=c}#ia(e){for(const{postponedRefCopies:t,pagesMap:a}of e)for(const e of a.keys())t.put(e,[])}#la(e){for(const{postponedRefCopies:t,oldRefMapping:a}of e){for(const[e,n]of t.items()){const t=a.get(e);for(const e of n)e(t)}t.clear()}}#ma(e,t,a=new RefSet){if(e instanceof Ref){if(!a.has(e)){a.put(e);this.#ma(this.xref[e.num],t,a)}return}if(Array.isArray(e)){for(const n of e)this.#ma(n,t,a);return}let n;e instanceof BaseStream?({dict:n}=e):e instanceof Dict&&(n=e);if(n){t(n);for(const e of n.getRawValues())this.#ma(e,t,a)}}async#fa(e){let t=0;const{parentTree:a}=this;for(let e=0,n=this.newPages.length;e{const l=e.get("StructParent")??e.get("StructParents");if("number"!=typeof l)return;i.add(l);let f=n.get(l);const c=f instanceof Ref?f:null;if(c){const e=o.fetch(c);Array.isArray(e)&&(f=e)}Array.isArray(f)&&f.every(e=>null===e)&&(f=null);if(!f){e.has("StructParent")?e.delete("StructParent"):e.delete("StructParents");return}let h=r.get(l);if(void 0===h){h=t++;r.set(l,h);a.set(h,[s,f])}e.has("StructParent")?e.set("StructParent",h):e.set("StructParents",h)},c)}const{structTreeKids:n,idTree:s,classMap:r,roleMap:i,namespaces:o,structTreeAF:l,structTreePronunciationLexicon:f}=this;for(const t of e){const{document:{xref:e},oldRefMapping:a,parentTree:c,usedStructParents:h,structTreeRoot:u,idTree:m,classMap:p,roleMap:d,namespaces:g,structTreeAF:b,structTreePronunciationLexicon:w}=t;if(!u)continue;this.currentDocument=t;const j=new RefSet;for(const[e,t]of c||[])!h.has(e)&&t instanceof Ref&&j.put(t);const k=new Map;for(const[e,t]of m||[]){let a=e;if(s.has(e))for(let t=1;;t++){const n=`${e}_${t}`;if(!s.has(n)){k.set(e,n);a=n;break}}s.set(a,t)}const y=new Map;if(p?.size>0)for(let[t,a]of p){a=await this.#Yt(a,!0,e);if(r.has(t))for(let e=1;;e++){const a=`${t}_${e}`;if(!r.has(a)){y.set(t,a);t=a;break}}r.set(t,a)}const q=new Map;if(d?.size>0)for(const[e,t]of d){const a=i.get(e);if(a){if(a!==t)for(let a=1;;a++){const n=`${e}_${a}`;if(!i.has(n)){q.set(e,n);i.set(n,t);break}}}else i.set(e,t)}if(g?.length>0)for(const t of g){const a=await e.fetchIfRefAsync(t);let n=a.get("NS");if(!n||o.has(n))continue;n=stringToPDFString(n,!1);const s=await this.#Yt(a,!0,e);o.set(n,s)}if(b)for(const t of b)l.push(await this.#Yt(t,!0,e));if(w)for(const t of w)f.push(await this.#Yt(t,!0,e));let v=u.dict.get("K");if(v){v=Array.isArray(v)?v:[v];for(let t of v){const a=t instanceof Ref?t:null;if(a&&j.has(a))continue;t=await e.fetchIfRefAsync(t);const s=await this.#Jt(a,t,e,j,k,y,q);s&&n.push(s)}for(const[e,t]of m||[]){const n=t instanceof Ref&&a.get(t),r=k.get(e)||e;n?s.set(r,n):s.delete(r)}}}for(const[e,[t,n]]of a){if(!n){a.delete(e);continue}if(!Array.isArray(n)){const s=t.get(n);void 0===s?a.delete(e):a.set(e,s);continue}const s=n.map(e=>e instanceof Ref&&t.get(e)||null);0===s.length||s.every(e=>null===e)?a.delete(e):a.set(e,s)}this.currentDocument=null}#ta(e){for(const t of e){if(!t.destinations)continue;const{destinations:e,pagesMap:a}=t,n=t.destinations=new Map;for(const[t,s]of Object.entries(e)){const e=s[0],r=e instanceof Ref&&a.get(e);if(r){(r.pointingNamedDestinations||=new Set).add(t);n.set(t,s)}}}}#ra(){const{namedDestinations:e}=this,getUniqueDestinationName=t=>{if(!e.has(t))return t;for(let a=1;;a++){const n=`${t}_${a}`;if(!e.has(n))return n}};for(let t=0,a=this.oldPages.length;t{"string"==typeof n&&e.set(a,t.get(stringToPDFString(n,!0))||n)};for(const t of e){const e=this.xref[t.num];if(!isName(e.get("Subtype"),"Link"))continue;const a=e.get("A");if(a instanceof Dict&&a.has("D")){const e=a.get("D");fixDestination(a,"D",e);continue}const n=e.get("Dest");fixDestination(e,"Dest",n)}}#aa(e){const collect=(e,t,a)=>{for(const n of e){"string"==typeof n.dest&&t?.has(n.dest)&&a.add(n.dest);n.items.length>0&&collect(n.items,t,a)}};for(const t of e){const{outline:e,destinations:a,usedNamedDestinations:n}=t;e?.length&&collect(e,a,n)}}#da(e,t){const{dest:a,action:n,url:s,unsafeUrl:r,attachment:i,setOCGState:o}=e;if(n||s||r||i||o)return!0;if(!a)return!1;if("string"==typeof a){const e=t.dedupNamedDestinations.get(a)||a;return this.namedDestinations.has(e)}return!!(Array.isArray(a)&&a[0]instanceof Ref)&&!!t.oldRefMapping.get(a[0])}#ga(e,t){const a=[];for(const n of e){const e=this.#ga(n.items,t),s=this.#da(n,t);(s||e.length>0)&&a.push({...n,dest:s?n.dest:null,rawDict:s?n.rawDict:null,items:e,_documentData:t})}return a}#ha(e){const t=[];for(const a of e){const{outline:e}=a;e?.length&&t.push(...this.#ga(e,a))}this.outlineItems=t.length>0?t:null}async#ba(e,t){const{dest:a,rawDict:n}=t,s=t._documentData;if(a){if("string"==typeof a){const t=s.dedupNamedDestinations.get(a)||a;e.set("Dest",stringToAsciiOrUTF16BE(t))}else if(Array.isArray(a)){const t=a.slice();t[0]instanceof Ref&&(t[0]=s.oldRefMapping.get(t[0])||t[0]);e.set("Dest",t)}return}const r=n?.get("A");if(r instanceof Dict){this.currentDocument=s;const t=await this.#$t(r,s.document.xref);this.currentDocument=null;e.set("A",t)}}async#wa(){const{outlineItems:e}=this;if(!e?.length)return;const[t,a]=this.newDict;a.setIfName("Type","Outlines");const assignRefs=e=>{for(const t of e){[t._ref]=this.newDict;t.items.length>0&&assignRefs(t.items)}};assignRefs(e);const fillItems=async(e,t)=>{let a=0;for(let n=0;n0&&r.set("Prev",e[n-1]._ref);n0){r.set("First",s.items[0]._ref);r.set("Last",s.items.at(-1)._ref);const e=await fillItems(s.items,s._ref);void 0!==s.count&&r.set("Count",s.count<0?-e:e);a+=void 0!==s.count&&s.count<0?1:e+1}else a+=1;await this.#ba(r,s);const i=(s.bold?2:0)|(s.italic?1:0);0!==i&&r.set("F",i);!s.color||0===s.color[0]&&0===s.color[1]&&0===s.color[2]||r.set("C",[s.color[0]/255,s.color[1]/255,s.color[2]/255])}return a},n=await fillItems(e,t);a.set("First",e[0]._ref);a.set("Last",e.at(-1)._ref);a.set("Count",n);this.rootDict.set("Outlines",t)}async#ca(e){this.#ja(e);this.#ka(e);this.#ya(e);await this.#qa(e);const t=this.fields;for(const a of e){let e=a.acroForm?.get("Fields")||null;!e&&a.fieldToParent.size>0&&(e=this.#va(a.fieldToParent,a.document.xref));if(Array.isArray(e)&&e.length>0){this.currentDocument=a;await this.#Sa(t,e);this.currentDocument=null}}this.#Aa(e)}#ya(e){let t=0,a=null;for(const n of e){const e=n.acroForm?.get("Q");if("number"==typeof e&&0!==e)if(a?.acroFormQ>0)n.acroFormQ=e;else if(0!==t){if(e!==t){a.acroFormQ||=t;n.acroFormQ=e;t=0}}else{t=e;a=n}}t>0&&(this.acroFormQ=t)}#ja(e){let t=0,a=!1;for(const n of e){if(!n.acroForm)continue;const e=n.acroForm.get("SigFlags");"number"==typeof e&&n.hasSignatureAnnotations&&(t|=e);!0===n.acroForm.get("NeedAppearances")&&(a=!0)}this.acroFormSigFlags=t;this.acroFormNeedAppearances=a}#Aa(e){const t=[];for(const a of e){const e=a.acroForm?.get("CO")||null;if(!Array.isArray(e))continue;const{oldRefMapping:n}=a;for(const a of e){const e=a instanceof Ref&&n.get(a);e&&t.push(e)}}this.acroFormCalculationOrder=t.length>0?t:null}#ka(e){let t=null,a=null;for(const n of e){const e=n.acroForm?.get("DA")||null;if(e&&"string"==typeof e)if(a?.acroFormDefaultAppearance)n.acroFormDefaultAppearance=e;else if(t){if(e!==t){a.acroFormDefaultAppearance||=t;n.acroFormDefaultAppearance=e;t=null}}else{t=e;a=n}}t&&(this.acroFormDefaultAppearance=t)}async#qa(e){let t=null,a=null,n=null;for(const s of e){const e=s.acroForm?.get("DR")||null;if(e&&e instanceof Dict)if(n?.acroFormDefaultResources)s.acroFormDefaultResources=e;else if(t){if(!deepCompare(t,e)){n.acroFormDefaultResources||=t;s.acroFormDefaultResources=e;t=null;a=null}}else{t=e;a=s.acroForm.getRaw("DR");n=s}}if(t){this.currentDocument=n;this.acroFormDefaultResources=await this.#Yt(a,!0,n.document.xref);this.currentDocument=null}}#va(e,t){const a=[],n=new RefSet;for(const[s,r]of e){if(!r){a.push(s);continue}let e=r,i=r;for(;;){e=t.fetchIfRef(e)?.getRaw("Parent")||null;if(!e)break;i=e}if(i instanceof Ref&&!n.has(i)){a.push(i);n.put(i)}}return a}async#Sa(e,t){const a=new RefSet,n=[{kids:t,newKids:e,pos:0,oldParentRef:null,parentRef:null,parent:null}],{document:{xref:s},oldRefMapping:r,fieldToParent:i,acroFormDefaultAppearance:o,acroFormDefaultResources:l,acroFormQ:f}=this.currentDocument,c=[],h=[];for(;n.length>0;){const e=n.at(-1),{kids:t,newKids:u,parent:m,pos:p}=e;if(p===t.length){n.pop();if(0===u.length||!m)continue;const t=this.xref[e.parentRef.num]=this.cloneDict(m);t.delete("Parent");t.delete("Kids");await this.#Yt(t,!1,s);t.set("Kids",u);if(n.length>0){const a=n.at(-1);if(!a.parentRef&&a.oldParentRef){const e=a.parentRef=this.newRef;t.set("Parent",e);r.put(a.oldParentRef,e)}a.newKids.push(e.parentRef)}continue}const d=t[e.pos++];if(!(d instanceof Ref)||a.has(d))continue;a.put(d);const g=s.fetchIfRef(d);if(g.has("Kids")){const e=g.get("Kids");if(!Array.isArray(e))continue;n.push({kids:e,newKids:[],pos:0,oldParentRef:d,parentRef:null,parent:g});continue}if(!i.has(d))continue;const b=r.get(d);if(!b)continue;u.push(b);if(!e.parentRef&&e.oldParentRef){e.parentRef=this.newRef;r.put(e.oldParentRef,e.parentRef)}const w=this.xref[b.num];e.parentRef&&w.set("Parent",e.parentRef);o&&isName(w.get("FT"),"Tx")&&!w.has("DA")&&c.push(w);l&&!w.has("Kids")&&w.get("AP")instanceof Dict&&h.push(w);f&&!w.has("Q")&&w.set("Q",f)}for(const e of c){getInheritableProperty({dict:e,key:"DA"})||e.set("DA",o)}const u=new Map;for(const e of h){const t=e.get("AP");for(const[,e]of t){if(!(e instanceof BaseStream))continue;let t=e.dict.getRaw("Resources");if(!t){const t=await u.getOrInsertComputed(l,()=>this.#$t(l,s));e.dict.set("Resources",t);continue}t=s.fetchIfRef(t);for(const[e,a]of l.getRawEntries())if(!t.has(e)){let n=a;a instanceof Ref?n=await this.#Yt(a,!0,s):(a instanceof Dict||a instanceof BaseStream||Array.isArray(a))&&(n=await u.getOrInsertComputed(a,()=>this.#$t(a,s)));t.set(e,n)}}}}async#na(){if(!this.hasSingleFile)return;const{documentData:{document:e,pageLabels:t}}=this.oldPages[0];if(!t)return;const a=e.numPages,n=[],s=new Set(this.oldPages.map(({page:{pageIndex:e}})=>e));let r=null,i=-1;for(let e=0;ee!==f[t])&&m.set(e,a)}const d=t.userUnit;1!==d&&m.set("UserUnit",d);m.setIfDict("Resources",await this.#Yt(c,!0,o));let g=null;if(n){const e=await this.#Yt(n,!0,o);this.#pa(e,r);Array.isArray(e)&&e.length>0&&(g=e)}const b=this.#Vt?.newAnnotationsByPage?.get(e);if(b){const{handler:e,task:a,imagesPromises:n}=this.#Vt,s=new RefSetCache,r=await AnnotationFactory.saveNewAnnotations(t.createAnnotationEvaluator(e),this.xrefWrapper,a,b,n,s);for(const[e,{data:t}]of s.items())this.xref[e.num]=t;g||=[];for(const{ref:e}of r.annotations)g.push(e)}m.setIfArray("Annots",g);if(this.useObjectStreams){const e=this.newRefCount,t=[];for(let a=p;a0;){const{dict:e,kids:t,parentRef:a}=s.pop();if(t.length<=16){e.set("Kids",t);for(const e of t)this.xref[e.num].set("Parent",a);continue}const n=Math.max(16,Math.ceil(t.length/16)),r=[];for(let e=0;ee.localeCompare(t):([e],[t])=>e-t),[n,s]=this.newDict,r=[{dict:s,entries:a,isRoot:!0}],i=t?"Names":"Nums";for(;r.length>0;){const{dict:e,entries:t,isRoot:a}=r.pop();if(t.length<=64){a||e.set("Limits",[t[0][0],t.at(-1)[0]]);e.set(i,t.flat());continue}const n=[],s=Math.max(64,Math.ceil(t.length/64));for(let e=0;e0){const e=this.#Ca(Array.from(this.parentTree.entries()),!1);this.xref[e.num].setIfName("Type","ParentTree");n.set("ParentTree",e);n.set("ParentTreeNextKey",this.parentTree.size)}if(this.idTree.size>0){const e=this.#Ca(Array.from(this.idTree.entries()),!0);this.xref[e.num].setIfName("Type","IDTree");n.set("IDTree",e)}if(this.classMap.size>0){const e=this.newRef;this.xref[e.num]=this.classMap;n.set("ClassMap",e)}if(this.roleMap.size>0){const e=this.newRef;this.xref[e.num]=this.roleMap;n.set("RoleMap",e)}if(this.namespaces.size>0){const e=this.newRef;this.xref[e.num]=Array.from(this.namespaces.values());n.set("Namespaces",e)}if(this.structTreeAF.length>0){const e=this.newRef;this.xref[e.num]=this.structTreeAF;n.set("AF",e)}if(this.structTreePronunciationLexicon.length>0){const e=this.newRef;this.xref[e.num]=this.structTreePronunciationLexicon;n.set("PronunciationLexicon",e)}t.set("StructTreeRoot",a)}#Ra(){if(0===this.fields.length)return;const{rootDict:e}=this,t=this.newRef,a=this.xref[t.num]=new Dict;e.set("AcroForm",t);a.set("Fields",this.fields);this.acroFormNeedAppearances&&a.set("NeedAppearances",!0);this.acroFormSigFlags>0&&a.set("SigFlags",this.acroFormSigFlags);a.setIfArray("CO",this.acroFormCalculationOrder);a.setIfDict("DR",this.acroFormDefaultResources);this.acroFormDefaultAppearance&&a.set("DA",this.acroFormDefaultAppearance);this.acroFormQ>0&&a.set("Q",this.acroFormQ)}async#Ha(){const{rootDict:e}=this;e.setIfName("Type","Catalog");e.setIfName("Version",this.version);this.#Ra();this.#xa();this.#Ia();this.#Fa();this.#Ta();this.#Oa();await this.#wa()}#Ba(){const e=new Map;if(this.isSingleFile){const{xref:{trailer:t}}=this.oldPages[0].documentData.document,a=t.get("Info");for(const[t,n]of a||[])"string"==typeof n&&e.set(t,stringToPDFString(n))}e.delete("ModDate");e.set("CreationDate",getModificationDate());e.set("Creator","PDF.js");e.set("Producer","Firefox");this.author&&e.set("Author",this.author);this.title&&e.set("Title",this.title);for(const[t,a]of e)this.infoDict.set(t,stringToAsciiOrUTF16BE(a));return e}async#Da(){if(!this.isSingleFile)return[null,null,null];const{documentData:e}=this.oldPages[0],{document:{xref:{trailer:t,encrypt:a}}}=e;if(!t.has("Encrypt"))return[null,null,null];const n=t.get("Encrypt");if(!(n instanceof Dict))return[null,null,null];this.currentDocument=e;const s=[await this.#$t(n,t.xref),a,t.get("ID")];this.currentDocument=null;return s}async#Ma(){const e=new RefSetCache;e.put(Ref.get(0,65535),{data:null});for(let t=1,a=this.xref.length;t{this._contentLength=e.contentLength;this._isStreamingSupported=e.isStreamingSupported;this._isRangeSupported=e.isRangeSupported;this._headersCapability.resolve()},this._headersCapability.reject)}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader{_reader=null;constructor(e,t,a){super(e,t,a);const{msgHandler:n}=e._source,s=n.sendWithStream("GetRangeReader",{begin:t,end:a});this._reader=s.getReader()}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=Promise.withResolvers()}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}class WorkerMessageHandler{static{"undefined"==typeof window&&!e&&"undefined"!=typeof self&&"function"==typeof self.postMessage&&"onmessage"in self&&this.initializeFromPort(self)}static setup(e,t){let a=!1;e.on("test",t=>{if(!a){a=!0;e.send("test",t instanceof Uint8Array)}});e.on("configure",e=>{!function setVerbosityLevel(e){Number.isInteger(e)&&(ta=e)}(e.verbosity)});e.on("GetDocRequest",e=>this.createDocumentHandler(e,t))}static createDocumentHandler(e,t){let a,n=!1,s=null;const r=new Set,i=getVerbosityLevel(),{docId:o,apiVersion:l}=e,f="5.7.284";if(l!==f)throw new Error(`The API version "${l}" does not match the Worker version "${f}".`);const buildMsg=(e,t)=>`The \`${e}.prototype\` contains unexpected enumerable property "${t}", thus breaking e.g. \`for...in\` iteration of ${e}s.`;for(const e in{})throw new Error(buildMsg("Object",e));for(const e in[])throw new Error(buildMsg("Array",e));const c=o+"_worker";let h=new MessageHandler(c,o,t);function ensureNotTerminated(){if(n)throw new Error("Worker was terminated")}function startWorkerTask(e){r.add(e)}function finishWorkerTask(e){e.finish();r.delete(e)}async function loadDocument(e){await a.ensureDoc("checkHeader");await a.ensureDoc("parseStartXRef");await a.ensureDoc("parse",[e]);await a.ensureDoc("checkFirstPage",[e]);await a.ensureDoc("checkLastPage",[e]);const t=await a.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaResources");startWorkerTask(e);await a.ensureDoc("loadXfaResources",[h,e]);finishWorkerTask(e)}const[n,s]=await Promise.all([a.ensureDoc("numPages"),a.ensureDoc("fingerprints")]);return{numPages:n,fingerprints:s,htmlForXfa:t?await a.ensureDoc("htmlForXfa"):null}}function setupDoc(e){function onSuccess(e){ensureNotTerminated();h.send("GetDoc",{pdfInfo:e})}function onFailure(e){if(!n)if(e instanceof PasswordException){const t=new WorkerTask(`PasswordException: response ${e.code}`);startWorkerTask(t);h.sendWithPromise("PasswordRequest",e).then(function({password:e}){finishWorkerTask(t);a.updatePassword(e);pdfManagerReady()}).catch(function(){finishWorkerTask(t);h.send("DocException",e)})}else h.send("DocException",wrapReason(e))}function pdfManagerReady(){ensureNotTerminated();loadDocument(!1).then(onSuccess,function(e){ensureNotTerminated();e instanceof XRefParseException?a.requestLoadedStream().then(function(){ensureNotTerminated();loadDocument(!0).then(onSuccess,onFailure)}):onFailure(e)})}ensureNotTerminated();(async function getPdfManager({data:e,password:t,disableAutoFetch:a,rangeChunkSize:n,docBaseUrl:r,enableXfa:i,evaluatorOptions:l}){const f={source:null,disableAutoFetch:a,docBaseUrl:r,docId:o,enableXfa:i,evaluatorOptions:l,handler:h,length:0,password:t,rangeChunkSize:n};if(e){f.source=e;return new LocalPdfManager(f)}const c=new PDFWorkerStream({msgHandler:h}),u=c.getFullReader(),{promise:m,resolve:p,reject:d}=Promise.withResolvers();let g,b=[];s=e=>c.cancelAllRequests(e);u.headersReady.then(()=>{if(u.isRangeSupported){f.source=c;f.length=u.contentLength;f.disableAutoFetch||=u.isStreamingSupported;g=new NetworkPdfManager(f);for(const e of b)g.sendProgressiveData(e);b=null;p(g);s=null}}).catch(e=>{d(e);s=null});(async function readData(){let e=0;for(;;){const{value:t,done:a}=await u.read();ensureNotTerminated();if(a)break;e+=t.byteLength;u.isStreamingSupported||h.send("DocProgress",{loaded:e,total:u.contentLength});g?g.sendProgressiveData(t):b.push(t)}if(!g){f.source=arrayBuffersToBytes(b);b=null;g=new LocalPdfManager(f);p(g)}s=null})().catch(e=>{d(e);s=null});return m})(e).then(function(e){if(n){e.terminate(new AbortException("Worker was terminated."));throw new Error("Worker was terminated")}a=e;a.requestLoadedStream(!0).then(e=>{h.send("DataLoaded",{length:e.bytes.byteLength})})}).then(pdfManagerReady,onFailure)}h.on("GetPage",function(e){return a.getPage(e.pageIndex).then(function(e){return Promise.all([a.ensure(e,"rotate"),a.ensure(e,"ref"),a.ensure(e,"userUnit"),a.ensure(e,"view")]).then(function([e,t,a,n]){return{rotate:e,ref:t,refStr:t?.toString()??null,userUnit:a,view:n}})})});h.on("GetPageIndex",function(e){const t=Ref.get(e.num,e.gen);return a.ensureCatalog("getPageIndex",[t])});h.on("GetDestinations",function(e){return a.ensureCatalog("destinations")});h.on("GetDestination",function(e){return a.ensureCatalog("getDestination",[e.id])});h.on("GetPageLabels",function(e){return a.ensureCatalog("pageLabels")});h.on("GetPageLayout",function(e){return a.ensureCatalog("pageLayout")});h.on("GetPageMode",function(e){return a.ensureCatalog("pageMode")});h.on("GetViewerPreferences",function(e){return a.ensureCatalog("viewerPreferences")});h.on("GetOpenAction",function(e){return a.ensureCatalog("openAction")});h.on("GetAttachments",function(e){return a.ensureCatalog("attachments")});h.on("GetDocJSActions",function(e){return a.ensureCatalog("jsActions")});h.on("GetPageJSActions",function({pageIndex:e}){return a.getPage(e).then(e=>a.ensure(e,"jsActions"))});h.on("GetAnnotationsByType",async function({types:e,pageIndexesToSkip:t}){const[n,s]=await Promise.all([a.ensureDoc("numPages"),a.ensureDoc("annotationGlobals")]);if(!s)return null;const r=[],i=[];let o=null;try{for(let l=0,f=n;lt&&t.collectAnnotationsByType(h,o,e,i,s)||[]))}await Promise.all(r);return(await Promise.all(i)).filter(e=>!!e)}finally{o&&finishWorkerTask(o)}});h.on("GetOutline",function(e){return a.ensureCatalog("documentOutline")});h.on("GetOptionalContentConfig",function(e){return a.ensureCatalog("optionalContentConfig")});h.on("GetPermissions",function(e){return a.ensureCatalog("permissions")});h.on("GetMetadata",function(e){return Promise.all([a.ensureDoc("documentInfo"),a.ensureCatalog("metadata"),a.ensureCatalog("hasStructTree")])});h.on("GetMarkInfo",function(e){return a.ensureCatalog("markInfo")});h.on("GetData",function(e){return a.requestLoadedStream().then(e=>e.bytes)});h.on("GetAnnotations",function({pageIndex:e,intent:t}){return a.getPage(e).then(function(a){const n=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(n);return a.getAnnotationsData(h,n,t).then(e=>{finishWorkerTask(n);return e},e=>{finishWorkerTask(n);throw e})})});h.on("GetFieldObjects",function(e){return a.ensureDoc("fieldObjects").then(e=>e?.allFields||null)});h.on("HasJSActions",function(e){return a.ensureDoc("hasJSActions")});h.on("GetCalculationOrderIds",function(e){return a.ensureDoc("calculationOrderIds")});h.on("ExtractPages",async function({pageInfos:e,annotationStorage:t}){if(!e){warn("extractPages: nothing to extract.");return null}Array.isArray(e)||(e=[e]);let n,s=0;for(const t of e)if(null===t.document)t.document=a.pdfDocument;else if(ArrayBuffer.isView(t.document)){const e=new LocalPdfManager({source:t.document,docId:`${o}_extractPages_${s++}`,handler:h,password:t.password??null,evaluatorOptions:Object.assign({},a.evaluatorOptions)});let n=!1,r=!0;for(;;)try{await e.requestLoadedStream();await e.ensureDoc("checkHeader");await e.ensureDoc("parseStartXRef");await e.ensureDoc("parse",[n]);break}catch(t){if(t instanceof XRefParseException){if(!1===n){n=!0;continue}r=!1;warn("extractPages: XRefParseException.")}else if(t instanceof PasswordException){const a=new WorkerTask(`PasswordException: response ${t.code}`);startWorkerTask(a);try{const{password:a}=await h.sendWithPromise("PasswordRequest",t);e.updatePassword(a)}catch{r=!1;warn("extractPages: invalid password.")}finally{finishWorkerTask(a)}}else{r=!1;warn("extractPages: invalid document.")}if(!r)break}r||(t.document=null);if(await e.ensureDoc("isPureXfa")){t.document=null;warn("extractPages does not support pure XFA documents.")}else t.document=e.pdfDocument}else warn("extractPages: invalid document.");try{const a=new PDFEditor;n=new WorkerTask(`ExtractPages: ${e.length} page(s)`);startWorkerTask(n);return await a.extractPages(e,t,h,n)}catch(e){warn(`extractPages: "${e}".`);return null}finally{n&&finishWorkerTask(n)}});h.on("SaveDocument",async function({isPureXfa:e,numPages:t,annotationStorage:n,filename:s}){const r=[a.requestLoadedStream(),a.ensureCatalog("acroForm"),a.ensureCatalog("acroFormRef"),a.ensureDoc("startXRef"),a.ensureDoc("xref"),a.ensureCatalog("structTreeRoot")],i=new RefSetCache,o=[],l=e?null:getNewAnnotationsMap(n),[f,c,u,m,p,d]=await Promise.all(r),g=p.trailer.getRaw("Root")||null;let b;if(l){d?await d.canUpdateStructTree({pdfManager:a,newAnnotationsByPage:l})&&(b=d):await StructTreeRoot.canCreateStructureTree({catalogRef:g,pdfManager:a,newAnnotationsByPage:l})&&(b=null);const e=AnnotationFactory.generateImages(n.values(),p,a.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===b?o:[];for(const[n,s]of l)t.push(a.getPage(n).then(t=>{const a=new WorkerTask(`Save (editor): page ${n}`);startWorkerTask(a);return t.saveNewAnnotations(h,a,s,e,i).finally(function(){finishWorkerTask(a)})}));null===b?o.push(Promise.all(t).then(async()=>{await StructTreeRoot.createStructureTree({newAnnotationsByPage:l,xref:p,catalogRef:g,pdfManager:a,changes:i})})):b&&o.push(Promise.all(t).then(async()=>{await b.updateStructureTree({newAnnotationsByPage:l,pdfManager:a,changes:i})}))}if(e)o.push(a.ensureDoc("serializeXfaData",[n]));else for(let e=0;ee.needAppearances),y=c instanceof Dict&&c.get("XFA")||null;let q=null,v=!1;if(Array.isArray(y)){for(let e=0,t=y.length;e{p.resetNewTemporaryRef()})});h.on("GetOperatorList",function(e,t){const{pageId:n,pageIndex:s}=e;a.getPage(n).then(function(a){const n=new WorkerTask(`GetOperatorList: page ${s}`);startWorkerTask(n);const r=i>=ce?Date.now():0;a.getOperatorList({handler:h,sink:t,task:n,intent:e.intent,cacheKey:e.cacheKey,annotationStorage:e.annotationStorage,modifiedIds:e.modifiedIds,pageIndex:s}).then(function(e){finishWorkerTask(n);r&&info(`page=${s+1} - getOperatorList: time=${Date.now()-r}ms, len=${e.length}`);t.close()},function(e){finishWorkerTask(n);n.terminated||t.error(e)})})});h.on("GetTextContent",function(e,t){const{pageId:n,pageIndex:s,includeMarkedContent:r,disableNormalization:o}=e;a.getPage(n).then(function(e){const a=new WorkerTask("GetTextContent: page "+s);startWorkerTask(a);const n=i>=ce?Date.now():0;e.extractTextContent({handler:h,task:a,sink:t,includeMarkedContent:r,disableNormalization:o}).then(function(){finishWorkerTask(a);n&&info(`page=${s+1} - getTextContent: time=`+(Date.now()-n)+"ms");t.close()},function(e){finishWorkerTask(a);a.terminated||t.error(e)})})});h.on("GetStructTree",function(e){return a.getPage(e.pageIndex).then(e=>a.ensure(e,"getStructTree"))});h.on("FontFallback",function(e){return a.fontFallback(e.id,h)});h.on("Cleanup",function(e){return a.cleanup(!0)});h.on("Terminate",async function(e){n=!0;const t=[];if(a){a.terminate(new AbortException("Worker was terminated."));const e=a.cleanup();t.push(e);a=null}else clearGlobalCaches();s?.(new AbortException("Worker was terminated."));for(const e of r){t.push(e.finished);e.terminate()}await Promise.all(t);h.destroy();h=null});h.on("Ready",function(t){setupDoc(e);e=null});return c}static initializeFromPort(e){const t=new MessageHandler("worker","main",e);this.setup(t,e);t.send("ready",null)}}globalThis.pdfjsWorker={WorkerMessageHandler};export{WorkerMessageHandler}; \ No newline at end of file diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb new file mode 100644 index 00000000..30d52963 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitFixed.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitFixed.pfb new file mode 100644 index 00000000..f12dcbce Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitFixed.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb new file mode 100644 index 00000000..cf8e24ae Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb new file mode 100644 index 00000000..d2880017 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb new file mode 100644 index 00000000..d71697d4 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitSerif.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitSerif.pfb new file mode 100644 index 00000000..3fa682ef Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitSerif.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb new file mode 100644 index 00000000..ff7c6dde Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb new file mode 100644 index 00000000..460231fb Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb new file mode 100644 index 00000000..d03a7c78 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb b/web/public/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb new file mode 100644 index 00000000..c8f9bca7 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb differ diff --git a/web/public/vendor/pdfjs/standard_fonts/LICENSE_FOXIT b/web/public/vendor/pdfjs/standard_fonts/LICENSE_FOXIT new file mode 100644 index 00000000..8b4ed6dd --- /dev/null +++ b/web/public/vendor/pdfjs/standard_fonts/LICENSE_FOXIT @@ -0,0 +1,27 @@ +// Copyright 2014 PDFium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/web/public/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION b/web/public/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION new file mode 100644 index 00000000..aba73e8a --- /dev/null +++ b/web/public/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION @@ -0,0 +1,102 @@ +Digitized data copyright (c) 2010 Google Corporation + with Reserved Font Arimo, Tinos and Cousine. +Copyright (c) 2012 Red Hat, Inc. + with Reserved Font Name Liberation. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE The goals of the Open Font License (OFL) are to stimulate +worldwide development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to provide +a free and open framework in which fonts may be shared and improved in +partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. +The fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + + + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. +This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components +as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting ? in part or in whole ? +any of the components of the Original Version, by changing formats or +by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer +or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components,in + Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the + corresponding Copyright Holder. This restriction only applies to the + primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + +5) The Font Software, modified or unmodified, in part or in whole, must + be distributed entirely under this license, and must not be distributed + under any other license. The requirement for fonts to remain under + this license does not apply to any document created using the Font + Software. + + + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + + + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + diff --git a/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf new file mode 100644 index 00000000..ee237154 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf differ diff --git a/web/public/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf new file mode 100644 index 00000000..42b5717d Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf differ diff --git a/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf new file mode 100644 index 00000000..0cf61263 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf differ diff --git a/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf new file mode 100644 index 00000000..366d1489 Binary files /dev/null and b/web/public/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf differ diff --git a/web/public/vendor/pdfjs/text_layer_builder.css b/web/public/vendor/pdfjs/text_layer_builder.css new file mode 100644 index 00000000..a2eb5ad6 --- /dev/null +++ b/web/public/vendor/pdfjs/text_layer_builder.css @@ -0,0 +1,144 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.textLayer { + color-scheme: only light; + + position: absolute; + text-align: initial; + inset: 0; + overflow: clip; + opacity: 1; + line-height: 1; + text-size-adjust: none; + forced-color-adjust: none; + transform-origin: 0 0; + caret-color: CanvasText; + z-index: 0; + + &.highlighting { + touch-action: none; + } + + :is(span, br) { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0% 0%; + } + + /* We multiply the font size by --min-font-size, and then scale the text + * elements by 1/--min-font-size. This allows us to effectively ignore the + * minimum font size enforced by the browser, so that the text layer s + * can always match the size of the text in the canvas. */ + --min-font-size: 1; + --text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size)); + --min-font-size-inv: calc(1 / var(--min-font-size)); + + > :not(.markedContent), + .markedContent span:not(.markedContent) { + z-index: 1; + + --font-height: 0; /* set by text_layer.js */ + font-size: calc(var(--text-scale-factor) * var(--font-height)); + + --scale-x: 1; + --rotate: 0deg; + transform: rotate(var(--rotate)) scaleX(var(--scale-x)) + scale(var(--min-font-size-inv)); + } + + .markedContent { + display: contents; + } + + span[role="img"] { + user-select: none; + cursor: default; + } + + .highlight { + --highlight-bg-color: rgb(180 0 170 / 0.25); + --highlight-selected-bg-color: rgb(0 100 0 / 0.25); + --highlight-backdrop-filter: none; + --highlight-selected-backdrop-filter: none; + + @media screen and (forced-colors: active) { + --highlight-bg-color: transparent; + --highlight-selected-bg-color: transparent; + --highlight-backdrop-filter: var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter: var( + --hcm-highlight-selected-filter + ); + } + + margin: -1px; + padding: 1px; + background-color: var(--highlight-bg-color); + backdrop-filter: var(--highlight-backdrop-filter); + border-radius: 4px; + + &.appended { + position: initial; + } + + &.begin { + border-radius: 4px 0 0 4px; + } + + &.end { + border-radius: 0 4px 4px 0; + } + + &.middle { + border-radius: 0; + } + + &.selected { + background-color: var(--highlight-selected-bg-color); + backdrop-filter: var(--highlight-selected-backdrop-filter); + } + } + + ::selection { + /* stylelint-disable declaration-block-no-duplicate-properties */ + /*#if !MOZCENTRAL*/ + background: rgba(0 0 255 / 0.25); + /*#endif*/ + /* stylelint-enable declaration-block-no-duplicate-properties */ + background: color-mix(in srgb, AccentColor, transparent 75%); + } + + /* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */ + /*#if !MOZCENTRAL*/ + br::selection { + background: transparent; + } + /*#endif*/ + + .endOfContent { + display: block; + position: absolute; + inset: 100% 0 0; + z-index: 0; + cursor: default; + user-select: none; + } + + &.selecting .endOfContent { + top: 0; + } +} diff --git a/web/public/vendor/pdfjs/wasm/LICENSE_JBIG2 b/web/public/vendor/pdfjs/wasm/LICENSE_JBIG2 new file mode 100644 index 00000000..37a329c6 --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/LICENSE_JBIG2 @@ -0,0 +1,196 @@ +// Copyright 2014 The PDFium Authors +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/web/public/vendor/pdfjs/wasm/LICENSE_OPENJPEG b/web/public/vendor/pdfjs/wasm/LICENSE_OPENJPEG new file mode 100644 index 00000000..e8fa4104 --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/LICENSE_OPENJPEG @@ -0,0 +1,39 @@ +/* + * The copyright in this software is being made available under the 2-clauses + * BSD License, included below. This software may be subject to other third + * party and contributor rights, including patent rights, and no such rights + * are granted under this license. + * + * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2014, Professor Benoit Macq + * Copyright (c) 2003-2014, Antonin Descampe + * Copyright (c) 2003-2009, Francois-Olivier Devaux + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France + * Copyright (c) 2012, CS Systemes d'Information, France + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ diff --git a/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_JBIG2 b/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_JBIG2 new file mode 100644 index 00000000..f1845baf --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_JBIG2 @@ -0,0 +1,13 @@ +Copyright 2026 Mozilla Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_OPENJPEG b/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_OPENJPEG new file mode 100644 index 00000000..623929b8 --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_OPENJPEG @@ -0,0 +1,22 @@ +Copyright (c) 2024, Mozilla Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_QCMS b/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_QCMS new file mode 100644 index 00000000..7e1aeb34 --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/LICENSE_PDFJS_QCMS @@ -0,0 +1,22 @@ +Copyright (c) 2025, Mozilla Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/web/public/vendor/pdfjs/wasm/LICENSE_QCMS b/web/public/vendor/pdfjs/wasm/LICENSE_QCMS new file mode 100644 index 00000000..eec8246d --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/LICENSE_QCMS @@ -0,0 +1,21 @@ +qcms +Copyright (C) 2009-2024 Mozilla Corporation +Copyright (C) 1998-2007 Marti Maria + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/web/public/vendor/pdfjs/wasm/jbig2.wasm b/web/public/vendor/pdfjs/wasm/jbig2.wasm new file mode 100644 index 00000000..254e1e48 Binary files /dev/null and b/web/public/vendor/pdfjs/wasm/jbig2.wasm differ diff --git a/web/public/vendor/pdfjs/wasm/jbig2_nowasm_fallback.js b/web/public/vendor/pdfjs/wasm/jbig2_nowasm_fallback.js new file mode 100644 index 00000000..c86528f0 --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/jbig2_nowasm_fallback.js @@ -0,0 +1,15 @@ +/* THIS FILE IS GENERATED - DO NOT EDIT */ +async function JBig2(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";var readAsync;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var WebAssembly={Memory:function(opts){this.buffer=new ArrayBuffer(opts["initial"]*65536)},Module:function(binary){},Instance:function(module,info){this.exports=( +// EMSCRIPTEN_START_ASM +function instantiate(ha){var a;var b=new Uint8Array(123);for(var c=25;c>=0;--c){b[48+c]=52+c;b[65+c]=c;b[97+c]=26+c}b[43]=62;b[47]=63;function i(j,k,l){var d,e,c=0,f=k,g=l.length,h=k+(g*3>>2)-(l[g-2]=="=")-(l[g-1]=="=");for(;c>4;if(f>2;if(f>>0;s=s>>>0;if(q+s>a.length)throw"trap: invalid memory.fill";a.fill(u,q,q+s)}function ga(n){var v=new ArrayBuffer(16777216);var w=new Int8Array(v);var x=new Int16Array(v);var y=new Int32Array(v);var z=new Uint8Array(v);var A=new Uint16Array(v);var B=new Uint32Array(v);var C=new Float32Array(v);var D=new Float64Array(v);var E=Math.imul;var F=Math.fround;var G=Math.abs;var H=Math.clz32;var I=Math.min;var J=Math.max;var K=Math.floor;var L=Math.ceil;var M=Math.trunc;var N=Math.sqrt;var O=n.a;var P=O.a;var Q=O.b;var R=O.c;var S=O.d;var T=O.e;var U=O.f;var V=O.g;var W=O.h;var X=O.i;var Y=72736;var Z=0; +// EMSCRIPTEN_START_FUNCS +function yc(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,q=0,r=0,s=0,u=0,v=0,C=0,D=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0;J=Y-16|0;Y=J;a:{b:{c:{d:{c=y[a+4>>2];b=y[c+8>>2];i=y[c+4>>2];e:{if(b>>>0>i>>>0){break e}if((b|0)==(i|0)){aa=2;break c}R=a+80|0;while(1){if(b>>>0>i>>>0){break e}if(i-b>>>0<11){break c}f:{g:{h:{m=y[R>>2];i:{if(!m){b=na(72);w[b+4|0]=0;y[b>>2]=0;t(b+8|0,0,64);y[J+4>>2]=0;ib(R,b);ac(J+4|0);j:{c=y[a+80>>2];k:{if(ra(y[a+4>>2],c)){break k}if(va(y[a+4>>2],c+4|0)){break k}b=y[a+4>>2];d=y[b+8>>2];g=y[b>>2];l:{m:{if(B[b+4>>2]<=d>>>0){w[J+15|0]=0;break m}d=z[d+g|0];w[J+15|0]=d;if(d>>>0<224){break m}if(ra(b,c+8|0)){break k}b=y[c+8>>2]&536870911;y[c+8>>2]=b;if(b>>>0>64){break k}g=y[a+4>>2];i=y[g+8>>2];d=i+(b+8>>>3|0)|0;if(d>>>0>>0){break l}h=g;g=y[g+4>>2];y[h+8>>2]=d>>>0>>0?d:g;break l}if(va(b,J+15|0)){break k}b=z[J+15|0]>>>5|0;y[c+8>>2]=b}g=z[c+4|0];n:{if(!b){break n}d=y[c>>2];xc(c+12|0,b);b=0;d=(d>>>0>65536?4:d>>>0>256?2:1)-1|0;while(1){if(y[c+8>>2]<=(b|0)){break n}o:{p:{switch(d-1|0){default:if(va(y[a+4>>2],J+15|0)){break k}m=z[J+15|0];y[y[c+12>>2]+(b<<2)>>2]=m;break o;case 0:if(Wa(y[a+4>>2],J+12|0)){break k}m=A[J+12>>1];y[y[c+12>>2]+(b<<2)>>2]=m;break o;case 1:break e;case 2:break p}}if(ra(y[a+4>>2],J+8|0)){break k}m=y[J+8>>2];y[y[c+12>>2]+(b<<2)>>2]=m}b=b+1|0;if(B[c>>2]>m>>>0){continue}break}break k}b=y[a+4>>2];q:{if(!(g&64)){if(va(b,J+15|0)){break k}y[c+24>>2]=z[J+15|0];break q}if(ra(b,c+24|0)){break k}}if(!ra(y[a+4>>2],c+28|0)){break j}}ib(R,0);break i}b=y[a+4>>2];d=y[b+20>>2];y[c+40>>2]=y[b+16>>2];y[c+44>>2]=d;b=y[b+8>>2];y[c+48>>2]=1;y[c+36>>2]=b;y[a+84>>2]=b;m=y[a+80>>2]}r:{while(1){h=0;c=0;f=Y-368|0;Y=f;b=2;s:{t:{u:{v:{w:{x:{y:{z:{A:{B:{C:{D:{E:{F:{G:{H:{I:{J:{K:{L:{M:{N:{O:{P:{Q:{R:{S:{T:{U:{d=z[m+4|0]&63;switch(d-16|0){case 0:break T;case 1:case 2:case 3:case 5:case 8:case 9:case 10:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 21:case 25:case 28:case 29:case 30:case 31:break B;case 4:case 6:case 7:break S;case 20:case 22:case 23:break R;case 24:case 26:case 27:break Q;case 32:break P;case 33:break O;case 34:break N;case 35:break u;case 36:break M;case 37:break L;default:break U}}V:{switch(d|0){case 0:b=0;s=0;F=0;N=0;e=Y-272|0;Y=e;l=1;if(!Wa(y[a+4>>2],e+118|0)){j=na(56);y[j+40>>2]=0;y[j+32>>2]=0;y[j+36>>2]=0;y[j+24>>2]=0;y[j+28>>2]=0;y[j+16>>2]=0;y[j+20>>2]=0;h=A[e+118>>1];c=h&1;w[j|0]=c;d=h>>>10&3;w[j+3|0]=d;g=h&12288;w[j+2|0]=(g|0)!=0;i=(h&254)>>>1&1;w[j+1|0]=i;Q=j+16|0;W:{X:{Y:{if(!c){c=d?2:8;d=j+44|0;while(1){if((b|0)!=(c|0)){g=b+d|0;b=b+1|0;if(!va(y[a+4>>2],g)){continue}break W}break}if(z[j+1|0]!=1){break X}if(!z[j+2|0]){break Y}break X}if(g|!i){break X}}c=j+52|0;b=0;while(1){if((b|0)==4){break X}d=b+c|0;b=b+1|0;if(!va(y[a+4>>2],d)){continue}break}break W}if(ra(y[a+4>>2],j+12|0)){break W}if(ra(y[a+4>>2],j+8|0)|B[j+12>>2]>65535|B[j+8>>2]>65535){break W}b=y[m+8>>2];i=(b|0)>0?b:0;b=0;while(1){if((b|0)==(i|0)){c=1;k=0;b=0;while(1)if((b|0)==(i|0)){Z:{_:{$:{aa:{ba:{ca:{da:{ea:{fa:{if(!(c&1)){break fa}y[j+4>>2]=k;U=Ub(e+104|0,k);n=y[U>>2];ga:{if((n|0)==y[U+4>>2]){break ga}b=y[m+8>>2];q=(b|0)>0?b:0;c=1;k=0;while(1){if((q|0)==(F|0)){break ga}ha:{b=Ma(a,y[y[m+12>>2]+(F<<2)>>2]);if(z[b+4|0]&63){break ha}b=y[b+56>>2];g=y[b+24>>2];d=y[b+28>>2]-g>>2;b=0;while(1){if((b|0)==(d|0)){g=0;i=0;if(c&1){b=d+k|0;g=b>>>0>=d>>>0;i=g?b:0}k=i;c=g;break ha}i=b+k|0;if(!(i>>>0>=b>>>0&c)){break fa}y[n+(i<<2)>>2]=y[g+(b<<2)>>2];b=b+1|0;continue}}F=F+1|0;continue}}bb(Q,U);ia:{if(z[j|0]!=1){break ia}b=h>>>2&3;if((b|0)==2){break Z}d=h>>>4&3;if((d|0)==2){break Z}ja:{ka:{switch(b|0){case 0:c=sa(a,4);b=0;break ja;case 1:c=sa(a,5);b=0;break ja;default:break ka}}b=Ga(a,m,0);if(!b){break Z}c=y[b+68>>2];b=1}y[j+28>>2]=c;la:{ma:{switch(d|0){case 0:c=sa(a,2);break la;case 1:c=sa(a,3);break la;default:break ma}}c=Ga(a,m,b);if(!c){break Z}b=b+1|0;c=y[c+68>>2]}y[j+32>>2]=c;na:{if(!(h&64)){c=sa(a,1);break na}c=Ga(a,m,b);if(!c){break Z}b=b+1|0;c=y[c+68>>2]}y[j+36>>2]=c;if(z[j+1|0]!=1){break ia}oa:{if(!(h&128)){b=sa(a,1);break oa}b=Ga(a,m,b);if(!b){break Z}b=y[b+68>>2]}y[j+40>>2]=b}W=z[j+1|0];V=z[j|0];b=z[j+3|0];c=z[j+2|0];y[e+100>>2]=0;y[e+92>>2]=0;y[e+96>>2]=0;y[e+88>>2]=0;y[e+80>>2]=0;y[e+84>>2]=0;c=c?1024:8192;b=b?(b|0)==1?8192:1024:65536;pa:{if(!(!(h&256)|!s)){if(!(V&1)){d=Sb(e+92|0,y[s+56>>2]);if((b|0)!=y[e+96>>2]-y[d>>2]>>3){break _}}if(!(W&1)){break pa}b=Sb(e+80|0,y[s+56>>2]+12|0);if((c|0)==y[e+84>>2]-y[b>>2]>>3){break pa}break _}if(!(V&1)){kb(e+92|0,b)}if(!(W&1)){break pa}kb(e+80|0,c)}b=y[m+44>>2];d=b;c=y[m+40>>2];y[e+64>>2]=c;y[e+68>>2]=b;g=y[m+36>>2];y[e+72>>2]=g;y[m+52>>2]=2;qa:{if(!(b|c)|z[a+48|0]!=1){break qa}i=y[a+108>>2];b=i;while(1){b=y[b+4>>2];if((i|0)==(b|0)){break qa}if((c|0)!=y[b+8>>2]|(d|0)!=y[b+12>>2]|(g|0)!=y[b+16>>2]){continue}break}c=Ad(y[b+24>>2]);y[e+120>>2]=0;Rb(m+56|0,c);Za(e+120|0);zd(y[a+108>>2],e- -64|0,b+24|0);c=y[a+108>>2];d=y[b>>2];g=y[b+4>>2];y[d+4>>2]=g;y[g>>2]=d;y[c+8>>2]=y[c+8>>2]-1;oc(b);break aa}if(!(V&1)){u=Ra(y[a+4>>2]);y[e+60>>2]=u;I=y[e+92>>2];d=y[e+96>>2];C=y[e+80>>2];g=y[e+84>>2];ba=Fa();y[e+268>>2]=ba;ca=Fa();y[e+252>>2]=ca;da=Fa();y[e+248>>2]=da;ea=Fa();y[e+244>>2]=ea;i=y[j+8>>2]+y[j+4>>2]|0;b=0;while(1){c=b;b=b+1|0;if(i>>>0>1<>>0){continue}break}_=I+3240|0;S=g-C>>3;P=Cd(e+120|0,c&255);O=gb(e+256|0,y[j+8>>2]);G=d-I>>3;X=G>>>0>405;i=0;F=0;while(1){ra:{sa:{ta:{b=y[j+8>>2];if(b>>>0>i>>>0){y[e+240>>2]=0;Aa(ba,u,e+236|0);F=y[e+236>>2]+F|0;if(F>>>0>65535){break ta}K=0;while(1){ua:{l=Aa(ca,u,e+232|0);if(!l|B[j+8>>2]<=i>>>0){break ua}l=1;K=y[e+232>>2]+K|0;if(K>>>0>65535){break ua}if(!(!F|!K)){va:{wa:{xa:{ya:{za:{if(!z[j+1|0]){r=qb();y[e+228>>2]=r;y[r+8>>2]=F;y[r+4>>2]=K;w[r|0]=0;b=z[j+3|0];w[r+1|0]=0;w[r+2|0]=0;w[r+3|0]=b;w[r+16|0]=z[j+44|0];w[r+17|0]=z[j+45|0];w[r+18|0]=z[j+46|0];w[r+19|0]=z[j+47|0];w[r+20|0]=z[j+48|0];w[r+21|0]=z[j+49|0];w[r+22|0]=z[j+50|0];w[r+23|0]=z[j+51|0];Aa:{switch(b|0){case 0:if(xd(r)){D=nc(r,u,I,G,0);break wa}D=mc(r,u,I,G,0);break wa;case 1:if(wd(r)){D=nc(r,u,I,G,1);break wa}D=mc(r,u,I,G,1);break wa;case 2:if(Qb(r)){D=nc(r,u,I,G,2);break wa}D=mc(r,u,I,G,2);break wa;default:break Aa}}if(!Qb(r)){break za}D=xa(K,F);y[e+160>>2]=D;if(!qa(D)){break ya}b=y[r+4>>2];if(!b){break fa}b=b-1|0;L=b>>>3|0;T=(b&7)+1|0;g=0;k=0;n=0;c=0;while(1){Ba:{Ca:{if(B[r+8>>2]>n>>>0){ja(e+192|0,D,n);h=y[e+196>>2];q=y[e+192>>2];if(z[r+1|0]!=1){b=c;break Ca}if(z[u|0]){break ya}if(!X){break fa}b=0;d=ka(u,_);if((d|0)==(c|0)){break Ca}y[e+184>>2]=h;y[e+180>>2]=q;y[e+172>>2]=k;y[e+168>>2]=g;b=y[e+184>>2];y[e+8>>2]=y[e+180>>2];y[e+12>>2]=b;b=y[e+172>>2];y[e>>2]=y[e+168>>2];y[e+4>>2]=b;Pa(e+8|0,e);c=c^d;break Ba}y[e+160>>2]=0;break xa}c=0;H=0;Da:{if(!n){Ea:{while(1){k=7;v=0;if((H|0)==(L|0)){break Ea}Fa:{while(1){if((k|0)>=0){if(z[u|0]){break Fa}if(c>>>0>=G>>>0){break fa}d=ka(u,(c<<3)+I|0);c=d|c<<1&1006;v=d<>>0>>0){break ya}}k=0;v=0;while(1){if((k|0)==(T|0)){break Da}if(z[u|0]){break ya}if(c>>>0>=G>>>0){break fa}d=ka(u,(c<<3)+I|0);c=d|c<<1&1006;v=d<<7-k|v;k=k+1|0;continue}}if(!k){break fa}$=k-1|0;H=z[g|0];k=H>>>1&112;d=0;while(1){Ga:{if((d|0)!=(L|0)){if((d|0)==($|0)){break fa}s=d+1|0;H=z[s+g|0]|H<<8;c=7;v=0;while(1){if((c|0)<0){break Ga}if(z[u|0]){break ya}if(k>>>0>=G>>>0){break fa}M=ka(u,(k<<3)+I|0);k=M|(H>>>c+1&16|k<<1&1006);v=M<>>0>=G>>>0){break fa}g=ka(u,(k<<3)+I|0);k=g|(d>>>8-c&16|k<<1&1006);v=g<<7-c|v;c=c+1|0;continue}break}break Da}if((d|0)==(h|0)){break fa}w[d+q|0]=v;d=s;continue}}if(h>>>0<=L>>>0){break fa}w[q+L|0]=v;c=b}n=n+1|0;g=q;k=h;continue}}Aa(da,u,e+180|0);d=y[e+180>>2];if(d>>>0>=2){g=wa(6);y[e+168>>2]=g;h=wa(8);y[e+160>>2]=h;k=wa(11);y[e+228>>2]=k;l=wa(15);y[e+224>>2]=l;n=wa(15);y[e+220>>2]=n;q=wa(15);y[e+216>>2]=q;s=wa(15);y[e+212>>2]=s;r=wa(1);y[e+208>>2]=r;c=Yb();y[e+204>>2]=c;b=z[j|0];y[c+20>>2]=1;y[c+16>>2]=d;y[c+12>>2]=F;y[c+8>>2]=K;w[c+1|0]=1;w[c|0]=b;b=y[j+4>>2]+i|0;y[c+24>>2]=b;d=Ub(e+192|0,b);b=y[j+4>>2];v=y[j+16>>2];if(b>>>0>y[j+20>>2]-v>>2>>>0){break fa}M=b;b=y[d>>2];vd(v,M,b,y[e+196>>2]-b>>2);v=b+(y[j+4>>2]<<2)|0;D=y[O>>2];b=0;while(1)if((b|0)==(i|0)){bb(c+40|0,d);x[c+4>>1]=0;y[c+52>>2]=0;y[c+56>>2]=1;w[c+3|0]=0;y[c+88>>2]=r;y[c+84>>2]=s;y[c+80>>2]=q;y[c+76>>2]=n;y[c+72>>2]=l;y[c+68>>2]=k;y[c+64>>2]=h;y[c+60>>2]=g;w[c+2|0]=z[j+2|0];w[c+92|0]=z[j+52|0];w[c+93|0]=z[j+53|0];w[c+94|0]=z[j+54|0];w[c+95|0]=z[j+55|0];b=Bd(c,u,C,S,P);y[e+176>>2]=0;oa(e+240|0,b);la(e+176|0);b=y[e+240>>2];ya(d);pc(e+204|0);pa(e+208|0);pa(e+212|0);pa(e+216|0);pa(e+220|0);pa(e+224|0);pa(e+228|0);pa(e+160|0);pa(e+168|0);if(!b){break ta}break va}else{H=b<<2;y[H+v>>2]=y[D+H>>2];b=b+1|0;continue}}b=0;if((d|0)!=1){break va}c=y[j+4>>2];Ac(y[P+36>>2],u,e+192|0);b=y[e+192>>2];if(b>>>0>=c+i>>>0){break ta}c=y[j+4>>2];if(c>>>0>b>>>0){b=y[Q>>2]+(b<<2)|0}else{b=b-c|0;c=y[O>>2];if(b>>>0>=y[O+4>>2]-c>>2>>>0){break fa}b=c+(b<<2)|0}c=y[b>>2];if(!c){break ta}Aa(y[P+28>>2],u,e+168|0);Aa(y[P+32>>2],u,e+160|0);b=db();y[e+228>>2]=b;y[b+8>>2]=F;y[b+4>>2]=K;d=z[j+2|0];y[b+20>>2]=c;w[b|0]=d;y[b+12>>2]=y[e+168>>2];c=y[e+160>>2];w[b+1|0]=0;y[b+16>>2]=c;w[b+24|0]=z[j+52|0];w[b+25|0]=z[j+53|0];w[b+26|0]=z[j+54|0];w[b+27|0]=z[j+55|0];b=cb(b,u,C,S);y[e+224>>2]=0;oa(e+240|0,b);la(e+224|0);b=y[e+240>>2];Ua(e+228|0);if(!b){break ta}break va}D=xa(K,F);y[e+160>>2]=D;Ha:{Ia:{if(!qa(D)){break Ia}n=0;Qa(D,0);h=0;c=0;b=0;while(1){Ja:{Ka:{if(B[r+8>>2]>b>>>0){ja(e+192|0,D,b);g=y[e+196>>2];q=y[e+192>>2];if(z[r+1|0]!=1){d=c;break Ka}if(z[u|0]){break Ia}if(!X){break fa}d=0;k=ka(u,_);if((k|0)==(c|0)){break Ka}y[e+184>>2]=g;y[e+180>>2]=q;y[e+172>>2]=h;y[e+168>>2]=n;d=y[e+184>>2];y[e+24>>2]=y[e+180>>2];y[e+28>>2]=d;d=y[e+172>>2];y[e+16>>2]=y[e+168>>2];y[e+20>>2]=d;c=c^k;D=y[e+160>>2];Pa(e+24|0,e+16|0);break Ja}y[e+160>>2]=0;break Ha}H=0;L=0;if(z[r+2|0]==1){ja(e+192|0,y[r+12>>2],b);L=y[e+192>>2];H=y[e+196>>2]}D=y[e+160>>2];ja(e+192|0,D,w[r+17|0]+b|0);T=y[e+196>>2];$=y[e+192>>2];s=z[n|0]>>>6|0;k=y[D+8>>2];c=!h|(k|0)<2?0:s&1;v=!h|(k|0)<=0?c:s&2|c;s=0;c=0;while(1){if(B[r+4>>2]<=c>>>0){c=d;break Ja}La:{Ma:{if(z[r+2|0]==1){if(ia(y[y[r+12>>2]+8>>2],c,L,H)){break Ma}}k=ia(k,w[r+16|0]+c|0,$,T);if(z[u|0]){break Ia}k=v<<5|k<<4|s;if(k>>>0>=G>>>0){break fa}M=ka(u,(k<<3)+I|0);k=y[D+8>>2];if(!M){break Ma}La(k,c,q,g,1);k=y[D+8>>2];M=1;break La}M=0}v=ia(k,c+2|0,n,h)|v<<1&30;c=c+1|0;s=s<<1&14|M;continue}}b=b+1|0;n=q;h=g;continue}}D=0}la(e+160|0);break wa}D=0}la(e+160|0)}y[e+192>>2]=0;oa(e+240|0,D);la(e+192|0);b=y[e+240>>2];ua(e+228|0);if(!b){break ua}}c=y[O>>2];y[e+240>>2]=0;oa((i<<2)+c|0,b)}i=i+1|0;continue}break}b=l^1;break sa}y[e+200>>2]=0;y[e+192>>2]=0;y[e+196>>2]=0;ud(e+192|0,b+y[j+4>>2]|0);g=y[e+192>>2];i=0;b=0;k=0;Na:{while(1){if(y[j+8>>2]+y[j+4>>2]>>>0>b>>>0){Aa(ea,u,e+160|0);d=y[e+160>>2];c=d+b|0;if(!(c>>>0<=y[j+8>>2]+y[j+4>>2]>>>0&c>>>0>=d>>>0)){break Na}Pb(e+180|0,g,0,b);sd(e+168|0,y[e+180>>2],y[e+184>>2],d,i);k=(i&1?d:0)+k|0;i=(i^-1)&1;b=c;continue}break}if(B[j+12>>2]>>0){break Na}N=lc();d=y[O>>2];b=0;l=0;while(1){c=y[j+4>>2];if(c+y[j+8>>2]>>>0<=b>>>0){break Na}if(!(!(y[g+(b>>>3&536870908)>>2]>>>b&1)|B[j+12>>2]<=l>>>0)){Oa:{if(b>>>0>>0){c=y[y[Q>>2]+(b<<2)>>2];if(!c){i=0;break Oa}i=kc(c);break Oa}c=d+(b-c<<2)|0;i=y[c>>2];y[c>>2]=0}rd(N,i);l=l+1|0}b=b+1|0;continue}}Ob(e+192|0);break ra}b=0}la(e+240|0);if(b){continue}}break}$a(O);qc(P);Ea(e+244|0);Ea(e+248|0);Ea(e+252|0);Ea(e+268|0);y[e+120>>2]=0;Rb(m+56|0,N);Za(e+120|0);if(y[m+56>>2]){ta(y[a+4>>2]);b=y[a+4>>2];c=y[b+8>>2];if(c>>>0<=4294967293){g=b;c=c+2|0;b=y[b+4>>2];y[g+8>>2]=b>>>0>c>>>0?c:b}ua(e+60|0);break ba}ua(e+60|0);break $}g=y[e+80>>2];b=y[e+84>>2];n=y[a+4>>2];q=qd(n);y[e+268>>2]=q;C=gb(e+120|0,y[j+8>>2]);y[e+264>>2]=0;y[e+256>>2]=0;y[e+260>>2]=0;if(!z[j+1|0]){xc(e+256|0,y[j+8>>2])}u=b-g>>3;y[e+252>>2]=0;s=0;d=0;Pa:while(1){if(B[j+8>>2]<=d>>>0){break da}if(za(q,y[j+28>>2],e+160|0)){break ea}s=y[e+160>>2]+s|0;if(s>>>0>65535){break ea}F=0;c=0;i=d;while(1){b=za(q,y[j+32>>2],e+248|0);y[e+244>>2]=b;Qa:{Ra:{Sa:{Ta:{Ua:{switch(b|0){case 0:if(B[j+8>>2]<=i>>>0){break ea}c=y[e+248>>2]+c|0;if(c>>>0>65535){break ea}if(!c|!s){break Qa}if(z[j+1|0]!=1){break Ra}if(za(q,y[j+40>>2],e+240|0)){break ea}oa(e+252|0,0);h=y[e+240>>2];if(h>>>0<2){break Ta}v=wa(6);y[e+236>>2]=v;D=wa(8);y[e+232>>2]=D;H=wa(11);y[e+228>>2]=H;I=wa(15);y[e+224>>2]=I;G=wa(15);y[e+220>>2]=G;K=wa(15);y[e+216>>2]=K;O=wa(15);y[e+212>>2]=O;b=1;P=wa(1);y[e+208>>2]=P;l=Yb();y[e+204>>2]=l;k=z[j|0];y[l+20>>2]=1;y[l+16>>2]=h;y[l+12>>2]=s;y[l+8>>2]=c;w[l+1|0]=1;w[l|0]=k;h=y[j+4>>2]+i|0;y[l+24>>2]=h;r=Ed(e+192|0,h);k=y[j+8>>2]+y[j+4>>2]|0;while(1){h=b;b=b+1|0;if(k>>>0>1<>>0){continue}break};h=h&255;k=y[r>>2];L=y[l+24>>2];b=0;while(1)if((b|0)==(L|0)){bb(l+28|0,r);h=Ub(e+180|0,y[l+24>>2]);b=y[j+4>>2];k=y[j+16>>2];if(b>>>0>y[j+20>>2]-k>>2>>>0){break fa}M=b;b=y[h>>2];vd(k,M,b,y[e+184>>2]-b>>2);L=b+(y[j+4>>2]<<2)|0;k=y[C>>2];b=0;while(1)if((b|0)==(i|0)){bb(l+40|0,h);x[l+4>>1]=0;y[l+52>>2]=0;y[l+56>>2]=1;w[l+3|0]=0;y[l+88>>2]=P;y[l+84>>2]=O;y[l+80>>2]=K;y[l+76>>2]=G;y[l+72>>2]=I;y[l+68>>2]=H;y[l+64>>2]=D;y[l+60>>2]=v;w[l+2|0]=z[j+2|0];w[l+92|0]=z[j+52|0];w[l+93|0]=z[j+53|0];w[l+94|0]=z[j+54|0];w[l+95|0]=z[j+55|0];b=Dd(l,n,g,u);y[e+176>>2]=0;oa(e+252|0,b);la(e+176|0);b=y[e+252>>2];ya(h);ya(r);pc(e+204|0);pa(e+208|0);pa(e+212|0);pa(e+216|0);pa(e+220|0);pa(e+224|0);pa(e+228|0);pa(e+232|0);pa(e+236|0);if(b){break Sa}break ea}else{N=b<<2;y[N+L>>2]=y[k+N>>2];b=b+1|0;continue}}else{N=k+(b<<3)|0;y[N+4>>2]=b;y[N>>2]=h;b=b+1|0;continue};case 1:break Ua;default:break ea}}Va:{if(!z[j+1|0]){if(za(q,y[j+36>>2],e+192|0)){break ea}ta(n);y[e+180>>2]=0;Wa:{if(!y[e+192>>2]){if((F|0)>65535){break Va}b=F+7>>>3|0;c=se(b,0,s);if(Z){break Va}h=y[n+4>>2];k=y[n+8>>2];if(h>>>0>>0){break fa}if(c>>>0>h-k>>>0){break Va}c=0;h=xa(F,s);y[e+248>>2]=0;oa(e+180|0,h);la(e+248|0);v=y[e+180>>2];while(1){if((c|0)==(s|0)){break Wa}ja(e+168|0,v,c);l=y[n+8>>2];h=y[n+4>>2];if(l>>>0>h>>>0|b>>>0>h-l>>>0|b>>>0>B[e+172>>2]){break fa}if(b){if(b){p(y[e+168>>2],y[n>>2]+l|0,b)}l=y[n+8>>2]}h=b+l|0;if(h>>>0>=l>>>0){k=y[n+4>>2];y[n+8>>2]=h>>>0>>0?h:k}c=c+1|0;continue}}b=qb();y[e+248>>2]=b;y[b+8>>2]=s;y[b+4>>2]=F;w[b|0]=1;hb(b,e+180|0,n);ta(n);ua(e+248|0);v=y[e+180>>2]}Xa:{if(!v){break Xa}c=d>>>0>i>>>0?d:i;h=y[C>>2];b=0;k=y[e+256>>2];while(1){if((c|0)==(d|0)){break Xa}l=d<<2;r=l+k|0;F=sb(v,b,0,y[r>>2],s);y[e+248>>2]=0;oa(h+l|0,F);d=d+1|0;la(e+248|0);b=y[r>>2]+b|0;continue}}la(e+180|0)}d=i;continue Pa}la(e+180|0);break ea}if((h|0)!=1){k=y[C>>2];b=y[e+252>>2];break Sa}r=y[j+8>>2]+y[j+4>>2]|0;b=1;l=1;while(1){k=b;b=b+1|0;h=l;l=h+1|0;if(r>>>0>1<>>0){continue}break}y[e+192>>2]=h;h=k&255;l=0;b=0;while(1){if((b|0)!=(h|0)){if(wb(n,e+192|0)){break ea}b=b+1|0;l=y[e+192>>2]|l<<1;continue}break}b=y[j+4>>2];if(b+i>>>0<=l>>>0){break ea}k=y[C>>2];if(b>>>0>l>>>0){b=y[Q>>2]+(l<<2)|0}else{b=l-b|0;if(b>>>0>=y[C+4>>2]-k>>2>>>0){break fa}b=(b<<2)+k|0}h=y[b>>2];if(!h){break ea}b=wa(15);y[e+180>>2]=b;l=wa(1);y[e+236>>2]=l;Ya:{if(za(q,b,e+232|0)){break Ya}if(za(q,b,e+228|0)){break Ya}if(za(q,l,e+244|0)){break Ya}ta(n);r=y[n+8>>2];y[e+192>>2]=r;b=db();y[e+224>>2]=b;y[b+8>>2]=s;y[b+4>>2]=c;l=z[j+2|0];y[b+20>>2]=h;w[b|0]=l;y[b+12>>2]=y[e+232>>2];h=y[e+228>>2];l=0;w[b+1|0]=0;y[b+16>>2]=h;w[b+24|0]=z[j+52|0];w[b+25|0]=z[j+53|0];w[b+26|0]=z[j+54|0];w[b+27|0]=z[j+55|0];h=Ra(n);y[e+220>>2]=h;b=cb(b,h,g,u);y[e+216>>2]=0;oa(e+252|0,b);la(e+216|0);b=y[e+252>>2];if(b){ta(n);l=y[n+8>>2];if(l>>>0<=4294967293){h=l+2|0;l=y[n+4>>2];l=h>>>0>>0?h:l;y[n+8>>2]=l}l=y[e+244>>2]==(l-r|0)}ua(e+220|0);Ua(e+224|0);pa(e+236|0);pa(e+180|0);if(l){break Sa}break ea}pa(e+236|0);pa(e+180|0);break ea}y[e+252>>2]=0;oa((i<<2)+k|0,b);if(z[j+1|0]){break Qa}}y[y[e+256>>2]+(i<<2)>>2]=c}F=c+F|0;i=i+1|0;continue}}}o()}b=0;break ca}i=wa(1);y[e+248>>2]=i;y[e+200>>2]=0;y[e+192>>2]=0;y[e+196>>2]=0;ud(e+192|0,y[j+8>>2]+y[j+4>>2]|0);g=y[e+192>>2];l=0;b=0;k=0;Za:{while(1){if(y[j+8>>2]+y[j+4>>2]>>>0>b>>>0){_a:{$a:{if(za(q,i,e+244|0)){break $a}d=y[e+244>>2];c=d+b|0;if(c>>>0>>0){break $a}if(c>>>0<=y[j+8>>2]+y[j+4>>2]>>>0){break _a}}b=0;break Za}Pb(e+180|0,g,0,b);sd(e+160|0,y[e+180>>2],y[e+184>>2],d,l);k=(l&1?d:0)+k|0;l=(l^-1)&1;b=c;continue}break}b=0;if(B[j+12>>2]>>0){break Za}b=lc();d=y[C>>2];l=0;i=0;while(1){c=y[j+4>>2];if(c+y[j+8>>2]>>>0<=l>>>0){break Za}if(!(!(y[g+(l>>>3&536870908)>>2]>>>l&1)|B[j+12>>2]<=i>>>0)){ab:{if(c>>>0>l>>>0){c=y[y[Q>>2]+(l<<2)>>2];if(!c){c=0;break ab}c=kc(c);break ab}h=d+(l-c<<2)|0;c=y[h>>2];y[h>>2]=0}rd(b,c);i=i+1|0}l=l+1|0;continue}}Ob(e+192|0);pa(e+248|0)}la(e+252|0);Ja(e+256|0);$a(C);Ua(e+268|0);y[e+120>>2]=0;Rb(m+56|0,b);Za(e+120|0);if(!y[m+56>>2]){break $}ta(y[a+4>>2])}if(z[a+48|0]!=1){break aa}fa=e,ga=Ad(y[m+56>>2]),y[fa+120>>2]=ga;b=y[a+108>>2];l=y[b+8>>2];while(1){if(l>>>0>=2){c=y[b>>2];d=y[c>>2];g=y[c+4>>2];y[d+4>>2]=g;y[g>>2]=d;y[b+8>>2]=y[b+8>>2]-1;oc(c);l=l-1|0;b=y[a+108>>2];continue}break}c=b;b=e+120|0;zd(c,e- -64|0,b);Za(b)}l=0;if(!(z[e+119|0]&2)){break _}if(!(V&1)){b=y[m+56>>2];y[e+48>>2]=y[e+92>>2];c=y[e+96>>2];d=y[e+100>>2];y[e+96>>2]=0;y[e+100>>2]=0;y[e+52>>2]=c;y[e+56>>2]=d;y[e+92>>2]=0;c=b;b=e+48|0;pd(c,b);Ja(b)}if(!(W&1)){break _}b=y[m+56>>2];y[e+36>>2]=y[e+80>>2];c=y[e+84>>2];d=y[e+88>>2];y[e+84>>2]=0;y[e+88>>2]=0;y[e+40>>2]=c;y[e+44>>2]=d;y[e+80>>2]=0;c=b+12|0;b=e+36|0;pd(c,b);Ja(b);break _}l=1}Ja(e+80|0);Ja(e+92|0)}ya(U);break W}else{d=Ma(a,y[y[m+12>>2]+(b<<2)>>2]);if(!(z[d+4|0]&63)){g=0;if(c&1){c=y[d+56>>2];c=y[c+28>>2]-y[c+24>>2]>>2;g=c+k|0;n=g;g=c>>>0<=g>>>0;k=g?n:0}else{k=0}s=d;c=g}b=b+1|0;continue}}c=b<<2;b=b+1|0;if(Ma(a,y[c+y[m+12>>2]>>2])){continue}break}}ya(Q);ma(j)}Y=e+272|0;b=l;break u;case 1:case 2:case 3:case 5:break B;case 4:case 6:case 7:break V;default:break K}}b=1;if(z[a+49|0]!=1){break u}if(tb(a,f+332|0)){break u}d=f+14|0;if(Wa(y[a+4>>2],d)){break u}c=y[f+332>>2];if(c-1>>>0>65534){break u}q=y[f+336>>2];if(q-1>>>0>65534){break u}h=Yb();y[f+308>>2]=h;y[h+12>>2]=q;y[h+8>>2]=c;c=A[f+14>>1];g=c&1;w[h|0]=g;w[h+2|0]=c>>>15;y[h+56>>2]=c>>>4&3;y[h+52>>2]=c>>>7&3;w[h+3|0]=c>>>6&1;w[h+4|0]=c>>>9&1;y[h+20>>2]=1<<(c>>>2&3);i=(c&254)>>>1&1;w[h+1|0]=i;k=c>>>10&31;w[h+5|0]=(k>>>0>15?-32:0)|k;bb:{if(g){if(Wa(y[a+4>>2],d)){break v}if(z[h+1|0]!=1){break w}if(!z[h+2|0]){break bb}break w}if(!i|c<<16>>16<0){break w}}c=h+92|0;d=0;while(1){if((d|0)==4){break w}g=c+d|0;d=d+1|0;if(!va(y[a+4>>2],g)){continue}break}break v}g=na(12);y[g+8>>2]=0;y[g>>2]=0;y[g+4>>2]=0;if(va(y[a+4>>2],f+352|0)){break y}b=1;if(va(y[a+4>>2],g+1|0)){break x}if(va(y[a+4>>2],g+2|0)){break x}if(ra(y[a+4>>2],g+4|0)){break x}i=y[g+4>>2];if(i>>>0>65535){break x}b=z[f+352|0];d=b&1;w[g|0]=d;b=b>>>1&3;w[g+8|0]=b;y[m+52>>2]=3;if(d){d=y[a+4>>2];b=wc(g);y[f+24>>2]=b;if(b){y[f+332>>2]=0;hb(b,f+332|0,d);cb:{d=y[f+332>>2];if(!d){break cb}c=vc(i+1|0);b=0;while(1){if(b>>>0>i>>>0){break cb}h=z[g+1|0];h=sb(d,E(h,b),0,h,z[g+2|0]);k=y[c+4>>2];y[f+312>>2]=0;oa((b<<2)+k|0,h);b=b+1|0;la(f+312|0);continue}}la(f+332|0)}ua(f+24|0);y[f+332>>2]=0;Xb(m+60|0,c);Zb(f+332|0);if(!y[m+60>>2]){break y}ta(y[a+4>>2]);b=0;break x}k=b?(b|0)==1?8192:1024:65536;l=ab(k,8);y[f+336>>2]=k;y[f+332>>2]=l;n=Ra(y[a+4>>2]);y[f+16>>2]=n;d=wc(g);y[f+312>>2]=d;if(!d){break z}w[d+1|0]=0;w[d+2|0]=0;w[d+3|0]=b;h=z[g+1|0];w[d+17|0]=0;w[d+16|0]=0-h;if(!b){w[d+22|0]=254;w[d+23|0]=254;w[d+18|0]=253;w[d+19|0]=255;w[d+20|0]=2;w[d+21|0]=254}y[f+356>>2]=0;y[f+40>>2]=0;y[f+32>>2]=l;y[f+36>>2]=k;y[f+28>>2]=n;y[f+24>>2]=f+356;b=Wb(d,f+24|0);y[f+40>>2]=0;while(1){if((b|0)==3){b=Vb(d,f+24|0);continue}break}y[d+44>>2]=0;y[d+48>>2]=0;y[d+36>>2]=0;y[d+40>>2]=0;y[d+28>>2]=0;y[d+32>>2]=0;if(!y[f+356>>2]){break A}c=vc(i+1|0);b=0;while(1){if(b>>>0>i>>>0){break A}d=sb(y[f+356>>2],E(b,h),0,h,z[g+2|0]);k=y[c+4>>2];y[f>>2]=0;oa((b<<2)+k|0,d);b=b+1|0;la(f);continue}}b=1;if(z[a+49|0]!=1){break u}q=na(56);y[q+16>>2]=0;db:{if(tb(a,f+312|0)){break db}if(va(y[a+4>>2],f+14|0)){break db}if(ra(y[a+4>>2],q+32|0)){break db}if(ra(y[a+4>>2],q+36|0)){break db}if(ra(y[a+4>>2],q+40|0)){break db}if(ra(y[a+4>>2],q+44|0)){break db}if(Wa(y[a+4>>2],q+48|0)){break db}if(Wa(y[a+4>>2],q+50|0)){break db}C=y[q+32>>2];if(C-1>>>0>65534){break db}e=y[q+36>>2];if(e-1>>>0>65534){break db}v=y[f+312>>2];if(v-1>>>0>65534){break db}k=y[f+316>>2];if(k-1>>>0>65534){break db}y[q+4>>2]=k;y[q>>2]=v;d=z[f+14|0];F=d&1;w[q+8|0]=F;w[q+20|0]=d>>>7;i=d>>>3|0;K=i&1;w[q+28|0]=K;s=d>>>1&3;w[q+9|0]=s;g=d>>>4|0;y[q+24>>2]=(g&7)==4?4:g&3;if(y[m+8>>2]!=1){break db}g=Ma(a,y[y[m+12>>2]>>2]);if(!g|(z[g+4|0]&63)!=16){break db}g=y[g+60>>2];if(!g){break db}j=y[g>>2];if(!j){break db}y[q+12>>2]=j;y[q+16>>2]=g+4;b=y[y[g+4>>2]>>2];g=y[b+8>>2];w[q+52|0]=g;r=y[b+12>>2];w[q+53|0]=r;y[m+52>>2]=1;eb:{fb:{gb:{if(d&1){d=y[a+4>>2];b=1;while(1){c=b;b=b+1|0;if(j>>>0>1<>>0){continue}break}x[f+80>>1]=0;y[f+36>>2]=0;y[f+48>>2]=0;y[f+52>>2]=0;y[f+56>>2]=0;y[f+60>>2]=0;y[f+64>>2]=0;y[f+68>>2]=0;y[f+72>>2]=0;y[f+84>>2]=0;y[f+88>>2]=0;y[f+92>>2]=0;y[f+96>>2]=0;y[f+100>>2]=0;y[f+32>>2]=e;y[f+28>>2]=C;w[f+24|0]=F;b=c&255;g=gb(f+332|0,b);i=y[g>>2];c=(i+(b<<2)|0)-4|0;hb(f+24|0,c,d);h=0;hb:{if(!y[c>>2]){break hb}ta(d);c=y[d+8>>2];if(c>>>0<=4294967292){c=c+3|0;h=y[d+4>>2];y[d+8>>2]=c>>>0>>0?c:h}b=b-2|0;while(1){if((b|0)>=0){c=i+(b<<2)|0;hb(f+24|0,c,d);h=0;if(!y[c>>2]){break hb}ta(d);h=y[d+8>>2];if(h>>>0<=4294967292){h=h+3|0;l=y[d+4>>2];y[d+8>>2]=h>>>0>>0?h:l}fb(y[c>>2],0,0,0,0,y[c+4>>2],2);b=b-1|0;continue}break}h=uc(q,g)}b=h;$a(g);y[f+356>>2]=0;oa(m- -64|0,b);la(f+356|0);if(!y[m+64>>2]){break eb}ta(y[a+4>>2]);break gb}b=s?(s|0)==1?8192:1024:65536;d=ab(b,8);y[f+4>>2]=b;y[f>>2]=d;Q=Ra(y[a+4>>2]);y[f+308>>2]=Q;n=0;y[f+16>>2]=0;l=b;ib:{if(!(i&1)){break ib}b=xa(C,e);y[f+24>>2]=0;oa(f+16|0,b);b=r&255;O=1-b|0;D=0-(b>>>0>1)|0;b=g&255;P=1-b|0;H=0-(b>>>0>1)|0;la(f+24|0);n=y[f+16>>2];jb:while(1){if(!c&(e|0)==(h|0)){break ib}ja(f+24|0,n,h);b=y[q+44>>2];L=A[q+48>>1];g=se(h,c,L);r=b+g|0;b=Z+(b>>31)|0;V=g>>>0>r>>>0?b+1|0:b;b=y[q+40>>2];N=A[q+50>>1];g=se(h,c,N);I=b+g|0;b=Z+(b>>31)|0;U=g>>>0>I>>>0?b+1|0:b;g=0;i=0;W=y[f+28>>2];_=y[f+24>>2];while(1)if(!i&(g|0)==(C|0)){h=h+1|0;c=h?c:c+1|0;continue jb}else{S=y[n+8>>2];b=se(g,i,N);u=r-b|0;G=V-(Z+(b>>>0>r>>>0)|0)|0;b=G>>8;X=(G&255)<<24|u>>>8;T=se(g,i,L);u=T+I|0;G=Z+U|0;M=S;G=u>>>0>>0?G+1|0:G;S=(G&255)<<24|u>>>8;u=G>>8;La(M,g,_,W,k>>>0<=X>>>0&(b|0)>=0|(b|0)>0|(P>>>0>S>>>0&(H|0)>=(u|0)|(u|0)<(H|0)|((b|0)<=(D|0)&O>>>0>X>>>0|(b|0)<(D|0))|(v>>>0<=S>>>0&(u|0)>=0|(u|0)>0)));g=g+1|0;i=g?i:i+1|0;continue}}}g=d;b=1;while(1){c=b;b=b+1|0;if(j>>>0>1<>>0){continue}break}y[f+48>>2]=0;y[f+52>>2]=0;x[f+80>>1]=0;y[f+56>>2]=0;y[f+60>>2]=0;y[f+64>>2]=0;y[f+68>>2]=0;y[f+72>>2]=0;y[f+84>>2]=0;y[f+88>>2]=0;y[f+92>>2]=0;y[f+96>>2]=0;y[f+100>>2]=0;y[f+32>>2]=e;y[f+28>>2]=C;w[f+24|0]=F;y[f+36>>2]=n;w[f+26|0]=K;w[f+25|0]=0;w[f+27|0]=s;kb:{if(s>>>0>=2){x[f+40>>1]=65282;break kb}x[f+40>>1]=65283;if(s){break kb}x[f+46>>1]=65278;x[f+42>>1]=65533;x[f+44>>1]=65026}b=c&255;c=gb(f+356|0,b);h=y[c>>2];n=b-1|0;d=n;lb:{mb:{while(1){if((d|0)<0){break mb}y[f+352>>2]=0;y[f+348>>2]=0;y[f+340>>2]=g;y[f+344>>2]=l;y[f+336>>2]=Q;y[f+332>>2]=f+352;b=Wb(f+24|0,f+332|0);y[f+348>>2]=0;while(1){if((b|0)==3){b=Vb(f+24|0,f+332|0);continue}break}y[f+68>>2]=0;y[f+72>>2]=0;y[f+60>>2]=0;y[f+64>>2]=0;y[f+52>>2]=0;y[f+56>>2]=0;b=y[f+352>>2];nb:{if(!b){break nb}y[f+352>>2]=0;i=h+(d<<2)|0;oa(i,b);if((d|0)>=(n|0)){break nb}fb(y[i>>2],0,0,0,0,y[i+4>>2],2)}d=d-1|0;la(f+352|0);if(b){continue}break}b=0;break lb}b=uc(q,c)}$a(c);la(f+16|0);y[f+24>>2]=0;oa(m- -64|0,b);la(f+24|0);if(!y[m+64>>2]){break fb}ta(y[a+4>>2]);b=y[a+4>>2];c=y[b+8>>2];if(c>>>0<=4294967293){g=b;c=c+2|0;b=y[b+4>>2];y[g+8>>2]=b>>>0>c>>>0?c:b}ua(f+308|0);Ha(f)}if((z[m+4|0]&63)==20){b=0;break db}ob:{if(z[a+50|0]){break ob}b=y[y[a+24>>2]-4>>2];if(z[b+17|0]!=1){break ob}c=k+y[f+324>>2]|0;d=y[a+32>>2];if((c|0)<=y[d+12>>2]){break ob}eb(d,c,z[b+16|0])}b=y[f+320>>2];g=b;h=b>>31;b=y[f+324>>2];c=b;d=b>>31;b=z[f+328|0];fb(y[a+32>>2],g,h,c,d,y[m+64>>2],(b&7)==4?4:b&3);b=0;oa(m- -64|0,0);break db}ua(f+308|0);Ha(f)}b=1}ma(q);break u}b=1;if(z[a+49|0]!=1){break u}g=a+76|0;d=y[a+76>>2];if(!d){c=qb();y[f+24>>2]=c;pb:{qb:{if(tb(a,a+88|0)){break qb}if(va(y[a+4>>2],f+332|0)){break qb}d=y[a+92>>2];if((d|0)<0){break qb}i=y[a+88>>2];if((i|0)<0){break qb}y[c+8>>2]=d;y[c+4>>2]=i;d=z[f+332|0];i=d&1;w[c|0]=i;h=d>>>1&3;w[c+3|0]=h;w[c+1|0]=d>>>3&1;if(i){break pb}i=c+16|0;d=0;if(!h){while(1){if((d|0)==8){break pb}h=d+i|0;d=d+1|0;if(!va(y[a+4>>2],h)){continue}break qb}}while(1){if((d|0)==2){break pb}h=d+i|0;d=d+1|0;if(!va(y[a+4>>2],h)){continue}break}}ua(f+24|0);break u}w[c+2|0]=0;y[f+24>>2]=0;Va(g,c);ua(f+24|0);d=y[g>>2]}y[m+52>>2]=1;rb:{if(z[d|0]==1){hb(d,m- -64|0,y[a+4>>2]);if(!y[m+64>>2]){Va(g,0);break D}ta(y[a+4>>2]);break rb}if(y[a+60>>2]==y[a+64>>2]){b=z[d+3|0];kb(a+60|0,b?(b|0)==1?8192:1024:65536)}c=a+72|0;h=f;b=y[a+72>>2];if(b){d=b}else{d=Ra(y[a+4>>2]);y[f+24>>2]=0;Va(c,d);ua(f+24|0);d=y[a+72>>2]}y[h+28>>2]=d;d=m- -64|0;y[f+24>>2]=d;h=y[a+64>>2];i=y[a+60>>2];y[f+40>>2]=0;y[f+32>>2]=i;y[f+36>>2]=h-i>>3;i=y[a+76>>2];sb:{if(!b){b=Wb(i,f+24|0);break sb}b=Vb(i,f+24|0)}y[a+56>>2]=b;if((b|0)==3){if((z[m+4|0]&63)==36){break B}tb:{if(z[a+50|0]){break tb}b=y[y[a+24>>2]-4>>2];if(z[b+17|0]!=1){break tb}c=y[a+92>>2]+y[a+100>>2]|0;g=y[a+32>>2];if((c|0)<=y[g+12>>2]){break tb}eb(g,c,z[b+16|0])}b=y[a+76>>2];c=y[b+64>>2]+y[a+96>>2]|0;g=c;i=c>>31;c=y[b+68>>2]+y[a+100>>2]|0;h=b- -64|0;b=z[a+104|0];tc(y[a+32>>2],g,i,c,c>>31,y[d>>2],h,(b&7)==4?4:b&3);break B}b=y[a+76>>2];y[b+44>>2]=0;y[b+48>>2]=0;y[b+36>>2]=0;y[b+40>>2]=0;y[b+28>>2]=0;y[b+32>>2]=0;Va(c,0);y[a+64>>2]=y[a+60>>2];if(!y[d>>2]){y[a+56>>2]=-1;Va(g,0);break D}ta(y[a+4>>2]);b=y[a+4>>2];c=y[b+8>>2];if(c>>>0>4294967293){break rb}d=b;c=c+2|0;b=y[b+4>>2];y[d+8>>2]=b>>>0>c>>>0?c:b}if((z[m+4|0]&63)!=36){ub:{if(z[a+50|0]){break ub}b=y[y[a+24>>2]-4>>2];if(z[b+17|0]!=1){break ub}c=y[a+92>>2]+y[a+100>>2]|0;d=y[a+32>>2];if((c|0)<=y[d+12>>2]){break ub}eb(d,c,z[b+16|0])}b=y[a+76>>2];c=y[b+64>>2]+y[a+96>>2]|0;d=c;i=c>>31;c=y[b+68>>2]+y[a+100>>2]|0;h=b- -64|0;b=z[a+104|0];tc(y[a+32>>2],d,i,c,c>>31,y[m+64>>2],h,(b&7)==4?4:b&3);oa(m- -64|0,0)}b=0;Va(g,0);break u}b=1;if(z[a+49|0]!=1){break u}if(tb(a,f+24|0)){break u}if(va(y[a+4>>2],f+352|0)){break u}i=y[f+24>>2];if(i-1>>>0>65534){break u}g=y[f+28>>2];if(g-1>>>0>65534){break u}d=0;y[f+312>>2]=0;c=db();y[f+356>>2]=c;y[c+8>>2]=g;y[c+4>>2]=i;b=z[f+352|0];h=b&1;w[c|0]=h;w[c+1|0]=b>>>1&1;vb:{wb:{if(h){break wb}h=c+24|0;while(1){if((d|0)==4){break wb}b=1;k=d+h|0;d=d+1|0;if(!va(y[a+4>>2],k)){continue}break}break vb}h=y[m+8>>2];xb:{if((h|0)>0){d=0;b=1;while(1){if((d|0)==(h|0)){break vb}i=Ma(a,y[y[m+12>>2]>>2]);if(!i){break vb}k=(z[i+4|0]&63)-4|0;k=(k<<6|(k&252)>>>2)&255;if(!(1<>>0<=9:0)){d=d+1|0;continue}break}b=i- -64|0;break xb}d=sb(y[a+32>>2],y[f+32>>2],y[f+36>>2],i,g);y[f+332>>2]=0;b=f+312|0;oa(b,d);la(f+332|0)}b=y[b>>2];y[c+12>>2]=0;y[c+16>>2]=0;y[c+20>>2]=b;d=z[c|0];i=d?1024:8192;b=ab(i,8);y[f+336>>2]=i;y[f+332>>2]=b;i=Ra(y[a+4>>2]);y[f>>2]=i;y[m+52>>2]=1;c=cb(c,i,b,d?1024:8192);y[f+16>>2]=0;b=m- -64|0;oa(b,c);la(f+16|0);c=1;yb:{if(!y[m+64>>2]){break yb}ta(y[a+4>>2]);c=y[a+4>>2];d=y[c+8>>2];if(d>>>0<=4294967293){h=c;d=d+2|0;c=y[c+4>>2];y[h+8>>2]=c>>>0>d>>>0?d:c}c=0;if((z[m+4|0]&63)==40){break yb}zb:{if(z[a+50|0]){break zb}c=y[y[a+24>>2]-4>>2];if(z[c+17|0]!=1){break zb}d=g+y[f+36>>2]|0;g=y[a+32>>2];if((d|0)<=y[g+12>>2]){break zb}eb(g,d,z[c+16|0])}c=y[f+32>>2];d=c;i=c>>31;c=y[f+36>>2];g=c;h=c>>31;c=z[f+40|0];fb(y[a+32>>2],d,i,g,h,y[b>>2],(c&7)==4?4:c&3);oa(b,0);c=0}b=c;ua(f);Ha(f+332|0)}Ua(f+356|0);la(f+312|0);break u}c=na(20);y[c+16>>2]=0;y[c+8>>2]=0;y[c+12>>2]=0;y[c>>2]=0;y[c+4>>2]=0;y[f+24>>2]=c;if(ra(y[a+4>>2],c)){break E}if(ra(y[a+4>>2],c+4|0)){break E}if(ra(y[a+4>>2],c+8|0)){break E}if(ra(y[a+4>>2],c+12|0)){break E}if(va(y[a+4>>2],f+356|0)){break E}if(Wa(y[a+4>>2],f+312|0)){break E}w[c+16|0]=z[f+356|0]>>>2&1;g=A[f+312>>1];b=g&32767;x[c+18>>1]=b;w[c+17|0]=g>>>15;d=y[c+4>>2];if((d|0)!=-1){break J}if(g<<16>>16>=0){w[c+17|0]=1}if(z[a+50|0]){break F}d=b;break G}w[a+49|0]=0;break u}c=y[a+4>>2];d=y[c+8>>2];b=d+y[m+28>>2]|0;if(b>>>0>>0){break B}g=c;c=y[c+4>>2];y[g+8>>2]=b>>>0>>0?b:c;break B}c=y[a+4>>2];d=y[c+8>>2];b=d+y[m+28>>2]|0;if(b>>>0>>0){break B}g=c;c=y[c+4>>2];y[g+8>>2]=b>>>0>>0?b:c;break B}y[m+52>>2]=4;k=m+68|0;vb(k,0);g=y[a+4>>2];d=na(44);w[d+1|0]=0;t(d+4|0,0,40);if((va(g,f+356|0)|0)==-1){break I}b=1;i=z[f+356|0];w[d+1|0]=i&1;Ab:{Bb:{if((ra(g,f+332|0)|0)==-1){break Bb}if((ra(g,f+312|0)|0)==-1){break Bb}c=y[f+332>>2];h=y[f+312>>2];if((c|0)>(h|0)){break Bb}l=(i>>>4&7)+1|0;i=(i>>>1&7)+1|0;pb(d,0);y[f+28>>2]=c;w[f+24|0]=1;b=c;while(1){if((Ia(g,i,y[d+8>>2]+(y[d+4>>2]<<3)|0)|0)==-1){break Ab}if((Ia(g,l,y[d+20>>2]+(y[d+4>>2]<<2)|0)|0)==-1){break Ab}n=y[d+20>>2];q=y[d+4>>2]<<2;if(B[n+q>>2]>63){break Ab}y[q+y[d+32>>2]>>2]=b;b=y[n+(y[d+4>>2]<<2)>>2];if((b|0)>31){break Ab}if(z[Na(f+24|0,1<>2];if((h|0)>(b|0)){continue}break}if((Ia(g,i,y[d+8>>2]+(y[d+4>>2]<<3)|0)|0)==-1){break Ab}y[y[d+20>>2]+(y[d+4>>2]<<2)>>2]=32;if((c|0)==-2147483648){break Ab}y[y[d+32>>2]+(y[d+4>>2]<<2)>>2]=c-1;pb(d,1);if((Ia(g,i,y[d+8>>2]+(y[d+4>>2]<<3)|0)|0)==-1){break Ab}y[y[d+20>>2]+(y[d+4>>2]<<2)>>2]=32;y[y[d+32>>2]+(y[d+4>>2]<<2)>>2]=h;pb(d,1);b=y[d+4>>2];if(z[d+1|0]==1){if((Ia(g,i,y[d+8>>2]+(b<<3)|0)|0)==-1){break Ab}b=y[d+4>>2]+1|0;y[d+4>>2]=b}c=y[d+8>>2];if(y[d+12>>2]-c>>3>>>0>>0){break t}c=ob(c,b);w[d|0]=c;y[f+24>>2]=d;b=1;if(!c){break H}b=0;y[f+24>>2]=0;vb(k,d);ta(y[a+4>>2]);break H}w[d|0]=0;y[f+24>>2]=d;break H}break I}if((d|0)!=62){break B}c=y[a+4>>2];d=y[c+8>>2];b=d+y[m+28>>2]|0;if(b>>>0>>0){break B}g=c;c=y[c+4>>2];y[g+8>>2]=b>>>0>>0?b:c;break B}if(!z[a+50|0]){break G}break F}w[d|0]=0;y[f+24>>2]=d;b=1}pa(f+24|0);break u}b=xa(y[c>>2],d);y[f+332>>2]=0;oa(a+32|0,b);la(f+332|0)}if(qa(y[a+32>>2])){break C}y[a+56>>2]=-1}$b(f+24|0)}b=1;break u}Qa(y[a+32>>2],z[c+16|0]);Cb:{Db:{b=y[a+24>>2];h=y[a+28>>2];Eb:{if(b>>>0>>0){y[f+24>>2]=0;y[b>>2]=c;b=b+4|0;break Eb}d=y[a+20>>2];g=b-d|0;k=g>>2;i=k+1|0;if(i>>>0>=1073741824){break Db}b=0;h=h-d|0;l=h>>1;i=h>>>0>=2147483644?1073741823:i>>>0>>0?l:i;if(i){if(i>>>0>=1073741824){break Cb}b=na(i<<2)}y[f+24>>2]=0;h=b+g|0;y[h>>2]=c;c=h-(k<<2)|0;if(g){p(c,d,g)}y[a+28>>2]=(i<<2)+b;b=h+4|0;y[a+24>>2]=b;y[a+20>>2]=c;if(!d){break Eb}ma(d)}w[a+49|0]=1;y[a+24>>2]=b;$b(f+24|0);break B}Ba();o()}jb();o()}b=0;break u}la(f+356|0)}ua(f+312|0);y[f+24>>2]=0;Xb(m+60|0,c);Zb(f+24|0);if(y[m+60>>2]){ta(y[a+4>>2]);b=y[a+4>>2];c=y[b+8>>2];if(c>>>0<=4294967293){d=b;c=c+2|0;b=y[b+4>>2];y[d+8>>2]=b>>>0>c>>>0?c:b}ua(f+16|0);Ha(f+332|0);b=0;break x}ua(f+16|0);Ha(f+332|0)}b=1}ma(g);break u}if(ra(y[a+4>>2],h+16|0)){break v}c=y[a+4>>2];g=y[c>>2];c=y[c+4>>2];if((c|0)<0){break t}d=c>>>27&15;c=((c&134217727)<<5|g>>>27)&-32;if(!d&c>>>0>=2147483648|d){break t}if(!d&B[h+16>>2]>c>>>0){break v}n=0;b=y[m+8>>2];c=(b|0)>0?b:0;d=0;while(1){if((c|0)==(d|0)){i=1;b=0;while(1)if((b|0)==(c|0)){if(!(i&1)){break t}y[h+24>>2]=n;l=Ub(f+356|0,n);k=y[l>>2];Fb:{if((k|0)==y[l+4>>2]){break Fb}b=y[m+8>>2];s=(b|0)>0?b:0;i=1;n=0;c=0;while(1){if((c|0)==(s|0)){break Fb}Gb:{b=Ma(a,y[y[m+12>>2]+(c<<2)>>2]);if(z[b+4|0]&63){break Gb}b=y[b+56>>2];g=y[b+24>>2];d=y[b+28>>2]-g>>2;b=0;while(1){if((b|0)==(d|0)){g=0;if(i&1){b=d+n|0;d=b>>>0>=d>>>0;g=d;n=d?b:0}else{n=0}i=g;break Gb}C=b+n|0;if(!(C>>>0>=b>>>0&i)){break t}y[k+(C<<2)>>2]=y[g+(b<<2)>>2];b=b+1|0;continue}}c=c+1|0;continue}}bb(h+40|0,l);Hb:{Ib:{Jb:{Kb:{if(!z[h|0]){c=y[h+24>>2];b=0;while(1){d=b;b=b+1|0;if(c>>>0>1<>>0){continue}break}break Kb}n=y[h+24>>2];b=0;while(1){if((b|0)!=35){c=b<<3;b=b+1|0;if(!Ia(y[a+4>>2],4,c+(f+24|0)|0)){continue}break Jb}break}if(!ob(f+24|0,35)){break Jb}i=Ed(f+312|0,n);k=y[i>>2];c=0;Lb:while(1){Mb:{Nb:{Ob:{if((c|0)<(n|0)){d=0;y[f+20>>2]=0;w[f+16|0]=1;Pb:while(1){if(wb(y[a+4>>2],f+352|0)){break Nb}g=Tb(f+16|0);if(z[g|0]!=1){break Nb}b=0;C=y[f+20>>2]|y[f+352>>2];s=(C|0)>=0;y[g>>2]=s;j=g;g=s?C:0;y[j+4>>2]=g;if(!s){break t}d=d+1|0;while(1){if((b|0)==35){continue Pb}s=(f+24|0)+(b<<3)|0;if(!(y[s>>2]==(d|0)&(g|0)==y[s+4>>2])){b=b+1|0;continue}break}break}if(b>>>0<=31){y[k+(c<<3)>>2]=b;break Mb}g=y[a+4>>2];Qb:{switch(b-33|0){default:if(Ia(g,2,f+352|0)){break Nb}d=3;break Ob;case 1:if(Ia(g,7,f+352|0)){break Nb}d=11;break Ob;case 0:break Qb}}d=3;if(!Ia(g,3,f+352|0)){break Ob}break Nb}b=y[i+4>>2];if(!ob(k,b-k>>3)){break Nb}y[f+4>>2]=b;y[f>>2]=k;y[f+8>>2]=y[i+8>>2];d=0;y[i+8>>2]=0;y[i>>2]=0;y[i+4>>2]=0;ya(i);if((b|0)==(k|0)){break Ib}ta(y[a+4>>2]);bb(h+28|0,f);ya(f);b=1;if(z[h|0]!=1){break Kb}c=A[f+14>>1];g=c>>>12&3;if((g|0)==2){break Hb}k=c>>>10&3;if((k|0)==2){break Hb}n=c>>>8&3;if((n|0)==2){break Hb}i=c&3;if((i|0)==2){break Hb}s=c>>>6&3;if((s|0)==2){break Hb}Rb:{Sb:{switch(i|0){case 0:i=0;j=sa(a,6);break Rb;case 1:i=0;j=sa(a,7);break Rb;default:break Sb}}C=Ga(a,m,0);if(!C){break Hb}i=1;j=y[C+68>>2]}y[h+60>>2]=j;Tb:{Ub:{switch((c>>>2&3)-1|0){default:j=sa(a,8);break Tb;case 0:j=sa(a,9);break Tb;case 1:j=sa(a,10);break Tb;case 2:break Ub}}C=Ga(a,m,i);if(!C){break Hb}i=i+1|0;j=y[C+68>>2]}y[h+64>>2]=j;Vb:{Wb:{switch((c>>>4&3)-1|0){default:j=sa(a,11);break Vb;case 0:j=sa(a,12);break Vb;case 1:j=sa(a,13);break Vb;case 2:break Wb}}C=Ga(a,m,i);if(!C){break Hb}i=i+1|0;j=y[C+68>>2]}y[h+68>>2]=j;Xb:{Yb:{switch(s|0){case 0:j=sa(a,14);break Xb;case 1:j=sa(a,15);break Xb;default:break Yb}}s=Ga(a,m,i);if(!s){break Hb}i=i+1|0;j=y[s+68>>2]}y[h+72>>2]=j;Zb:{_b:{switch(n|0){case 0:n=sa(a,14);break Zb;case 1:n=sa(a,15);break Zb;default:break _b}}n=Ga(a,m,i);if(!n){break Hb}i=i+1|0;n=y[n+68>>2]}y[h+76>>2]=n;$b:{ac:{switch(k|0){case 0:n=sa(a,14);break $b;case 1:n=sa(a,15);break $b;default:break ac}}k=Ga(a,m,i);if(!k){break Hb}i=i+1|0;n=y[k+68>>2]}y[h+80>>2]=n;bc:{cc:{switch(g|0){case 0:g=sa(a,14);break bc;case 1:g=sa(a,15);break bc;default:break cc}}g=Ga(a,m,i);if(!g){break Hb}i=i+1|0;g=y[g+68>>2]}y[h+84>>2]=g;if(!(c&16384)){fa=h,ga=sa(a,1),y[fa+88>>2]=ga;break Kb}c=Ga(a,m,i);if(!c){break Hb}y[h+88>>2]=y[c+68>>2];break Kb}s=y[f+352>>2]+d|0;if((s|0)<=0){break Mb}g=c+s|0;if((g|0)>(n|0)){break Nb}d=0;b=(b|0)==32&(c|0)>0;c=k+(c<<3)|0;C=c-8|0;while(1)if((d|0)==(s|0)){c=g;continue Lb}else{y[c+(d<<3)>>2]=b?y[C>>2]:0;d=d+1|0;continue}}y[f+8>>2]=0;y[f>>2]=0;y[f+4>>2]=0;ya(i);break Ib}c=c+1|0;continue}}i=0;y[f+312>>2]=0;y[f+316>>2]=0;if(z[h+1|0]==1){b=z[h+2|0];c=b?1024:8192;g=ab(c,8);y[f+24>>2]=0;rc(f+312|0,g);y[f+316>>2]=c;y[f+28>>2]=0;Ha(f+24|0);i=b?1024:8192}y[m+52>>2]=1;dc:{ec:{fc:{gc:{if(z[h|0]==1){b=Dd(h,y[a+4>>2],y[f+312>>2],i);y[f+24>>2]=0;oa(m- -64|0,b);la(f+24|0);if(!y[m+64>>2]){break ec}ta(y[a+4>>2]);break gc}c=Ra(y[a+4>>2]);y[f>>2]=c;b=Cd(f+24|0,d&255);c=Bd(h,c,y[f+312>>2],i,b);y[f+16>>2]=0;oa(m- -64|0,c);la(f+16|0);if(!y[m+64>>2]){break fc}ta(y[a+4>>2]);c=y[a+4>>2];d=y[c+8>>2];if(d>>>0<=4294967293){g=c;d=d+2|0;c=y[c+4>>2];y[g+8>>2]=c>>>0>d>>>0?d:c}qc(b);ua(f)}b=0;if((z[m+4|0]&63)==4){break dc}hc:{if(z[a+50|0]){break hc}b=y[y[a+24>>2]-4>>2];if(z[b+17|0]!=1){break hc}c=q+y[f+344>>2]|0;d=y[a+32>>2];if((c|0)<=y[d+12>>2]){break hc}eb(d,c,z[b+16|0])}b=y[f+340>>2];g=b;h=b>>31;b=y[f+344>>2];c=b;d=b>>31;b=z[f+348|0];fb(y[a+32>>2],g,h,c,d,y[m+64>>2],(b&7)==4?4:b&3);oa(m- -64|0,0);b=0;break dc}qc(b);ua(f)}b=1}Ha(f+312|0);break Hb}y[f+8>>2]=0;y[f>>2]=0;y[f+4>>2]=0}ya(f);b=1}ya(l);break v}else{d=Ma(a,y[y[m+12>>2]+(b<<2)>>2]);if(!(z[d+4|0]&63)){g=0;if(i&1){d=y[d+56>>2];d=y[d+28>>2]-y[d+24>>2]>>2;g=d+n|0;i=g;g=d>>>0<=g>>>0;n=g?i:0}else{n=0}i=g}b=b+1|0;continue}}b=1;g=d<<2;d=d+1|0;if(Ma(a,y[g+y[m+12>>2]>>2])){continue}break}}pc(f+308|0)}Y=f+368|0;break s}o()}if(y[a+56>>2]!=3){break r}b=y[a+4>>2];c=y[b+8>>2];b=y[b+4>>2];if(c>>>0>b>>>0){break e}if((b|0)!=(c|0)){continue}break}y[a+52>>2]=2;break c}ic:{switch(b|0){default:ib(R,0);break i;case 0:break ic;case 2:break d}}g=y[R>>2];b=y[g+28>>2];if((b|0)==-1){break g}c=y[a+84>>2];b=b+c|0;if(b>>>0>=c>>>0){break h}}aa=1;break c}y[a+84>>2]=b;d=y[a+4>>2];c=y[d+4>>2];y[d+8>>2]=b>>>0>>0?b:c;break f}d=y[a+4>>2];b=y[d+8>>2];if(b>>>0>4294967291){break f}b=b+4|0;c=y[d+4>>2];y[d+8>>2]=b>>>0>>0?b:c}b=y[a+12>>2];i=y[a+16>>2];jc:{if(b>>>0>>0){y[R>>2]=0;y[b>>2]=g;b=b+4|0;break jc}c=y[a+8>>2];b=b-c|0;k=b>>2;d=k+1|0;if(d>>>0>=1073741824){break b}i=i-c|0;h=i>>1;d=i>>>0>=2147483644?1073741823:d>>>0>>0?h:d;if(d){if(d>>>0>=1073741824){break a}i=na(d<<2)}else{i=0}y[a+80>>2]=0;h=b+i|0;y[h>>2]=g;g=h-(k<<2)|0;if(b){p(g,c,b)}y[a+16>>2]=i+(d<<2);b=h+4|0;y[a+12>>2]=b;y[a+8>>2]=g;if(c){ma(c)}d=y[a+4>>2]}y[a+12>>2]=b;b=y[d+8>>2];i=y[d+4>>2];if(b>>>0<=i>>>0){continue}break}}o()}ib(R,0)}Y=J+16|0;return aa}Ba();o()}jb();o()}function cb(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0;e=Y-96|0;Y=e;f=y[a+8>>2];g=y[a+4>>2];a:{if(!(f-1>>>0<65535&g-1>>>0<=65534)){n=xa(g,f);break a}b:{c:{d:{e:{f:{g:{h:{i:{j:{k:{if(!z[a|0]){l:{if(z[a+24|0]!=255|z[a+25|0]!=255|(z[a+26|0]!=255|z[a+27|0]!=255)){break l}if(y[a+12>>2]){break l}k=y[a+20>>2];if((g|0)!=y[k+8>>2]){break l}if(!qa(k)){break b}p=y[a+4>>2];f=y[a+8>>2];n=xa(p,f);y[e+44>>2]=n;if(!qa(n)){break d}q=d;A=c;c=y[a+20>>2];G=y[c+8>>2];x=y[c+12>>2];c=y[a+16>>2];if(!((1-x|0)<=(c|0)&(c|0)<(x|0))){y[a+16>>2]=0}K=(f|0)>0?f:0;L=x+1|0;E=x-1|0;H=A+128|0;while(1){if((l|0)!=(K|0)){if(z[a+1|0]==1){if(z[b|0]){break d}if((d|0)==16|d>>>0<16){break k}C=ka(b,H)^C}ja(e+32|0,n,l);i=0;m:{if(!l){u=0;s=0;k=0;break m}ja(e+72|0,n,l-1|0);s=y[e+76>>2];if(!s){break k}u=y[e+72>>2];k=z[u|0]<<4}c=y[a+16>>2];y[e+88>>2]=0;y[e+92>>2]=0;y[e+80>>2]=0;y[e+84>>2]=0;y[e+72>>2]=0;y[e+76>>2]=0;y[e+16>>2]=0;y[e+8>>2]=0;y[e+12>>2]=0;g=l-c|0;if(!((g|0)<=0|(g|0)>(x|0))){ja(e+48|0,y[a+20>>2],g-1|0);c=y[e+52>>2];y[e+76>>2]=c;f=y[e+48>>2];y[e+72>>2]=f;if(!c){break k}c=z[f|0];y[e+8>>2]=c;i=c&192}j=0;c=0;n:{if((g|0)<0){break n}c=0;if((g|0)>=(x|0)){break n}ja(e+48|0,y[a+20>>2],g);c=y[e+52>>2];y[e+84>>2]=c;f=y[e+48>>2];y[e+80>>2]=f;if(!c){break k}c=z[f|0];y[e+12>>2]=c;c=c>>>3&24}if(!((g|0)<-1|(g|0)>=(E|0))){ja(e+48|0,y[a+20>>2],g+1|0);g=y[e+52>>2];y[e+92>>2]=g;f=y[e+48>>2];y[e+88>>2]=f;if(!g){break k}g=z[f|0];y[e+16>>2]=g;j=g>>>6|0}o:{if(!C){h=c|(k&3072|i)|j;r=0;v=y[e+32>>2];I=y[e+36>>2];m=0;p:while(1){if((m|0)>=(p|0)){break o}q:{if(!l){break q}k=k<<8;if((p|0)<=(m+8|0)){break q}c=(m>>>3|0)+1|0;if(c>>>0>=s>>>0){break k}k=z[c+u|0]<<4|k}r:{if((L+y[a+16>>2]|0)<(l|0)){y[e+16>>2]=0;y[e+8>>2]=0;y[e+12>>2]=0;g=0;i=0;c=0;break r}c=(m>>>3|0)+1|0;i=e+8|0;f=0;g=(G|0)<=(m+8|0);while(1){if((f|0)!=24){j=(e+72|0)+f|0;t=y[j+4>>2];s:{if(!t){break s}D=y[i>>2]<<8;y[i>>2]=D;if(g){break s}if(c>>>0>=t>>>0){break k}y[i>>2]=D|z[c+y[j>>2]|0]}i=i+4|0;f=f+8|0;continue}break}g=y[e+12>>2];i=y[e+8>>2];c=y[e+16>>2]}f=0;j=r+p|0;j=(j|0)>0?j:0;D=(j|0)>=8?8:j;j=0;while(1){if((f|0)==(D|0)){c=m>>>3|0;if(c>>>0>=I>>>0){break k}w[c+v|0]=j;r=r-8|0;m=m+8|0;continue p}if(h>>>0>=q>>>0){break k}F=ka(b,A+(h<<3)|0);t=7-f|0;j=F<>>13-f&1|(g>>>10-f&8|(i>>>t&64|(k>>>t&1024|h<<1&6582|F<<9)));f=f+1|0;continue}}}Jb(e+48|0,a,l);i=c|(k&3072|i)|j;c=0;v=y[e+32>>2];I=y[e+36>>2];D=y[e+60>>2];F=y[e+56>>2];r=0;while(1){if((c|0)>=(p|0)){break o}t:{if(!l){g=c+8|0;break t}k=k<<8;g=c+8|0;if((p|0)<=(g|0)){break t}f=(c>>>3|0)+1|0;if(f>>>0>=s>>>0){break k}k=z[f+u|0]<<4|k}j=c>>>3|0;m=j+1|0;f=0;h=e+8|0;while(1){u:{if((f|0)==24){f=0;m=r+p|0;m=(m|0)>0?m:0;J=(m|0)>=8?8:m;N=y[e+16>>2];M=y[e+12>>2];O=y[e+8>>2];m=0;while(1){if((f|0)==(J|0)){break u}t=y[y[a+20>>2]+8>>2];P=c+f|0;h=ia(t,P,F,D);v:{if(z[a+1|0]==1){if(Ib(t,P,h,e+48|0)){break v}}if(z[b|0]){break d}if(i>>>0>=q>>>0){break k}h=ka(b,A+(i<<3)|0)}t=7-f|0;i=N>>>13-f&1|(M>>>10-f&8|(k>>>t&1024|i<<1&6582|h<<9|O>>>t&64));f=f+1|0;m=h<>2];w:{if(!J){break w}N=y[h>>2]<<8;y[h>>2]=N;if((g|0)>=(G|0)){break w}if(m>>>0>=J>>>0){break k}y[h>>2]=N|z[m+y[t>>2]|0]}h=h+4|0;f=f+8|0;continue}break}if(j>>>0>=I>>>0){break k}w[j+v|0]=m;r=r-8|0;c=g;continue}}l=l+1|0;continue}break}y[e+44>>2]=0;break c}n=xa(g,f);y[e+44>>2]=n;if(!qa(n)){break f}Qa(n,0);A=c;u=c+128|0;c=0;while(1){if(B[a+8>>2]>c>>>0){if(z[a+1|0]==1){if(z[b|0]){break f}if((d|0)==16|d>>>0<16){break k}m=ka(b,u)^m}ja(e+32|0,n,c);f=e+72|0;ja(f,n,c-1|0);g=y[e+76>>2];q=y[e+72>>2];dd(f,a,c);f=0;k=y[n+8>>2];f=!g|(k|0)<2?f:z[q|0]>>>6&1;if(!(!g|(k|0)<=0)){f=z[q|0]>>>6&2|f}y[e+8>>2]=f;y[e+12>>2]=0;r=y[a+20>>2];f=y[r+8>>2];k=y[a+12>>2];i=1-k|0;l=y[e+72>>2];j=y[e+76>>2];h=0-k|0;Q=e,R=ia(f,i,l,j)|ia(f,h,l,j)<<1,y[Q+16>>2]=R;l=y[e+80>>2];j=y[e+84>>2];s=ia(f,i,l,j);p=s|ia(f,h,l,j)<<1;s=k^-1;Q=e,R=p|ia(f,s,l,j)<<2,y[Q+20>>2]=R;j=i;k=y[e+88>>2];i=y[e+92>>2];l=ia(f,j,k,i);Q=e,R=l|ia(f,h,k,i)<<1|ia(f,s,k,i)<<2,y[Q+24>>2]=R;f=e+48|0;ja(f,r,w[a+27|0]+(c-y[a+16>>2]|0)|0);k=y[e+52>>2];h=y[e+48>>2];ja(f,n,w[a+25|0]+c|0);f=y[e+52>>2];i=y[e+48>>2];x:{if(!m){l=q;q=g;j=i;i=f;g=k;r=h;f=0;k=y[e+32>>2];s=y[e+36>>2];while(1){if(B[a+4>>2]<=f>>>0){break x}p=e+8|0;h=cd(a,y[n+8>>2],p,f,r,g,j,i);if(z[b|0]){break f}if(d>>>0<=h>>>0){break k}bd(a,n,p,f,ka(b,A+(h<<3)|0),e+72|0,l,q,k,s);f=f+1|0;continue}}Jb(e+48|0,a,c);l=q;q=g;j=i;i=f;g=k;r=h;f=0;k=y[e+32>>2];s=y[e+36>>2];p=y[e+60>>2];C=y[e+56>>2];while(1){if(B[a+4>>2]<=f>>>0){break x}x=y[y[a+20>>2]+8>>2];h=ia(x,f,C,p);y:{if(z[a+1|0]==1){if(Ib(x,f,h,e+48|0)){break y}}h=cd(a,y[n+8>>2],e+8|0,f,r,g,j,i);if(z[b|0]==1){break f}if(d>>>0<=h>>>0){break k}h=ka(b,A+(h<<3)|0)}bd(a,n,e+8|0,f,h,e+72|0,l,q,k,s);f=f+1|0;continue}}c=c+1|0;continue}break}y[e+44>>2]=0;break e}z:{if(y[a+12>>2]){break z}k=y[a+20>>2];if((g|0)!=y[k+8>>2]){break z}if(!qa(k)){break b}q=y[a+4>>2];f=y[a+8>>2];n=xa(q,f);y[e+44>>2]=n;if(!qa(n)){break h}A=d;m=c;c=y[a+20>>2];x=y[c+8>>2];p=y[c+12>>2];c=y[a+16>>2];if(!((1-p|0)<=(c|0)&(c|0)<(p|0))){y[a+16>>2]=0}t=(f|0)>0?f:0;K=p-1|0;L=m- -64|0;while(1){A:{B:{if((l|0)!=(t|0)){if(z[a+1|0]==1){if(z[b|0]){break h}if((d|0)==8|d>>>0<8){break k}G=ka(b,L)^G}ja(e+32|0,n,l);i=0;C:{if(!l){C=0;s=0;k=0;break C}ja(e+72|0,n,l-1|0);s=y[e+76>>2];if(!s){break k}C=y[e+72>>2];k=z[C|0]<<1}c=y[a+16>>2];y[e+88>>2]=0;y[e+92>>2]=0;y[e+80>>2]=0;y[e+84>>2]=0;y[e+72>>2]=0;y[e+76>>2]=0;y[e+16>>2]=0;y[e+8>>2]=0;y[e+12>>2]=0;g=l-c|0;if(!((g|0)<=0|(g|0)>(p|0))){ja(e+48|0,y[a+20>>2],g-1|0);c=y[e+52>>2];y[e+76>>2]=c;f=y[e+48>>2];y[e+72>>2]=f;if(!c){break k}c=z[f|0];y[e+8>>2]=c;i=c>>>2&32}j=0;c=0;D:{if((g|0)<0){break D}c=0;if((g|0)>=(p|0)){break D}ja(e+48|0,y[a+20>>2],g);c=y[e+52>>2];y[e+84>>2]=c;f=y[e+48>>2];y[e+80>>2]=f;if(!c){break k}c=z[f|0];y[e+12>>2]=c;c=c>>>4&12}if(!((g|0)<-1|(g|0)>=(K|0))){ja(e+48|0,y[a+20>>2],g+1|0);g=y[e+52>>2];y[e+92>>2]=g;f=y[e+48>>2];y[e+88>>2]=f;if(!g){break k}g=z[f|0];y[e+16>>2]=g;j=g>>>6|0}if(G){break B}h=c|(k&384|i)|j;f=0;u=y[e+32>>2];E=y[e+36>>2];r=0;E:while(1){if((f|0)>=(q|0)){break A}F:{if(!l){c=f+8|0;break F}k=k<<8;c=f+8|0;if((q|0)<=(c|0)){break F}g=(f>>>3|0)+1|0;if(g>>>0>=s>>>0){break k}k=z[g+C|0]<<1|k}g=f>>>3|0;j=g+1|0;i=e+8|0;f=0;while(1){if((f|0)==24){f=0;i=r+q|0;i=(i|0)>0?i:0;j=(i|0)>=8?8:i;H=y[e+16>>2];v=y[e+12>>2];I=y[e+8>>2];i=0;while(1){if((f|0)==(j|0)){if(g>>>0>=E>>>0){break k}w[g+u|0]=i;r=r-8|0;f=c;continue E}if(h>>>0>=A>>>0){break k}D=ka(b,m+(h<<3)|0);F=7-f|0;i=D<>>13-f&1|(v>>>11-f&4|(I>>>9-f&32|(k>>>F&128|h<<1&794|D<<6)));f=f+1|0;continue}}H=(e+72|0)+f|0;v=y[H+4>>2];G:{if(!v){break G}I=y[i>>2]<<8;y[i>>2]=I;if((c|0)>=(x|0)){break G}if(j>>>0>=v>>>0){break k}y[i>>2]=I|z[j+y[H>>2]|0]}i=i+4|0;f=f+8|0;continue}}}y[e+44>>2]=0;break g}Jb(e+48|0,a,l);i=c|(k&384|i)|j;c=0;E=y[e+32>>2];H=y[e+36>>2];v=y[e+60>>2];I=y[e+56>>2];u=0;while(1){if((c|0)>=(q|0)){break A}H:{if(!l){g=c+8|0;break H}k=k<<8;g=c+8|0;if((q|0)<=(g|0)){break H}f=(c>>>3|0)+1|0;if(f>>>0>=s>>>0){break k}k=z[f+C|0]<<1|k}r=c>>>3|0;j=r+1|0;f=0;h=e+8|0;while(1){I:{if((f|0)==24){f=0;h=q+u|0;h=(h|0)>0?h:0;D=(h|0)>=8?8:h;F=y[e+16>>2];J=y[e+12>>2];N=y[e+8>>2];j=0;while(1){if((f|0)==(D|0)){break I}M=y[y[a+20>>2]+8>>2];O=c+f|0;h=ia(M,O,I,v);J:{if(z[a+1|0]==1){if(Ib(M,O,h,e+48|0)){break J}}if(z[b|0]){break h}if(i>>>0>=A>>>0){break k}h=ka(b,m+(i<<3)|0)}M=7-f|0;i=F>>>13-f&1|(J>>>11-f&4|(N>>>9-f&32|(k>>>M&128|i<<1&794|h<<6)));f=f+1|0;j=h<>2];K:{if(!F){break K}J=y[h>>2]<<8;y[h>>2]=J;if((g|0)>=(x|0)){break K}if(j>>>0>=F>>>0){break k}y[h>>2]=J|z[j+y[D>>2]|0]}h=h+4|0;f=f+8|0;continue}break}if(r>>>0>=H>>>0){break k}w[r+E|0]=j;u=u-8|0;c=g;continue}}l=l+1|0;continue}}n=xa(g,f);y[e+32>>2]=n;if(!qa(n)){break j}Qa(n,0);g=c;t=c- -64|0;while(1){if(B[a+8>>2]>u>>>0){if(z[a+1|0]==1){if(z[b|0]){break j}if((d|0)==8|d>>>0<8){break k}r=ka(b,t)^r}ja(e+8|0,n,u);c=e+72|0;ja(c,n,u-1|0);k=y[e+76>>2];A=y[e+72>>2];dd(c,a,u);h=0;c=y[n+8>>2];h=!k|(c|0)<2?h:z[A|0]>>>6&1;h=!k|(c|0)<=0?h:z[A|0]>>>6&2|h;c=y[y[a+20>>2]+8>>2];m=y[a+12>>2];f=0-m|0;p=y[e+72>>2];C=y[e+76>>2];ia(c,f,p,C);l=ia(c,f,p,C);j=1-m|0;q=y[e+80>>2];s=y[e+84>>2];i=ia(c,j,q,s)|ia(c,f,q,s)<<1|ia(c,m^-1,q,s)<<2;x=y[e+88>>2];G=y[e+92>>2];j=ia(c,j,x,G)|ia(c,f,x,G)<<1;L:{if(!r){m=0;K=y[e+12>>2];L=y[e+8>>2];f=0;while(1){if(B[a+4>>2]<=f>>>0){break L}if(z[b|0]){break j}c=i<<2|l<<5|m<<6|h<<7|j;if(c>>>0>=d>>>0){break k}m=ka(b,g+(c<<3)|0);La(y[n+8>>2],f,L,K,m);l=y[a+12>>2];h=ia(y[n+8>>2],f+2|0,A,k)|h<<1&6;c=y[y[a+20>>2]+8>>2];E=f-l|0;l=ia(c,E+1|0,p,C);E=E+2|0;i=ia(c,E,q,s)|i<<1&6;j=ia(c,E,x,G)|j<<1&2;f=f+1|0;continue}}Jb(e+48|0,a,u);c=0;K=y[e+12>>2];L=y[e+8>>2];E=y[e+60>>2];H=y[e+56>>2];f=0;while(1){if(B[a+4>>2]<=f>>>0){break L}v=y[y[a+20>>2]+8>>2];m=ia(v,f,H,E);M:{if(z[a+1|0]==1){if(Ib(v,f,m,e+48|0)){break M}}if(z[b|0]==1){break j}c=i<<2|l<<5|c<<6|h<<7|j;if(c>>>0>=d>>>0){break k}m=ka(b,g+(c<<3)|0)}La(y[n+8>>2],f,L,K,m);l=y[a+12>>2];h=ia(y[n+8>>2],f+2|0,A,k)|h<<1&6;c=y[y[a+20>>2]+8>>2];v=f-l|0;l=ia(c,v+1|0,p,C);v=v+2|0;i=ia(c,v,q,s)|i<<1&6;j=ia(c,v,x,G)|j<<1&2;f=f+1|0;c=m;continue}}u=u+1|0;continue}break}y[e+32>>2]=0;break i}o()}n=0}la(e+32|0);break a}n=0}la(e+44|0);break a}n=0}la(e+44|0);break a}n=0}la(e+44|0);break a}}Y=e+96|0;return n}function Zc(a,b,c,d,e,f,g,h){var i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0;i=Y-32|0;Y=i;j=d-(c>>>0<1048577)|0;l=f-(e>>>0<1048577)|0;a:{if((j|0)==-1&c-1048577>>>0<4292870143|(j|0)!=-1|((l|0)==-1&e-1048577>>>0<4292870143|(l|0)!=-1)){break a}p=y[h>>2];q=y[h+8>>2];j=y[h+4>>2];m=y[h+12>>2];y[i+28>>2]=y[b+8>>2];w[i+24|0]=1;l=Yc(i+24|0,c);v=z[l|0];k=y[i+28>>2];h=y[b+12>>2];y[l>>2]=1;y[l+4>>2]=h;l=Yc(l,e);h=m-j|0;m=y[i+28>>2];x=z[l|0]?(h|0)<(m|0)?h:m:h;l=0-e&f>>31;if((x|0)<=(l|0)){break a}h=q-p|0;m=v?(h|0)<(k|0)?h:k:h;h=0-c&d>>31;if((m|0)<=(h|0)){break a}b:{c:{p=h+p|0;if((p|0)<0){break c}c=!!c&(d|0)>=0|(d|0)>0?c:0;n=c+(m-h|0)|0;D=-1<<0-n;E=-1>>>c|0;v=j+l|0;k=h&31;j=x-l|0;q=c&31;d=!!e&(f|0)>=0|(f|0)>0?e:0;l=c>>>5|0;p=p>>>5|0;if((c^n-1)>>>0<=31){c=D&E;if((h^m-1)>>>0>=32){h=0;j=(j|0)>0?j:0;k=k-q|0;q=32-k|0;m=p<<2;while(1){if((h|0)==(j|0)){break a}f=i+8|0;Ca(f,a,h+v|0);e=y[i+12>>2];n=y[i+8>>2];Ca(f,b,d+h|0);if(!e){break b}f=y[i+12>>2];if(!f){break b}if(e>>>0

>>0){break c}y[i+20>>2]=e-p;y[i+16>>2]=m+n;e=_a(i+16|0);if(!y[i+20>>2]|f>>>0>>0|(f|0)==(l|0)){break c}f=y[i+8>>2]+(l<<2)|0;n=(te(e,24)&16711935|te(e&16711935,8))<>2]>>2];n=n|(te(e&16711935,8)|te(e,24)&16711935)>>>q;e=y[f>>2];e=Oa(g,n,te(e&16711935,8)|te(e,24)&16711935,c);I=f,J=te(e&16711935,8)|te(e,24)&16711935,y[I>>2]=J;h=h+1|0;continue}}h=0;f=(j|0)>0?j:0;if(k>>>0>q>>>0){k=k-q|0;q=p<<2;while(1){if((f|0)==(h|0)){break a}j=i+16|0;Ca(j,a,h+v|0);e=y[i+20>>2];m=y[i+16>>2];Ca(j,b,d+h|0);if(!e){break b}j=y[i+20>>2];if(!j){break b}if((e|0)==(p|0)|e>>>0

>>0|((j|0)==(l|0)|j>>>0>>0)){break c}e=y[i+16>>2]+(l<<2)|0;n=e;j=y[m+q>>2];j=(te(j&16711935,8)|te(j,24)&16711935)<>2];e=Oa(g,j,te(e&16711935,8)|te(e,24)&16711935,c);I=n,J=te(e&16711935,8)|te(e,24)&16711935,y[I>>2]=J;h=h+1|0;continue}}k=q-k|0;q=p<<2;while(1){if((f|0)==(h|0)){break a}j=i+16|0;Ca(j,a,h+v|0);e=y[i+20>>2];m=y[i+16>>2];Ca(j,b,d+h|0);if(!e){break b}j=y[i+20>>2];if(!j){break b}if((e|0)==(p|0)|e>>>0

>>0|((j|0)==(l|0)|j>>>0>>0)){break c}e=y[i+16>>2]+(l<<2)|0;n=e;j=y[m+q>>2];j=(te(j&16711935,8)|te(j,24)&16711935)>>>k|0;e=y[e>>2];e=Oa(g,j,te(e&16711935,8)|te(e,24)&16711935,c);I=n,J=te(e&16711935,8)|te(e,24)&16711935,y[I>>2]=J;h=h+1|0;continue}}x=n&31;A=(y[a+16>>2]>>>2)-(h>>>5)|0;if(k>>>0<=q>>>0){if((k|0)==(q|0)){k=0;e=((n|0)/32|0)-(c+31>>>5|0)|0;if((e|0)<0){break c}A=(j|0)>0?j:0;t=p<<2;d:while(1){if((k|0)==(A|0)){break a}f=i+8|0;Ca(f,a,k+v|0);c=y[i+12>>2];j=y[i+8>>2];Ca(f,b,d+k|0);if(!c){break b}h=y[i+12>>2];if(!h){break b}if(c>>>0

>>0){break c}y[i+20>>2]=c-p;y[i+16>>2]=j+t;if(h>>>0>>0){break c}j=h-l|0;y[i+12>>2]=j;y[i+8>>2]=y[i+8>>2]+(l<<2);if(q){c=_a(i+16|0);f=gc(f);h=te(c,24)&16711935|te(c&16711935,8);c=y[f>>2];c=Oa(g,h,te(c&16711935,8)|te(c,24)&16711935,E);I=f,J=te(c&16711935,8)|te(c,24)&16711935,y[I>>2]=J;j=y[i+12>>2]}if(e>>>0>j>>>0){break c}c=y[i+16>>2];if(B[i+20>>2]>>0){break c}h=y[i+8>>2];u=e<<2;m=h+u|0;n=y[i+20>>2];f=c;while(1)if((h|0)==(m|0)){if(x){if((e|0)==(n|0)|e>>>0>n>>>0|(e|0)==(j|0)){break c}c=y[f+u>>2];f=te(c&16711935,8)|te(c,24)&16711935;c=y[m>>2];c=Oa(g,f,te(c&16711935,8)|te(c,24)&16711935,D);I=m,J=te(c&16711935,8)|te(c,24)&16711935,y[I>>2]=J}k=k+1|0;continue d}else{r=y[c>>2];s=te(r&16711935,8)|te(r,24)&16711935;r=y[h>>2];r=Eb(g,s,te(r&16711935,8)|te(r,24)&16711935);I=h,J=te(r&16711935,8)|te(r,24)&16711935,y[I>>2]=J;c=c+4|0;h=h+4|0;continue}}}m=0;e=((n|0)/32|0)-(c+31>>>5|0)|0;if((e|0)<0){break c}n=q-k|0;u=32-n|0;F=(j|0)>0?j:0;G=p<<2;e:while(1){if((m|0)==(F|0)){break a}f=i+8|0;Ca(f,a,m+v|0);c=y[i+12>>2];h=y[i+8>>2];Ca(f,b,d+m|0);if(!c){break b}f=y[i+12>>2];if(!f){break b}if(c>>>0

>>0){break c}c=c-p|0;y[i+20>>2]=c;y[i+16>>2]=h+G;if(x){if(c>>>0>>0){break c}y[i+20>>2]=A}if(f>>>0>>0){break c}k=f-l|0;y[i+12>>2]=k;y[i+8>>2]=y[i+8>>2]+(l<<2);c=_a(i+16|0);j=te(c&16711935,8)|te(c,24)&16711935;if(q){c=gc(i+8|0);f=y[c>>2];f=Oa(g,j>>>n|0,te(f&16711935,8)|te(f,24)&16711935,E);h=te(f&16711935,8);I=c,J=te(f,24)&16711935|h,y[I>>2]=J;k=y[i+12>>2]}if(e>>>0>k>>>0){break c}c=y[i+16>>2];if(B[i+20>>2]>>0){break c}h=y[i+8>>2];C=e<<2;t=h+C|0;r=y[i+20>>2];f=c;while(1)if((h|0)==(t|0)){if(x){if(e>>>0>r>>>0){break c}h=j<>2];h=(te(c&16711935,8)|te(c,24)&16711935)>>>n|h}if((e|0)==(k|0)){break c}c=y[t>>2];c=Oa(g,h,te(c&16711935,8)|te(c,24)&16711935,D);I=t,J=te(c&16711935,8)|te(c,24)&16711935,y[I>>2]=J}m=m+1|0;continue e}else{H=j<>2];j=te(j&16711935,8)|te(j,24)&16711935;s=y[h>>2];s=Eb(g,H|j>>>n,te(s&16711935,8)|te(s,24)&16711935);I=h,J=te(s&16711935,8)|te(s,24)&16711935,y[I>>2]=J;c=c+4|0;h=h+4|0;continue}}}m=0;e=((n|0)/32|0)-(c+31>>>5|0)|0;if((e|0)<0){break c}n=k-q|0;t=32-n|0;F=(j|0)>0?j:0;G=p<<2;f:while(1){if((m|0)==(F|0)){break a}f=i+8|0;Ca(f,a,m+v|0);c=y[i+12>>2];h=y[i+8>>2];Ca(f,b,d+m|0);if(!c){break b}f=y[i+12>>2];if(!f){break b}if(c>>>0

>>0){break c}c=c-p|0;y[i+20>>2]=c;y[i+16>>2]=h+G;if(x){if(c>>>0>>0){break c}y[i+20>>2]=A}if(f>>>0>>0){break c}k=f-l|0;y[i+12>>2]=k;y[i+8>>2]=y[i+8>>2]+(l<<2);c=i+16|0;f=_a(c);j=te(f&16711935,8)|te(f,24)&16711935;if(q){c=_a(c);f=gc(i+8|0);h=j<>2];c=Oa(g,h|j>>>t,te(c&16711935,8)|te(c,24)&16711935,E);I=f,J=te(c&16711935,8)|te(c,24)&16711935,y[I>>2]=J;k=y[i+12>>2]}if(e>>>0>k>>>0){break c}c=y[i+16>>2];if(B[i+20>>2]>>0){break c}h=y[i+8>>2];C=e<<2;u=h+C|0;r=y[i+20>>2];f=c;while(1)if((h|0)==(u|0)){if(x){if(e>>>0>r>>>0){break c}h=j<>2];h=(te(c&16711935,8)|te(c,24)&16711935)>>>t|h}if((e|0)==(k|0)){break c}c=y[u>>2];c=Oa(g,h,te(c&16711935,8)|te(c,24)&16711935,D);I=u,J=te(c&16711935,8)|te(c,24)&16711935,y[I>>2]=J}m=m+1|0;continue f}else{H=j<>2];j=te(j&16711935,8)|te(j,24)&16711935;s=y[h>>2];s=Eb(g,H|j>>>t,te(s&16711935,8)|te(s,24)&16711935);I=h,J=te(s&16711935,8)|te(s,24)&16711935,y[I>>2]=J;c=c+4|0;h=h+4|0;continue}}}o()}}Y=i+32|0}function Dd(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0;f=Y-128|0;Y=f;n=xa(y[a+8>>2],y[a+12>>2]);y[f+124>>2]=n;if(qa(n)){Qa(n,z[a+4|0]);k=qd(b);y[f+116>>2]=k;e=0;a:{if(za(k,y[a+68>>2],f+120|0)){break a}y[f+108>>2]=y[f+120>>2];w[f+104|0]=1;q=Db(f+104|0,y[a+20>>2]);e=q;g=y[f+108>>2];h=0-g|0;y[e>>2]=z[e|0]&(g|0)!=-2147483648;y[e+4>>2]=h;y[f+100>>2]=0;w[f+96|0]=1;b:{c:{d:{e:while(1){if(B[a+16>>2]>r>>>0){if(za(k,y[a+68>>2],f+92|0)){break b}y[f+84>>2]=y[f+92>>2];e=1;w[f+80|0]=1;g=Db(f+80|0,y[a+20>>2]);s=Vc(q,y[g>>2],y[g+4>>2]);y[f+76>>2]=0;w[f+72|0]=1;while(1){f:{if(e&1){if(za(k,y[a+60>>2],f+8|0)){break b}h=Na(f+96|0,y[f+8>>2]);e=y[h+4>>2];y[f+72>>2]=y[h>>2];y[f+76>>2]=e;break f}g:{switch(za(k,y[a+64>>2],f+8|0)|0){case 0:break g;case 1:continue e;default:break b}}Na(Na(f+72|0,y[f+8>>2]),w[a+5|0])}e=1;j=0;g=y[a+20>>2];if((g|0)!=1){while(1){h=e;e=e+1|0;if(g>>>0>1<>>0){continue}break}if(Ia(b,h,f+8|0)){break b}j=z[f+8|0]}e=y[s+4>>2];if(!(y[s>>2]&1)){break b}h=e+j|0;e=h&(e^-1);if((e|0)<0){break b}t=(e|0)>=0?h:0;j=0;y[f+68>>2]=0;w[f+64|0]=1;h:while(1){if(wb(b,f+8|0)){break b}e=Tb(f- -64|0);if(z[e|0]!=1){break b}h=y[f+68>>2]|y[f+8>>2];g=(h|0)>=0;y[e>>2]=g;h=g?h:0;y[e+4>>2]=h;if(!g){break d}j=j+1|0;l=y[a+28>>2];i=y[a+24>>2];e=0;while(1){if((e|0)==(i|0)){continue h}g=l+(e<<3)|0;if(!(y[g>>2]==(j|0)&(h|0)==y[g+4>>2])){e=e+1|0;continue}break}break}i:{j:{k:{if(!z[a+1|0]){w[f+60|0]=0;y[f+56>>2]=0;break k}h=y[b>>2];g=y[b+8>>2];if(g>>>0>=B[b+4>>2]){break b}g=z[g+h|0];h=y[b+12>>2];_b(b);w[f+60|0]=0;y[f+56>>2]=0;if(g>>>7-h&1){break j}}Uc(f+56|0,y[y[a+40>>2]+(e<<2)>>2]);break i}if(za(k,y[a+72>>2],f+52|0)){break c}if(za(k,y[a+76>>2],f+48|0)){break c}if(za(k,y[a+80>>2],f+44|0)){break c}if(za(k,y[a+84>>2],f+40|0)){break c}if(za(k,y[a+88>>2],f+36|0)){break c}ta(b);j=y[y[a+40>>2]+(e<<2)>>2];if(!j){break c}l=y[f+52>>2];h=l>>31;e=y[j+8>>2]+l|0;if(e>>>0>>0?h+1|0:h){break c}i=y[f+48>>2];g=i>>31;h=i+y[j+12>>2]|0;if(h>>>0>>0?g+1|0:g){break c}g=y[b+8>>2];Cb(f+8|0,l,2,y[f+44>>2]);Cb(f+28|0,i,2,y[f+40>>2]);if(!(w[f+32|0]&1)|z[f+12|0]!=1){break c}i=db();y[f+24>>2]=i;y[i+8>>2]=h;y[i+4>>2]=e;e=z[a+2|0];y[i+20>>2]=j;w[i|0]=e;y[i+12>>2]=y[f+8>>2];e=y[f+28>>2];j=0;w[i+1|0]=0;y[i+16>>2]=e;w[i+24|0]=z[a+92|0];w[i+25|0]=z[a+93|0];w[i+26|0]=z[a+94|0];w[i+27|0]=z[a+95|0];e=Ra(b);y[f+20>>2]=e;if(fc(Tc(f+56|0,cb(i,e,c,d)))){ta(b);e=y[b+8>>2];if(e>>>0<=4294967293){h=e+2|0;e=y[b+4>>2];e=e>>>0>h>>>0?h:e;y[b+8>>2]=e}j=y[f+36>>2]==(e-g|0)}ua(f+20|0);Ua(f+24|0);if(!j){break c}}e=f+56|0;if(fc(e)){i=y[Xa(e)+8>>2];e=Xa(e);p=y[a+56>>2];j=y[e+12>>2];l:{m:{l=z[a+3|0];if(!l){e=z[f+72|0];if((p&-2)!=2){break m}if(!(e&1)){break c}g=y[f+76>>2];h=g>>31;e=i-1|0;m=e;e=e+g|0;g=m>>>0>e>>>0?h+1|0:h;g=e>>>0<2147483648&(g|0)<=0|(g|0)<0;y[f+72>>2]=0|g;y[f+76>>2]=g?e:0;if(!g){break c}break l}e=z[f+72|0];n:{switch(p|0){default:if(!(e&1)){break c}break l;case 0:case 2:break n}}if(!(e&1)){break c}g=y[f+76>>2];h=g>>31;e=j-1|0;m=e;e=e+g|0;h=m>>>0>e>>>0?h+1|0:h;g=e>>>0<2147483648&(h|0)<=0|(h|0)<0;y[f+72>>2]=0|g;y[f+76>>2]=g?e:0;if(g){break l}break c}if(!(e&1)){break c}}g=y[f+76>>2];Rc(f+8|0,l,p,g,t,i,j);m=Xa(f+56|0);e=y[f+8>>2];h=e;i=e>>31;e=y[f+12>>2];Hb(m,n,h,i,e,e>>31,y[a+52>>2]);e=y[f+16>>2];if(e){h=g>>31;m=e;e=e+g|0;g=m>>>0>e>>>0?h+1|0:h;h=e;g=e>>>0<2147483648&(g|0)<=0|(g|0)<0;e=0;y[f+72>>2]=e|g;y[f+76>>2]=g?h:0}r=r+1|0}mb(f+56|0);e=0;continue}}break}y[f+124>>2]=0;e=n;break a}o()}mb(f+56|0)}e=0}Ua(f+116|0)}la(f+124|0);Y=f+128|0;return e}function Bd(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0,C=0,D=0,E=0,F=0;f=Y-112|0;Y=f;k=xa(y[a+8>>2],y[a+12>>2]);y[f+108>>2]=k;g=0;a:{if(!qa(k)){break a}o=y[e>>2];g=0;if(!Aa(o,b,f+104|0)){break a}t=y[e+36>>2];u=y[e+32>>2];v=y[e+28>>2];x=y[e+24>>2];A=y[e+20>>2];C=y[e+16>>2];D=y[e+12>>2];E=y[e+8>>2];F=y[e+4>>2];Qa(k,z[a+4|0]);y[f+100>>2]=y[f+104>>2];w[f+96|0]=1;p=Db(f+96|0,y[a+20>>2]);e=p;h=y[f+100>>2];g=0-h|0;y[e>>2]=z[e|0]&(h|0)!=-2147483648;y[e+4>>2]=g;y[f+92>>2]=0;w[f+88|0]=1;e=y[a+16>>2];b:{c:while(1){if(e>>>0>m>>>0){y[f+84>>2]=0;w[f+80|0]=1;if(!Aa(o,b,f+76|0)){break b}y[f+68>>2]=y[f+76>>2];e=1;w[f+64|0]=1;g=Db(f- -64|0,y[a+20>>2]);q=Vc(p,y[g>>2],y[g+4>>2]);while(1){d:{if(e&1){Aa(F,b,f+4|0);e=Na(f+88|0,y[f+4>>2]);g=y[e+4>>2];y[f+80>>2]=y[e>>2];y[f+84>>2]=g;break d}if(!Aa(E,b,f+4|0)){e=y[a+16>>2];continue c}Na(Na(f+80|0,y[f+4>>2]),w[a+5|0])}e=y[a+16>>2];if(m>>>0>=e>>>0){continue c}y[f+60>>2]=0;if(y[a+20>>2]!=1){Aa(D,b,f+60|0)}e=y[q+4>>2];if(!(y[q>>2]&1)){break b}g=y[f+60>>2];n=e+g|0;if(((e^n)&(g^n))<0){break b}Ac(t,b,f+56|0);e=y[f+56>>2];if(e>>>0>=B[a+24>>2]){break b}e:{f:{g:{h:{i:{if(!z[a+1|0]){y[f+52>>2]=0;w[f+48|0]=0;y[f+44>>2]=0;break i}Aa(C,b,f+52|0);g=y[f+52>>2];w[f+48|0]=0;y[f+44>>2]=0;if(g){break h}}Uc(f+44|0,y[y[a+40>>2]+(e<<2)>>2]);break g}Aa(A,b,f+40|0);Aa(x,b,f+36|0);Aa(v,b,f+32|0);Aa(u,b,f+28|0);h=y[y[a+40>>2]+(e<<2)>>2];if(!h){break f}i=y[f+40>>2];g=i>>31;e=y[h+8>>2]+i|0;if(e>>>0>>0?g+1|0:g){break f}j=y[f+36>>2];g=j>>31;l=j+y[h+12>>2]|0;g=l>>>0>>0?g+1|0:g;if((g|0)==1|g>>>0>1){break f}Cb(f+4|0,i,1,y[f+32>>2]);Cb(f+20|0,j,1,y[f+28>>2]);if(!(w[f+24|0]&1)|z[f+8|0]!=1){break f}g=db();y[f+16>>2]=g;y[g+8>>2]=l;y[g+4>>2]=e;e=z[a+2|0];y[g+20>>2]=h;w[g|0]=e;y[g+12>>2]=y[f+4>>2];e=y[f+20>>2];w[g+1|0]=0;y[g+16>>2]=e;w[g+24|0]=z[a+92|0];w[g+25|0]=z[a+93|0];w[g+26|0]=z[a+94|0];w[g+27|0]=z[a+95|0];Tc(f+44|0,cb(g,b,c,d));Ua(f+16|0)}i=0;e=f+44|0;if(!fc(e)){break e}l=y[Xa(e)+8>>2];e=Xa(e);j=y[a+56>>2];r=y[e+12>>2];j:{k:{l:{s=z[a+3|0];if(!s){e=z[f+80|0];if((j&-2)!=2){break l}if(!(e&1)){break j}g=y[f+84>>2];e=g>>31;h=l-1|0;g=g+h|0;h=h>>>0>g>>>0?e+1|0:e;e=g;g=e>>>0<2147483648&(h|0)<=0|(h|0)<0;y[f+80>>2]=0|g;y[f+84>>2]=g?e:0;if(g){break k}break e}e=z[f+80|0];m:{switch(j|0){default:if(e&1){break k}break e;case 0:case 2:break m}}if(!(e&1)){break j}e=y[f+84>>2];g=e>>31;h=r-1|0;e=e+h|0;g=h>>>0>e>>>0?g+1|0:g;g=e>>>0<2147483648&(g|0)<=0|(g|0)<0;y[f+80>>2]=0|g;y[f+84>>2]=g?e:0;if(g){break k}break e}if(!(e&1)){break e}}g=y[f+84>>2];Rc(f+4|0,s,j,g,n,l,r);i=Xa(f+44|0);e=y[f+4>>2];h=e;j=e>>31;e=y[f+8>>2];Hb(i,k,h,j,e,e>>31,y[a+52>>2]);h=y[f+12>>2];if(h){e=g>>31;g=g+h|0;h=g>>>0>>0?e+1|0:e;e=g;g=e>>>0<2147483648&(h|0)<=0|(h|0)<0;h=0;y[f+80>>2]=g|h;y[f+84>>2]=g?e:0}i=1;m=m+1|0;break e}y[f+80>>2]=0;y[f+84>>2]=0;break e}i=0}mb(f+44|0);e=0;if(i){continue}break}break b}break}y[f+108>>2]=0;g=k;break a}g=0}la(f+108|0);Y=f+112|0;return g}function eb(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,q=0,r=0;j=Y-32|0;Y=j;a:{b:{c:{if(!qa(a)){break c}k=y[a+12>>2];if((k|0)>=(b|0)){break c}g=y[a+16>>2];if((268435452/(g|0)|0)<(b|0)){break c}if((k|0)<0){break b}d=g>>31;q=se(g,d,k);k=Z;if((k|0)==1|k>>>0>1){break b}g=se(g,d,b);d=Z;if((d|0)==1|d>>>0>1){break b}d:{if(z[a+4|0]==1){k=y[a>>2];y[a>>2]=0;y[j+20>>2]=0;_c(a,j+20|0);y[j+28>>2]=0;if(!d&g>>>0>2147479550|d){break a}e:{if(!k){g=Ya(g);break e}if(!g){ma(k);g=0;break e}f:{if(g>>>0>4294967239){break f}m=g>>>0<=8?8:g+3&-4;d=m+8|0;g=k;r=g-4|0;i=r;f=y[i>>2];e=f+i|0;h=y[e>>2];g:{h:{i:{if((h|0)!=y[(e+h|0)-4>>2]){h=f+h|0;if(h>>>0>=d+16>>>0){f=y[e+4>>2];e=y[e+8>>2];y[f+8>>2]=e;y[e+4>>2]=f;f=d+i|0;e=h-d|0;y[f>>2]=e;y[(f+(e&-4)|0)-4>>2]=e|1;e=y[f>>2]-8|0;j:{if(e>>>0<=127){n=(e>>>3|0)-1|0;break j}h=H(e);n=((e>>>29-h^4)-(h<<2)|0)+110|0;if(e>>>0<=4095){break j}e=((e>>>30-h^2)-(h<<1)|0)+71|0;n=e>>>0>=63?63:e}e=n;h=e<<4;y[f+4>>2]=h+6080;h=h+6088|0;y[f+8>>2]=y[h>>2];y[h>>2]=f;y[y[f+8>>2]+4>>2]=f;h=y[1779];f=e&31;if((e&63)>>>0>=32){f=1<>>32-f}y[1778]=e|y[1778];y[1779]=f|h;y[i>>2]=d;y[(i+(d&-4)|0)-4>>2]=d;e=1;break g}if(d>>>0>h>>>0){break i}d=y[e+4>>2];f=y[e+8>>2];y[d+8>>2]=f;y[f+4>>2]=d;y[i>>2]=h;y[(i+(h&-4)|0)-4>>2]=h;e=1;break g}if(f>>>0>=d+16>>>0){y[i>>2]=d;y[(i+(d&-4)|0)-4>>2]=d;i=d+i|0;d=f-d|0;y[i>>2]=d;y[(i+(d&-4)|0)-4>>2]=d|1;d=y[i>>2]-8|0;k:{if(d>>>0<=127){e=(d>>>3|0)-1|0;break k}f=H(d);e=((d>>>29-f^4)-(f<<2)|0)+110|0;if(d>>>0<=4095){break k}d=((d>>>30-f^2)-(f<<1)|0)+71|0;e=d>>>0>=63?63:d}f=e;d=f<<4;y[i+4>>2]=d+6080;d=d+6088|0;y[i+8>>2]=y[d>>2];y[d>>2]=i;y[y[i+8>>2]+4>>2]=i;i=y[1779];d=f&31;if((f&63)>>>0>=32){f=1<>>32-d}y[1778]=e|y[1778];y[1779]=f|i;e=1;break g}e=1;if(d>>>0<=f>>>0){break h}}e=0}}if(e){break e}g=Ya(m);if(!g){break f}l=y[r>>2]-8|0;l=l>>>0>m>>>0?m:l;if(l){p(g,k,l)}ma(k);l=g}g=l}if(!g){break a}nb(a,g);Ha(j+28|0);break d}Da(j+20|0,a);k=y[j+20>>2];l=y[j+24>>2];nb(a,ab(g,1));Da(j+12|0,a);if(l>>>0>B[j+16>>2]){break b}if(!l){break d}p(y[j+12>>2],k,l)}y[a+12>>2]=b;Da(j+20|0,a);a=y[j+24>>2];if(q>>>0>a>>>0){break b}c=0-c|0;b=q+y[j+20>>2]|0;a=a-q|0;while(1){if((a|0)<=0){break c}w[b|0]=c;a=a-1|0;b=b+1|0;continue}}Y=j+32|0;return}o()}rb();o()}function nc(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0;f=Y-48|0;Y=f;v=xa(y[a+4>>2],y[a+8>>2]);y[f+44>>2]=v;a:{b:{if(!qa(v)){break b}c:{m=y[a+4>>2];if(!m){break c}n=y[a+8>>2];L=e?n:n&2147483647;e=e<<1;B=e+2448|0;G=e+2466|0;C=e+2442|0;H=e+2436|0;I=e+2430|0;M=e+2460|0;N=e+2454|0;O=e+2424|0;e=m-1|0;p=e>>>3|0;J=(e&7)+1|0;n=0;m=0;e=0;while(1){d:{e:{if((x|0)!=(L|0)){ja(f+36|0,v,x);r=y[f+40>>2];s=y[f+36>>2];if(z[a+1|0]!=1){t=e;break e}if(z[b|0]){break b}h=A[O>>1];if(h>>>0>=d>>>0){break c}t=0;h=ka(b,(h<<3)+c|0);if((h|0)==(e|0)){break e}y[f+32>>2]=r;y[f+28>>2]=s;y[f+24>>2]=m;y[f+20>>2]=n;t=y[f+32>>2];y[f+8>>2]=y[f+28>>2];y[f+12>>2]=t;t=y[f+24>>2];y[f>>2]=y[f+20>>2];y[f+4>>2]=t;Pa(f+8|0,f);e=e^h;break d}y[f+44>>2]=0;break a}f:{g:{h:{if(x>>>0<=1){i=0;k=0;l=(x|0)!=1;if(!l){if(!m){break c}k=z[n|0]}q=A[I>>1];g=A[H>>1]&k>>>q;while(1){if((i|0)!=(p|0)){if(!l){e=i+1|0;if(e>>>0>=m>>>0){break c}k=z[e+n|0]|k<<8}j=0;e=7;while(1){if((e|0)>=0){if(z[b|0]){break b}if(d>>>0<=g>>>0){break c}u=A[B>>1]&k>>>e+q|(A[C>>1]&g)<<1;h=ka(b,(g<<3)+c|0);g=u|h;j=h<>>0<=g>>>0){break c}u=A[B>>1]&h>>>l-e|(A[C>>1]&g)<<1;i=ka(b,(g<<3)+c|0);g=u|i;j=i<<7-e|j;e=e+1|0;continue}}if(!g|!m|(m-1>>>0

>>0|p>>>0>r>>>0)|g-1>>>0

>>0){break c}k=z[n|0];F=A[I>>1];K=A[N>>1];D=z[i|0]<>1]&k>>>F|D&A[M>>1];u=p+s|0;h=n;l=s;while(1){i:{E=D<<8;if((l|0)==(u|0)){break i}h=h+1|0;k=z[h|0]|k<<8;i=i+1|0;D=E|z[i|0]<=0){if(z[b|0]){break b}if(d>>>0<=g>>>0){break c}E=A[B>>1]&k>>>e+F|(A[G>>1]&D>>>e|(A[C>>1]&g)<<1);q=ka(b,(g<<3)+c|0);g=E|q;j=q<>>0<=g>>>0){break c}h=ka(b,(g<<3)+c|0);i=7-j|0;e=h<>1]&l>>>q-j|(A[G>>1]&E>>>i|(A[C>>1]&g)<<1));j=j+1|0;continue}}if(p>>>0>=r>>>0){break c}w[p+s|0]=j;break f}if(p>>>0>=r>>>0){break c}w[u|0]=e}e=t}x=x+1|0;i=n;g=m;n=s;m=r;continue}}o()}v=0}la(f+44|0);Y=f+48|0;return v}function ic(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;a:{b:{c:{d:{if(!z[a+1|0]){g=y[a+60>>2];break d}f=y[b+4>>2];if(z[f|0]){break a}h=y[c>>2];if(h>>>0>=B[b+12>>2]){break c}g=y[a+60>>2]^ka(f,y[b+8>>2]+(h<<3)|0);y[a+60>>2]=g}if(g){ed(a,y[y[b>>2]>>2]);break b}f=y[a+24>>2];if(f>>>0<=1){if((f|0)==1){if(!y[a+40>>2]){break c}j=z[y[a+36>>2]]}i=y[c+12>>2]&j>>>y[c+8>>2];h=(f|0)!=1;while(1){if((d|0)!=(k|0)){if(!h){f=k+1|0;if(f>>>0>=B[a+40>>2]){break c}j=z[f+y[a+36>>2]|0]|j<<8}g=7;l=0;while(1){if((g|0)>=0){f=y[b+4>>2];if(z[f|0]){break a}if(B[b+12>>2]<=i>>>0){break c}f=ka(f,y[b+8>>2]+(i<<3)|0);i=f|(y[c+16>>2]&j>>>y[c+8>>2]+g|(y[c+4>>2]&i)<<1);l=f<>2]<=k>>>0){break c}w[y[a+44>>2]+k|0]=l;k=k+1|0;continue}break}f=j<<8;g=0;l=0;while(1){if((e|0)!=(g|0)){h=y[b+4>>2];if(z[h|0]){break a}if(B[b+12>>2]<=i>>>0){break c}h=ka(h,y[b+8>>2]+(i<<3)|0);j=7-g|0;l=h<>2]&f>>>j+y[c+8>>2]|(y[c+4>>2]&i)<<1);g=g+1|0;continue}break}if(B[a+48>>2]<=d>>>0){break c}w[y[a+44>>2]+d|0]=l;break b}if(!y[a+32>>2]|!y[a+40>>2]){break c}j=z[y[a+36>>2]];k=z[y[a+28>>2]]<>2];i=y[c+12>>2]&j>>>y[c+8>>2]|k&y[c+24>>2];f=0;while(1){e:{if((d|0)!=(f|0)){h=f+1|0;if(h>>>0>=B[a+32>>2]|h>>>0>=B[a+40>>2]){break c}k=z[h+y[a+28>>2]|0]<>2]|k<<8;j=z[h+y[a+36>>2]|0]|j<<8;g=7;l=0;while(1){if((g|0)<0){break e}m=y[b+4>>2];if(z[m|0]){break a}if(B[b+12>>2]<=i>>>0){break c}m=ka(m,y[b+8>>2]+(i<<3)|0);i=m|(y[c+16>>2]&j>>>y[c+8>>2]+g|(y[c+28>>2]&k>>>g|(y[c+4>>2]&i)<<1));l=m<>2];if(z[h|0]){break a}if(B[b+12>>2]<=i>>>0){break c}k=ka(h,y[b+8>>2]+(i<<3)|0);h=7-g|0;f=k<>2]&l>>>h+y[c+8>>2]|(y[c+28>>2]&j>>>h|(y[c+4>>2]&i)<<1));g=g+1|0;continue}break}if(B[a+48>>2]<=d>>>0){break c}w[y[a+44>>2]+d|0]=f;break b}if(B[a+48>>2]<=f>>>0){break c}w[y[a+44>>2]+f|0]=l;f=h;continue}}o()}return 1}return 0}function pe(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0;c=Y-48|0;Y=c;m=y[b+4>>2];t=y[b+12>>2];u=y[b+8>>2];g=y[y[b>>2]>>2];ja(c+40|0,g,y[a+24>>2]-1|0);ja(c+32|0,g,y[a+24>>2]-2|0);A=u+317736|0;n=y[c+36>>2];p=y[c+32>>2];d=y[a+24>>2];C=t>>>0>39717;q=4;a:{while(1){b:{c:{if(B[a+8>>2]>d>>>0){j=y[c+44>>2];k=y[c+40>>2];ja(c+32|0,g,d);h=y[c+36>>2];r=y[c+32>>2];y[c+40>>2]=r;y[c+44>>2]=h;d:{e:{f:{if(!z[a+1|0]){d=y[a+60>>2];break f}if(z[m|0]){break b}if(!C){break e}d=y[a+60>>2]^ka(m,A);y[a+60>>2]=d}if(d){y[c+20>>2]=j;y[c+16>>2]=k;y[c+24>>2]=r;y[c+28>>2]=h;y[c+8>>2]=r;y[c+12>>2]=h;h=y[c+20>>2];y[c>>2]=y[c+16>>2];y[c+4>>2]=h;Pa(c+8|0,c);break d}i=0;v=0;x=0;if(z[a+2|0]==1){ja(c+32|0,y[a+12>>2],y[a+24>>2]);x=y[c+36>>2];v=y[c+32>>2]}d=c+32|0;ja(d,g,y[a+24>>2]+w[a+17|0]|0);D=y[c+36>>2];E=y[c+32>>2];ja(d,g,y[a+24>>2]+w[a+19|0]|0);F=y[c+36>>2];G=y[c+32>>2];ja(d,g,y[a+24>>2]+w[a+21|0]|0);H=y[c+36>>2];I=y[c+32>>2];ja(d,g,y[a+24>>2]+w[a+23|0]|0);J=y[c+36>>2];K=y[c+32>>2];e=y[g+8>>2];i=!n|(e|0)<2?i:z[p|0]>>>6&1;i=!n|(e|0)<=0?i:z[p|0]>>>6&2|i;f=0;f=!j|(e|0)<3?f:z[k|0]>>>5&1;f=!j|(e|0)<2?f:z[k|0]>>>5&2|f;f=!j|(e|0)<=0?f:z[k|0]>>>5&4|f;s=0;d=0;while(1){if(B[a+4>>2]<=d>>>0){break d}g:{h:{if(z[a+2|0]==1){if(ia(y[y[a+12>>2]+8>>2],d,v,x)){break h}}l=ia(e,w[a+16|0]+d|0,E,D);L=ia(e,w[a+18|0]+d|0,G,F);M=ia(e,w[a+20|0]+d|0,I,H);e=ia(e,w[a+22|0]+d|0,K,J);if(z[m|0]){break b}e=f<<5|i<<12|l<<4|L<<10|M<<11|e<<15|s;if(e>>>0>=t>>>0){break e}l=ka(m,(e<<3)+u|0);e=y[g+8>>2];if(!l){break h}La(e,d,r,h,1);e=y[g+8>>2];l=1;break g}l=0}i=ia(e,d+2|0,p,n)|i<<1&6;f=ia(e,d+3|0,k,j)|f<<1&30;d=d+1|0;s=s<<1&14|l;continue}}o()}h=y[b+16>>2];if(!h){break c}if(!(_[y[y[h>>2]+8>>2]](h)|0)){break c}y[a+24>>2]=y[a+24>>2]+1;q=3}y[a+52>>2]=q;break a}d=y[a+24>>2]+1|0;y[a+24>>2]=d;p=k;n=j;continue}break}q=-1}Y=c+48|0;return q|0} +function mc(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0;f=Y-48|0;Y=f;i=xa(y[a+4>>2],y[a+8>>2]);y[f+44>>2]=i;a:{b:{if(!qa(i)){break b}Qa(i,0);K=e|2;L=5-e|0;M=4-e|0;k=e>>>1|0;N=k^3;j=e<<1;O=j+2490|0;P=j+2484|0;Q=j+2478|0;R=j+2472|0;S=k^1;T=2-k|0;x=e&1;U=x+1|0;V=j+2424|0;j=0;k=0;while(1){c:{d:{e:{if(B[a+8>>2]>m>>>0){ja(f+36|0,i,m);t=y[f+40>>2];u=y[f+36>>2];if(z[a+1|0]!=1){n=g;break e}if(z[b|0]){break b}h=A[V>>1];if(h>>>0>=d>>>0){break d}n=0;h=ka(b,(h<<3)+c|0);if((h|0)==(g|0)){break e}y[f+32>>2]=t;y[f+28>>2]=u;y[f+24>>2]=k;y[f+20>>2]=j;n=y[f+32>>2];y[f+8>>2]=y[f+28>>2];y[f+12>>2]=n;n=y[f+24>>2];y[f>>2]=y[f+20>>2];y[f+4>>2]=n;g=g^h;i=y[f+44>>2];Pa(f+8|0,f);break c}y[f+44>>2]=0;break a}C=0;D=0;E=0;if(z[a+2|0]==1){ja(f+36|0,y[a+12>>2],m);E=y[f+40>>2];D=y[f+36>>2]}g=f+36|0;i=y[f+44>>2];ja(g,i,w[a+17|0]+m|0);W=y[f+40>>2];X=y[f+36>>2];F=0;G=0;H=0;I=0;J=0;if(!e){ja(g,i,w[a+19|0]+m|0);C=y[f+40>>2];F=y[f+36>>2];ja(g,i,w[a+21|0]+m|0);G=y[f+40>>2];H=y[f+36>>2];ja(g,i,w[a+23|0]+m|0);J=y[f+36>>2];I=y[f+40>>2]}h=y[i+8>>2];p=ia(h,U,r,s)|ia(h,x,r,s)<<1;p=!s|(e|0)!=1|(h|0)<=0?p:z[r|0]>>>5&4|p;q=ia(h,T,j,k)|ia(h,S,j,k)<<1;q=!k|e>>>0>1|(h|0)<=0?q:z[j|0]>>>5&4|q;v=0;g=0;while(1){if(B[a+4>>2]<=g>>>0){g=n;break c}f:{g:{if(z[a+2|0]==1){if(ia(y[y[a+12>>2]+8>>2],g,D,E)){break g}}if(z[b|0]){break b}l=q<>1]|v;if(!e){l=ia(h,w[a+18|0]+g|0,F,C)<<10|ia(h,w[a+20|0]+g|0,H,G)<<11|ia(h,w[a+22|0]+g|0,J,I)<<15|l}if(d>>>0<=l>>>0){break d}l=ka(b,(l<<3)+c|0);h=y[i+8>>2];if(!l){break g}La(h,g,u,t,1);h=y[i+8>>2];l=1;break f}l=0}p=(ia(h,g+K|0,r,s)|p<<1)&A[Q>>1];q=(ia(h,g+N|0,j,k)|q<<1)&A[P>>1];g=g+1|0;v=A[O>>1]&(l|v<<1);continue}}o()}m=m+1|0;r=j;s=k;j=u;k=t;continue}}i=0}la(f+44|0);Y=f+48|0;return i}function ke(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0;k=Y-16|0;Y=k;h=y[b+4>>2];l=y[b+12>>2];m=y[b+8>>2];n=y[y[b>>2]>>2];if(!y[a+48>>2]){Da(k+8|0,n);d=y[k+12>>2];y[a+44>>2]=y[k+8>>2];y[a+48>>2]=d}a:{b:{c:{d=y[a+4>>2];if(!d){break c}r=m+3240|0;d=d-1|0;i=d>>>3|0;p=(d&7)+1|0;c=y[a+24>>2];s=l>>>0>405;while(1){d:{if(B[a+8>>2]<=c>>>0){c=4}else{e:{if(!z[a+1|0]){c=y[a+60>>2];break e}if(z[h|0]){break b}if(!s){break c}c=y[a+60>>2]^ka(h,r);y[a+60>>2]=c}f:{if(c){ed(a,n);break f}c=0;g=0;g:{if(!y[a+24>>2]){h:{while(1){e=7;f=0;if((g|0)==(i|0)){break h}i:{while(1){if((e|0)>=0){if(z[h|0]){break i}if(c>>>0>=l>>>0){break c}d=ka(h,(c<<3)+m|0);c=d|c<<1&1006;f=d<>2]<=g>>>0){break c}w[y[a+44>>2]+g|0]=f;g=g+1|0;continue}break}if(g>>>0>>0){break b}}e=0;f=0;while(1){if((e|0)==(p|0)){break g}if(z[h|0]){break b}if(c>>>0>=l>>>0){break c}d=ka(h,(c<<3)+m|0);c=d|c<<1&1006;f=d<<7-e|f;e=e+1|0;continue}}if(!y[a+40>>2]){break c}g=z[y[a+36>>2]];e=g>>>1&112;d=0;while(1){j:{if((d|0)!=(i|0)){j=d+1|0;if(j>>>0>=B[a+40>>2]){break c}g=z[j+y[a+36>>2]|0]|g<<8;c=7;f=0;while(1){if((c|0)<0){break j}if(z[h|0]){break b}if(e>>>0>=l>>>0){break c}q=ka(h,(e<<3)+m|0);e=q|(g>>>c+1&16|e<<1&1006);f=q<>>0>=l>>>0){break c}j=ka(h,(e<<3)+m|0);e=j|(d>>>8-c&16|e<<1&1006);f=j<<7-c|f;c=c+1|0;continue}break}if(B[a+48>>2]<=i>>>0){break c}w[y[a+44>>2]+i|0]=f;break f}if(B[a+48>>2]<=d>>>0){break c}w[y[a+44>>2]+d|0]=f;d=j;continue}}if(B[a+48>>2]<=i>>>0){break c}w[y[a+44>>2]+i|0]=f}Kb(a,n);d=y[b+16>>2];if(!d){break d}if(!(_[y[y[d>>2]+8>>2]](d)|0)){break d}y[a+24>>2]=y[a+24>>2]+1;c=3}y[a+52>>2]=c;break a}c=y[a+24>>2]+1|0;y[a+24>>2]=c;continue}}o()}c=-1}Y=k+16|0;return c|0}function $d(a,b,c,d,e,f){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0;g=Y-80|0;Y=g;w[g+72|0]=0;y[g+68>>2]=0;i=c+31>>>3&536870908;k=E(i,d);h=yd(k,1);y[g>>2]=0;j=g+68|0;nb(j,h);Ha(g);a:{b:{if(!qa(j)){break b}j=Nb(j);h=g+56|0;y[g+60>>2]=h;y[g+56>>2]=h;y[g+64>>2]=0;y[g+48>>2]=0;y[g+52>>2]=0;l=a;y[g+32>>2]=a;y[g+36>>2]=b;y[g>>2]=c;h=e;y[g+24>>2]=h;y[g+28>>2]=f;y[g+16>>2]=0;y[g+20>>2]=0;y[g+40>>2]=j;y[g+44>>2]=k;y[g+8>>2]=0;y[g+12>>2]=0;y[g+4>>2]=d;y[g+48>>2]=i;a=se(d,0,i);if(Z|a>>>0>k>>>0){break a}e=j;while(1){if((a|0)>0){w[e|0]=0;a=a-1|0;e=e+1|0;continue}break}a=b;b=g+56|0;a=zc(na(116),l,a,b,0);if(f){b=zc(na(116),h,f,b,1);y[g+76>>2]=0;zb(a,b);Ab(g+76|0)}e=c+7>>>3|0;y[g+76>>2]=0;f=g+52|0;zb(f,a);Ab(g+76|0);c:{d:{b=y[g+52>>2];a=y[b>>2];if(!a){break d}if(!yc(a)){break d}y[b+56>>2]=-1;a=0;break c}y[b+52>>2]=0;a=na(20);y[a+16>>2]=0;y[a+8>>2]=0;y[a+12>>2]=0;w[a+4|0]=0;y[a>>2]=0;e:{if((c|d)<0|i>>>0>268435452){break e}h=i<<3;if((h|0)<(c|0)|(!(!i|(d|0)<=0|k)|(2147483616/(h>>>0)|0)<(d|0))){break e}y[a+16>>2]=i;y[a+12>>2]=d;y[a+8>>2]=c;y[g+76>>2]=j;_c(a,g+76|0)}y[g+76>>2]=0;oa(b+32|0,a);la(g+76|0);w[b+50|0]=1;a=id(b)}f:{while(1){g:{a=a&1;h:{b=y[y[g+52>>2]+56>>2];if((b|0)!=4){break h}zb(g+52|0,0);b=-1;if(!a){break h}i:{b=E(y[g+48>>2],y[g+4>>2]);if(b>>>0>B[g+44>>2]){break i}a=y[g+40>>2];if(a&3){break i}c=(b&-4)+a|0;b=4;while(1){if((a|0)==(c|0)){break h}y[a>>2]=y[a>>2]^-1;a=a+4|0;continue}}o()}j:{switch(b-3|0){case 0:break j;case 1:break g;default:break f}}a=id(y[g+52>>2]);continue}break}X(Nb(g+68|0)|0,e|0,i|0,d|0)}Ab(f);if(!y[g+64>>2]){break b}a=y[g+60>>2];b=y[a>>2];c=y[y[g+56>>2]+4>>2];y[b+4>>2]=c;y[c>>2]=b;y[g+64>>2]=0;while(1){if((g+56|0)==(a|0)){break b}b=y[a+4>>2];oc(a);a=b;continue}}ub(g+68|0);Y=g+80|0;return}o()}function Ya(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0;a:{b:{while(1){if(a>>>0>4294967239){break b}b=y[1779];c=b;f=y[1778];a=a>>>0<=8?8:a+3&-4;c:{if(a>>>0<=127){d=(a>>>3|0)-1|0;break c}e=H(a);d=((a>>>29-e^4)-(e<<2)|0)+110|0;if(a>>>0<=4095){break c}e=((a>>>30-e^2)-(e<<1)|0)+71|0;d=e>>>0>=63?63:e}g=d;d=g&31;if((g&63)>>>0>=32){e=0;b=b>>>d|0}else{e=b>>>d|0;b=((1<>>d}if(b|e){while(1){d=e;d:{if(d|b){c=d-1|0;e=c+1|0;f=c;c=b-1|0;f=(c|0)!=-1?e:f;e=H(d^f);e=(e|0)==32?H(b^c)+32|0:e;c=63-e|0;Z=0-(e>>>0>63)|0;break d}Z=0;c=64}f=c;c=f&31;if((f&63)>>>0>=32){e=0;h=d>>>c|0}else{e=d>>>c|0;h=((1<>>c}g=f+g|0;b=g<<4;d=y[b+6088>>2];c=b+6080|0;e:{if((d|0)!=(c|0)){b=dc(d,a);if(b){break a}b=y[d+4>>2];f=y[d+8>>2];y[b+8>>2]=f;y[f+4>>2]=b;y[d+8>>2]=c;y[d+4>>2]=y[c+4>>2];y[c+4>>2]=d;y[y[d+4>>2]+8>>2]=d;g=g+1|0;b=(e&1)<<31|h>>>1;e=e>>>1|0;break e}j=y[1779];f=g&63;b=f;d=b&31;if(b>>>0>=32){b=0;c=-1>>>d|0}else{b=-1>>>d|0;c=b|(1<>>0>=32){b=c<>>32-d|b<>>0>=32){b=-1<>>32-b}k=c&-2;c=f&31;if(f>>>0>=32){f=0;c=b>>>c|0}else{f=b>>>c|0;c=((1<>>c}b=c|i;Z=d|f;y[1778]=y[1778]&b;y[1779]=Z&j;b=h^1}if(b|e){continue}break}f=y[1778];c=y[1779]}e=H(c);d=63-((e|0)==32?H(f)+32|0:e)|0;f:{if(!(c|f)){g=0;break f}b=d<<4;g=y[b+6088>>2];if(!c&f>>>0<1073741824){break f}e=98;c=b+6080|0;if((c|0)==(g|0)){break f}while(1){b=dc(g,a);if(b){break a}g=y[g+8>>2];if((c|0)==(g|0)){break f}b=e;e=b-1|0;if(b){continue}break}}if(Fd(a+48|0)){continue}break}if(!g){break b}e=(d<<4)+6080|0;if((e|0)==(g|0)){break b}while(1){b=dc(g,a);if(b){break a}g=y[g+8>>2];if((e|0)!=(g|0)){continue}break}}b=0}return b}function Ld(a,b){a=a|0;b=+b;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;c=a<<3;o=c+7136|0;i=D[c+7168>>3];if(i!=0){l=D[o>>3];m=J(l,b);b=m-l;if(b<0x10000000000000000&b>=0){d=~~b>>>0;if(G(b)>=1){c=~~(b>0?I(K(b*2.3283064365386963e-10),4294967295):L((b-+(~~b>>>0>>>0))*2.3283064365386963e-10))>>>0}else{c=0}}else{c=0}if(i<0x10000000000000000&i>=0){h=~~i>>>0;if(G(i)>=1){e=~~(i>0?I(K(i*2.3283064365386963e-10),4294967295):L((i-+(~~i>>>0>>>0))*2.3283064365386963e-10))>>>0}else{e=0}}else{e=0}a:{b:{c:{d:{e:{f:{g:{h:{i:{j:{k:{if(c){if(!h){break k}if(!e){break j}f=H(e)-H(c)|0;if(f>>>0<=31){break i}break c}if((e|0)==1|e>>>0>1){break c}Z=0;c=(d>>>0)/(h>>>0)|0;break a}if(!d){break h}if(!e|e-1&e){break g}c=c>>>ue(e)|0;Z=0;break a}if(!(h-1&h)){break f}k=(H(h)+33|0)-H(c)|0;j=0-k|0;break d}k=f+1|0;j=63-f|0;break d}Z=0;c=(c>>>0)/(e>>>0)|0;break a}f=H(e)-H(c)|0;if(f>>>0<31){break e}break c}if((h|0)==1){break b}h=ue(h);e=h&31;if((h&63)>>>0>=32){c=c>>>e|0}else{f=c>>>e|0;c=((1<>>e}Z=f;break a}k=f+1|0;j=63-f|0}f=k&63;g=f&31;if(f>>>0>=32){f=0;n=c>>>g|0}else{f=c>>>g|0;n=((1<>>g}j=j&63;g=j&31;if(j>>>0>=32){c=d<>>32-g|c<>>31;f=n<<1|c>>>31;g=s-(p+(f>>>0>j>>>0)|0)>>31;q=g&h;n=f-q|0;f=p-((e&g)+(f>>>0>>0)|0)|0;c=c<<1|d>>>31;d=r|d<<1;r=g&1;k=k-1|0;if(k){continue}break}}Z=c<<1|d>>>31;c=r|d<<1;break a}d=0;c=0}Z=c;c=d}d=Z;c=c+1|0;d=c?d:d+1|0;l=(+(c>>>0)+ +(d>>>0)*4294967296)*i+l;m=l-m}D[o>>3]=l;Q(a|0,+m)|0;a=(a|0)==2?27:(a|0)==1?26:14;c=a-1|0;l:{if(y[1780]>>>c&1){y[1782]=y[1782]|1<>2];if(c){_[c|0](a)}}}function ne(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0,C=0,D=0,E=0,F=0;c=Y-48|0;Y=c;n=y[b+4>>2];t=y[b+12>>2];u=y[b+8>>2];i=y[y[b>>2]>>2];ja(c+40|0,i,y[a+24>>2]-1|0);ja(c+32|0,i,y[a+24>>2]-2|0);C=u+15528|0;l=y[c+36>>2];m=y[c+32>>2];e=y[a+24>>2];D=t>>>0>1941;p=4;a:{while(1){b:{c:{if(B[a+8>>2]>e>>>0){j=y[c+44>>2];k=y[c+40>>2];ja(c+32|0,i,e);h=y[c+36>>2];q=y[c+32>>2];y[c+40>>2]=q;y[c+44>>2]=h;d:{e:{f:{if(!z[a+1|0]){e=y[a+60>>2];break f}if(z[n|0]){break b}if(!D){break e}e=y[a+60>>2]^ka(n,C);y[a+60>>2]=e}if(e){y[c+20>>2]=j;y[c+16>>2]=k;y[c+24>>2]=q;y[c+28>>2]=h;y[c+8>>2]=q;y[c+12>>2]=h;h=y[c+20>>2];y[c>>2]=y[c+16>>2];y[c+4>>2]=h;Pa(c+8|0,c);break d}f=0;v=0;x=0;if(z[a+2|0]==1){ja(c+32|0,y[a+12>>2],y[a+24>>2]);x=y[c+32>>2];v=y[c+36>>2]}ja(c+32|0,i,y[a+24>>2]+w[a+17|0]|0);E=y[c+36>>2];F=y[c+32>>2];d=y[i+8>>2];f=!l|(d|0)<3?f:z[m|0]>>>5&1;f=!l|(d|0)<2?f:z[m|0]>>>5&2|f;f=!l|(d|0)<=0?f:z[m|0]>>>5&4|f;g=0;g=!j|(d|0)<3?g:z[k|0]>>>5&1;g=!j|(d|0)<2?g:z[k|0]>>>5&2|g;g=!j|(d|0)<=0?g:z[k|0]>>>5&4|g;s=0;e=0;while(1){if(B[a+4>>2]<=e>>>0){break d}g:{h:{if(z[a+2|0]==1){if(ia(y[y[a+12>>2]+8>>2],e,x,v)){break h}}d=ia(d,w[a+16|0]+e|0,F,E);if(z[n|0]){break b}d=g<<4|f<<9|d<<3|s;if(d>>>0>=t>>>0){break e}r=ka(n,(d<<3)+u|0);d=y[i+8>>2];if(!r){break h}La(d,e,q,h,1);d=y[i+8>>2];r=1;break g}r=0}A=e+3|0;f=ia(d,A,m,l)|f<<1&14;g=ia(d,A,k,j)|g<<1&30;e=e+1|0;s=s<<1&6|r;continue}}o()}h=y[b+16>>2];if(!h){break c}if(!(_[y[y[h>>2]+8>>2]](h)|0)){break c}y[a+24>>2]=y[a+24>>2]+1;p=3}y[a+52>>2]=p;break a}e=y[a+24>>2]+1|0;y[a+24>>2]=e;m=k;l=j;continue}break}p=-1}Y=c+48|0;return p|0}function le(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0,C=0,D=0,E=0;c=Y-48|0;Y=c;l=y[b+4>>2];s=y[b+12>>2];t=y[b+8>>2];i=y[y[b>>2]>>2];ja(c+40|0,i,y[a+24>>2]-1|0);ja(c+32|0,i,y[a+24>>2]-2|0);A=t+1832|0;m=y[c+36>>2];n=y[c+32>>2];d=y[a+24>>2];C=s>>>0>229;a:{while(1){b:{c:{if(B[a+8>>2]<=d>>>0){d=4}else{j=y[c+44>>2];k=y[c+40>>2];ja(c+32|0,i,d);f=y[c+36>>2];p=y[c+32>>2];y[c+40>>2]=p;y[c+44>>2]=f;d:{e:{f:{if(!z[a+1|0]){d=y[a+60>>2];break f}if(z[l|0]){break b}if(!C){break e}d=y[a+60>>2]^ka(l,A);y[a+60>>2]=d}if(d){y[c+20>>2]=j;y[c+16>>2]=k;y[c+24>>2]=p;y[c+28>>2]=f;y[c+8>>2]=p;y[c+12>>2]=f;f=y[c+20>>2];y[c>>2]=y[c+16>>2];y[c+4>>2]=f;Pa(c+8|0,c);break d}g=0;u=0;v=0;if(z[a+2|0]==1){ja(c+32|0,y[a+12>>2],y[a+24>>2]);v=y[c+32>>2];u=y[c+36>>2]}ja(c+32|0,i,y[a+24>>2]+w[a+17|0]|0);D=y[c+36>>2];E=y[c+32>>2];e=y[i+8>>2];g=!m|(e|0)<2?g:z[n|0]>>>6&1;g=!m|(e|0)<=0?g:z[n|0]>>>6&2|g;h=0;h=!j|(e|0)<2?h:z[k|0]>>>6&1;h=!j|(e|0)<=0?h:z[k|0]>>>6&2|h;r=0;d=0;while(1){if(B[a+4>>2]<=d>>>0){break d}g:{h:{if(z[a+2|0]==1){if(ia(y[y[a+12>>2]+8>>2],d,v,u)){break h}}e=ia(e,w[a+16|0]+d|0,E,D);if(z[l|0]){break b}e=h<<3|g<<7|e<<2|r;if(e>>>0>=s>>>0){break e}q=ka(l,(e<<3)+t|0);e=y[i+8>>2];if(!q){break h}La(e,d,p,f,1);e=y[i+8>>2];q=1;break g}q=0}x=d+2|0;g=ia(e,x,n,m)|g<<1&6;h=ia(e,x,k,j)|h<<1&14;d=d+1|0;r=r<<1&2|q;continue}}o()}f=y[b+16>>2];if(!f){break c}if(!(_[y[y[f>>2]+8>>2]](f)|0)){break c}y[a+24>>2]=y[a+24>>2]+1;d=3}y[a+52>>2]=d;break a}d=y[a+24>>2]+1|0;y[a+24>>2]=d;n=k;m=j;continue}break}d=-1}Y=c+48|0;return d|0}function ec(a,b,c,d,e,f,g,h){var i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,A=0,C=0,D=0,E=0;k=((b&536870911)<<3|a>>>29)&-8;m=e;n=g;p=d;q=f;r=1;e=-1;a:{b:{c:while(1){j=r&1;u=j?326:325;v=j?1392:1056;w=j?325:326;x=j?1056:1392;d:while(1){A=e+1|0;t=e>>>3|0;C=t+q|0;D=1<<((e^-1)&7);d=y[c>>2];E=(e|0)<0;while(1){if(d>>>0>=k>>>0){break b}if(E){f=1}else{if(n>>>0<=t>>>0){break a}f=(z[C|0]&D)!=0}d=h;g=d;l=f^1;i=cc(q,n,d,A,l);s=d;e:{if((i|0)>=(d|0)){break e}f:{if((f|0)==(j|0)){l=f;break f}i=cc(q,n,h,i+1|0,f)}s=h;if((d|0)<=(i|0)){break e}g=i;s=cc(q,n,d,i+1|0,l)}d=s;g:{f=0;h:{if(Ka(a,b,c)){break h}if(B[c>>2]>=k>>>0){break b}i=Ka(a,b,c);if(B[c>>2]>=k>>>0){break b}l=Ka(a,b,c);f=l?1:-1;if(i){break h}if(l){d=0;while(1){f=bc(x,w,a,b,c);d=f+d|0;if((f|0)>63){continue}break}d=(e>>>31|0)+d|0;if((d|0)<0){break b}f=d+e|0;if(!j){lb(p,m,h,e,f)}d=0;while(1){e=bc(v,u,a,b,c);d=e+d|0;if((e|0)>63){continue}break}if((d|0)<0){break b}e=d+f|0;if(j){lb(p,m,h,f,e)}if((e|0)<(h|0)){continue d}break b}if(B[c>>2]>=k>>>0){break b}if(Ka(a,b,c)){if(!j){lb(p,m,h,e,d)}e=d;if((d|0)<(h|0)){continue d}break b}if(B[c>>2]>=k>>>0){break b}d=Ka(a,b,c);if(B[c>>2]>=k>>>0){break b}i=Ka(a,b,c);f=i?2:-2;if(d){break h}d=y[c>>2];if(!i){break g}if(d>>>0>=k>>>0){break b}f=Ka(a,b,c)?3:-3}d=f+g|0;if(!j){lb(p,m,h,e,d)}if((d|0)<=(e|0)|(d|0)>=(h|0)){break b}r=r^1;e=d;continue c}if(d>>>0>=k>>>0){break b}f=Ka(a,b,c);d=y[c>>2];if(f){d=d+3|0;y[c>>2]=d;continue}break}break}break}y[c>>2]=d+5}return}o()}function uc(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0;f=Y-32|0;Y=f;m=xa(y[a>>2],y[a+4>>2]);y[f+28>>2]=m;a:{if(!qa(m)){break a}Qa(m,z[a+20|0]);b:{while(1){if(!k&l>>>0>=B[a+36>>2]|k){y[f+28>>2]=0;d=m;break a}d=y[b>>2];g=y[b+4>>2];e=0;y[f+24>>2]=0;y[f+16>>2]=0;y[f+20>>2]=0;c=g-d|0;h=c>>2;c:{d:{if((d|0)==(g|0)){j=0;break d}if(h>>>0>=536870912){break c}j=na(c<<1);y[f+16>>2]=j;e=(h<<3)+j|0;y[f+24>>2]=e;c=j;while(1){if((c|0)!=(e|0)){y[c>>2]=0;y[c+4>>2]=0;c=c+8|0;continue}break}y[f+20>>2]=e}if(h>>>0>e-j>>3>>>0){break b}c=j;while(1){if((d|0)==(g|0)){e:{c=y[a+44>>2];d=se(l,k,A[a+48>>1]);e=c+d|0;q=e;c=Z+(c>>31)|0;r=d>>>0>e>>>0?c+1|0:c;c=y[a+40>>2];d=se(l,k,A[a+50>>1]);e=c+d|0;s=e;c=Z+(c>>31)|0;t=d>>>0>e>>>0?c+1|0:c;g=0;e=0;f:while(1){if(!e&B[a+32>>2]<=g>>>0|e){break e}i=y[b>>2];p=y[b+4>>2]-i>>2;c=0;d=0;while(1){h=c&255;if(h>>>0>=p>>>0){c=y[a+12>>2]-1|0;h=y[y[y[a+16>>2]>>2]+((c>>>0>>0?c:d)<<2)>>2];d=se(g,e,A[a+48>>1]);c=d+s|0;i=Z+t|0;d=c>>>0>>0?i+1|0:i;i=se(g,e,A[a+50>>1]);p=q-i|0;u=(d&255)<<24|c>>>8;n=d>>8;c=r-(Z+(i>>>0>q>>>0)|0)|0;d=c>>8;Hb(h,m,u,n,(c&255)<<24|p>>>8,d,y[a+24>>2]);g=g+1|0;e=g?e:e+1|0;continue f}else{n=(h<<3)+j|0;d=d|ia(y[y[i+(h<<2)>>2]+8>>2],g,y[n>>2],y[n+4>>2])<>2],l);e=y[f+12>>2];y[c>>2]=y[f+8>>2];y[c+4>>2]=e;c=c+8|0;d=d+4|0;continue}break}l=l+1|0;k=l?k:k+1|0;ya(f+16|0);continue}break}Ba()}o()}la(f+28|0);Y=f+32|0;return d}function sb(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,q=0,r=0,s=0,t=0,u=0;f=Y-16|0;Y=f;a:{j=xa(d,e);if(!qa(j)){break a}if(!qa(a)|(b|0)<0|(c|0)<0|y[a+8>>2]<=(b|0)){break a}g=y[a+12>>2];if((g|0)<=(c|0)){break a}b:{if(!(b&7)){d=y[a+16>>2];b=b>>>3|0;if(d>>>0<=b>>>0){break b}d=d-b|0;e=y[j+16>>2];d=d>>>0>>0?d:e;if(!d){break b}e=0;h=g-c|0;i=y[j+12>>2];h=(h|0)<(i|0)?h:i;h=(h|0)>0?h:0;while(1){if((e|0)==(h|0)){break a}ja(f+8|0,a,c+e|0);i=y[f+12>>2];if(i>>>0>>0|d>>>0>i-b>>>0){break b}i=y[f+8>>2];ja(f,j,e);if(d>>>0>B[f+4>>2]){break b}if(d){p(y[f>>2],b+i|0,d)}e=e+1|0;continue}}h=b>>>5|0;e=y[a+16>>2]>>>2|0;if(h>>>0>=e>>>0){break b}d=y[j+16>>2]>>>2|0;if(!d){break b}e=e-h|0;i=d>>>0>e>>>0?e:d;q=b&31;d=g-c|0;e=y[j+12>>2];d=(d|0)<(e|0)?d:e;r=(d|0)>0?d:0;s=b^-1;c:while(1){if((l|0)==(r|0)){break a}d=f+8|0;ja(d,a,c+l|0);e=y[f+8>>2];if(e&3){break b}g=y[f+12>>2]>>>2|0;if(g>>>0>>0){break b}y[f+12>>2]=g-h;y[f+8>>2]=e+(h<<2);Ca(f,j,l);if(B[f+4>>2]>>0){break b}e=y[f>>2];g=_a(d);d=y[f+8>>2];n=y[f+12>>2];k=i>>>0>n>>>0?n:i;if(k>>>0>B[f+12>>2]){break b}k=(k<<2)+e|0;g=te(g,24)&16711935|te(g&16711935,8);while(1)if((e|0)==(k|0)){if(i>>>0>n>>>0){d=g<>2]=u}l=l+1|0;continue c}else{m=g<>2];g=te(g&16711935,8)|te(g,24)&16711935;m=m|g>>>1>>>s;t=e,u=te(m&16711935,8)|te(m,24)&16711935,y[t>>2]=u;d=d+4|0;e=e+4|0;continue}}}o()}Y=f+16|0;return j}function je(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,x=0,A=0;c=Y-48|0;Y=c;i=y[b+4>>2];q=y[b+12>>2];r=y[b+8>>2];h=y[y[b>>2]>>2];ja(c+40|0,h,y[a+24>>2]-1|0);u=r+3240|0;d=y[a+24>>2];v=q>>>0>405;j=4;a:{while(1){b:{c:{if(B[a+8>>2]>d>>>0){k=y[c+44>>2];l=y[c+40>>2];ja(c+32|0,h,d);f=y[c+36>>2];m=y[c+32>>2];y[c+40>>2]=m;y[c+44>>2]=f;d:{e:{f:{if(!z[a+1|0]){d=y[a+60>>2];break f}if(z[i|0]){break b}if(!v){break e}d=y[a+60>>2]^ka(i,u);y[a+60>>2]=d}if(d){y[c+20>>2]=k;y[c+16>>2]=l;y[c+24>>2]=m;y[c+28>>2]=f;y[c+8>>2]=m;y[c+12>>2]=f;f=y[c+20>>2];y[c>>2]=y[c+16>>2];y[c+4>>2]=f;Pa(c+8|0,c);break d}g=0;s=0;t=0;if(z[a+2|0]==1){ja(c+32|0,y[a+12>>2],y[a+24>>2]);t=y[c+36>>2];s=y[c+32>>2]}ja(c+32|0,h,y[a+24>>2]+w[a+17|0]|0);x=y[c+36>>2];A=y[c+32>>2];e=y[h+8>>2];g=!k|(e|0)<2?g:z[l|0]>>>6&1;g=!k|(e|0)<=0?g:z[l|0]>>>6&2|g;p=0;d=0;while(1){if(B[a+4>>2]<=d>>>0){break d}g:{h:{if(z[a+2|0]==1){if(ia(y[y[a+12>>2]+8>>2],d,s,t)){break h}}e=ia(e,w[a+16|0]+d|0,A,x);if(z[i|0]){break b}e=g<<5|e<<4|p;if(e>>>0>=q>>>0){break e}n=ka(i,(e<<3)+r|0);e=y[h+8>>2];if(!n){break h}La(e,d,m,f,1);e=y[h+8>>2];n=1;break g}n=0}g=ia(e,d+2|0,l,k)|g<<1&30;d=d+1|0;p=p<<1&14|n;continue}}o()}f=y[b+16>>2];if(!f){break c}if(!(_[y[y[f>>2]+8>>2]](f)|0)){break c}y[a+24>>2]=y[a+24>>2]+1;j=3}y[a+52>>2]=j;break a}d=y[a+24>>2]+1|0;y[a+24>>2]=d;continue}break}j=-1}Y=c+48|0;return j|0}function Hd(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;c=y[b+56>>2];d=y[b+60>>2];f=b+48|0;Hc(c,d,f);h=((d&536870911)<<3|c>>>29)&-8;if(h>>>0<=B[b+48>>2]){y[a>>2]=0;y[a+4>>2]=0;return}Ic(y[b+64>>2],y[b+68>>2]);i=b- -64|0;c=y[b+44>>2];a:{if((c|0)<0){e=y[b+64>>2];c=y[b+68>>2]-e|0;d=0;l=c;g=y[b+76>>2];c=y[b+80>>2]-g|0;ec(y[b+56>>2],y[b+60>>2],f,e|d,l,d|g,c,y[b+4>>2]);Gc(b+76|0,i);break a}if(!c){d=y[b+64>>2];c=y[b+68>>2]-d|0;Fc(y[b+56>>2],y[b+60>>2],f,d,c,y[b+4>>2]);break a}e=Ka(y[b+56>>2],y[b+60>>2],f);g=y[b+64>>2];c=y[b+68>>2]-g|0;d=y[b+56>>2];j=y[b+60>>2];b:{if(e){Fc(d,j,f,g,c,y[b+4>>2]);break b}l=d;d=0;e=c;k=y[b+76>>2];c=y[b+80>>2]-k|0;ec(l,j,f,d|g,e,d|k,c,y[b+4>>2])}Gc(b+76|0,i)}if(z[b+53|0]==1){Hc(y[b+56>>2],y[b+60>>2],f)}c:{d:{e:{if(z[b+52|0]!=1){break e}d=y[f>>2];if(h>>>0<=d>>>0){break e}e=d+7&-8;i=y[b+56>>2];g=y[b+60>>2];while(1){c=d>>>3|0;h=c+i|0;j=1<<((d^-1)&7);k=c>>>0>>0;c=1;f:{while(1){if(!c|d>>>0>=e>>>0){break f}if(!k){break d}if(z[h|0]&j){c=0;w[b+52|0]=0;continue}break}d=d+1|0;continue}break}if(!c){break e}y[f>>2]=e}c=y[b+64>>2];if(!z[b+54|0]){d=y[b+68>>2]-c|0;break c}if(c&3){break d}d=y[b+68>>2]-c|0;e=c+(d&-4)|0;b=c;while(1){if((b|0)==(e|0)){break c}y[b>>2]=y[b>>2]^-1;b=b+4|0;continue}}o()}y[a+4>>2]=d;y[a>>2]=c}function hd(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0;d=Y-32|0;Y=d;i=y[a+24>>2];y[d+16>>2]=0;a:{b:{switch(z[a+3|0]){case 0:c=xd(a)?3:4;break a;case 1:c=wd(a)?5:6;break a;case 2:c=Qb(a)?7:8;break a;default:break b}}c=Qb(a)?9:10}e=Y-48|0;Y=e;y[e+24>>2]=0;if(c){y[e+16>>2]=0;y[e+12>>2]=c;y[e+8>>2]=2600;g=e+8|0;y[e+24>>2]=g}h=e+8|0;c:{if((h|0)==(d|0)){break c}c=y[d+16>>2];if((g|0)==(h|0)){if((d|0)==(c|0)){f=e+32|0;_[y[y[g>>2]+12>>2]](g,f);c=y[e+24>>2];_[y[y[c>>2]+16>>2]](c);y[e+24>>2]=0;c=y[d+16>>2];_[y[y[c>>2]+12>>2]](c,h);c=y[d+16>>2];_[y[y[c>>2]+16>>2]](c);y[d+16>>2]=0;y[e+24>>2]=h;_[y[y[e+32>>2]+12>>2]](f,d);_[y[y[e+32>>2]+16>>2]](f);y[d+16>>2]=d;break c}_[y[y[g>>2]+12>>2]](g,d);c=y[e+24>>2];_[y[y[c>>2]+16>>2]](c);y[e+24>>2]=y[d+16>>2];y[d+16>>2]=d;break c}if((d|0)==(c|0)){f=e+8|0;_[y[y[c>>2]+12>>2]](c,f);c=y[d+16>>2];_[y[y[c>>2]+16>>2]](c);y[d+16>>2]=y[e+24>>2];y[e+24>>2]=f;break c}y[e+24>>2]=c;y[d+16>>2]=g}gd(e+8|0);Y=e+48|0;c=y[y[b>>2]>>2];f=y[d+16>>2];y[d+28>>2]=b;if(f){f=_[y[y[f>>2]+24>>2]](f,a,d+28|0)|0;y[a+64>>2]=0;y[a+52>>2]=f;b=y[c+8>>2];y[a+68>>2]=i;y[a+72>>2]=b;y[a+76>>2]=y[a+24>>2];if((f|0)==4){y[a+24>>2]=0}gd(d);Y=d+32|0;return f}a=Fb(4);y[a>>2]=5228;P(a|0,5240,11);o()}function hb(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,q=0,r=0,s=0,t=0,u=0;d=Y-32|0;Y=d;h=xa(y[a+4>>2],y[a+8>>2]);y[d+8>>2]=h;Da(d,h);a:{l=y[d+4>>2];if(!l){oa(b,0);y[a+52>>2]=-1;break a}b:{f=y[h+16>>2];if((f|0)<0|!f){break b}j=y[c+12>>2]+(y[c+8>>2]<<3)|0;i=y[a+8>>2];n=y[a+4>>2];q=y[c>>2];r=y[c+4>>2];k=y[d>>2];y[d+20>>2]=0;y[d+12>>2]=0;y[d+16>>2]=0;g=d+12|0;y[d+24>>2]=g;jc(g,f);e=y[d+16>>2];g=f+e|0;while(1){if((e|0)!=(g|0)){w[e|0]=255;e=e+1|0;continue}break}w[d+28|0]=1;y[d+16>>2]=g;fd(d+24|0);m=y[d+12>>2];g=y[d+16>>2];y[d+24>>2]=j;j=0;s=(i|0)>0?i:0;e=0;g=g-m|0;t=e|m;while(1){if((j|0)!=(s|0)){i=k;e=f;if(l>>>0>>0){break b}while(1){if((e|0)>0){w[i|0]=255;e=e-1|0;i=i+1|0;continue}break}ec(q,r,d+24|0,k|u,f,t,g,n);if(f>>>0>g>>>0){break b}if(f){p(m,k,f)}j=j+1|0;l=l-f|0;k=f+k|0;continue}break}f=y[d+24>>2];g=d+12|0;ya(g);y[c+12>>2]=f&7;y[c+8>>2]=f>>>3;Da(g,h);e=y[d+12>>2];c=e+y[d+16>>2]|0;while(1)if((c|0)==(e|0)){y[a+64>>2]=0;y[a+52>>2]=4;c=y[h+8>>2];y[a+68>>2]=0;y[a+72>>2]=c;y[a+76>>2]=y[h+12>>2];y[d+8>>2]=0;oa(b,h);break a}else{w[e|0]=z[e|0]^-1;e=e+1|0;continue}}o()}la(d+8|0);Y=d+32|0}function ae(a,b,c,d,e,f,g,h,i,j){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;i=i|0;j=j|0;var k=0,l=0;k=Y-16|0;Y=k;l=i?i:c;a:{if((l|0)<=0){break a}j=j?j:d;if((j|0)<=0|(j|l)>>>0>65535){break a}i=na(88);y[i+40>>2]=0;y[i+32>>2]=-1;y[i+36>>2]=0;y[i+20>>2]=1;y[i+24>>2]=1;y[i+16>>2]=j;y[i+12>>2]=l;y[i+8>>2]=j;y[i+4>>2]=l;y[i+56>>2]=a;y[i+60>>2]=b;w[i+54|0]=(h|0)!=0;w[i+53|0]=(f|0)!=0;w[i+52|0]=(g|0)!=0;y[i+48>>2]=0;y[i+44>>2]=e;y[i>>2]=2e3;a=l+31>>>3&16380;y[i+28>>2]=a;od(i- -64|0,a);od(i+76|0,y[i+28>>2]);e=c+7>>>3|0;W(E(e,d)|0);a=0;while(1){b:{if((a|0)==(d|0)){break b}c:{j=y[i+32>>2];b=a+1|0;if((j|0)==(b|0)){j=y[i+40>>2];c=y[i+36>>2];break c}d:{if(!((j|0)>=0&(a|0)>=(j|0))){j=0;if(!(_[y[y[i>>2]+12>>2]](i)|0)){break b}c=0;break d}c=1}while(1){if(!c){y[i+32>>2]=j;c=1;continue}e:{if((a|0)>(j|0)){_[y[y[i>>2]+16>>2]](k+8|0,i);j=y[i+32>>2]+1|0;break e}_[y[y[i>>2]+16>>2]](k+8|0,i);j=y[k+12>>2];c=y[k+8>>2];y[i+36>>2]=c;y[i+40>>2]=j;y[i+32>>2]=y[i+32>>2]+1;break c}c=0;continue}}if(!j){break b}V(c|0,e|0,E(a,e)|0);a=b;continue}break}_[y[y[i>>2]+4>>2]](i)}Y=k+16|0}function Fd(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0;b=y[1508];c=a+7&-8;a=c+7|0;d=a>>>0<7?1:d;e=a&-8;a=e+b|0;a:{b:{if(!(a>>>0>>0?d+1|0:d)){if(a>>>0<=$()<<16>>>0){break b}if(R(a|0)|0){break b}}y[1516]=48;b=-1;break a}y[1508]=a}d=b;if((d|0)!=-1){a=d+c|0;y[a-4>>2]=16;f=a-16|0;y[f>>2]=16;b=y[1776];if(b){e=y[b+8>>2]}else{e=0}c:{d:{if((e|0)==(d|0)){e=y[d-4>>2]&-2;g=d-e|0;h=y[g-4>>2];y[b+8>>2]=a;b=h&-2;a=g-b|0;if(y[(a+y[a>>2]|0)-4>>2]&1){f=y[a+4>>2];g=y[a+8>>2];y[f+8>>2]=g;y[g+4>>2]=f;b=(b+(c+e|0)|0)-16|0;y[a>>2]=b;break c}a=d-16|0;break d}y[d>>2]=16;y[d+8>>2]=a;y[d+4>>2]=b;y[d+12>>2]=16;y[1776]=d;a=d+16|0}b=f-a|0;y[a>>2]=b}y[((b&-4)+a|0)-4>>2]=b|1;c=y[a>>2]-8|0;e:{if(c>>>0<=127){b=(c>>>3|0)-1|0;break e}e=H(c);b=((c>>>29-e^4)-(e<<2)|0)+110|0;if(c>>>0<=4095){break e}b=((c>>>30-e^2)-(e<<1)|0)+71|0;b=b>>>0>=63?63:b}c=b<<4;y[a+4>>2]=c+6080;c=c+6088|0;y[a+8>>2]=y[c>>2];y[c>>2]=a;y[y[a+8>>2]+4>>2]=a;c=y[1779];a=b&31;if((b&63)>>>0>=32){b=1<>>32-a}y[1778]=e|y[1778];y[1779]=b|c}return(d|0)!=-1}function Bb(a,b){var c=0,d=0,e=0;c=Y+-64|0;Y=c;d=y[a>>2];e=y[d-8>>2];d=y[d-4>>2];a:{if(y[d+4>>2]==y[b+4>>2]){a=e?0:a;break a}e=a+e|0;if((e|0)<=(a|0)){y[c+16>>2]=0;y[c+20>>2]=0;y[c+12>>2]=b;y[c+8>>2]=a;y[c+4>>2]=d;y[c+24>>2]=0;y[c+28>>2]=0;y[c+32>>2]=0;y[c+36>>2]=0;y[c+40>>2]=0;y[c+44>>2]=0;y[c+48>>2]=0;y[c+60>>2]=0;y[c+52>>2]=1;y[c+56>>2]=16777216;_[y[y[d>>2]+20>>2]](d,c+4|0,e,e,1,0);if(y[c+28>>2]){break a}}y[c+16>>2]=0;y[c+20>>2]=0;y[c+12>>2]=5336;y[c+8>>2]=a;y[c+4>>2]=b;y[c+24>>2]=0;y[c+28>>2]=0;y[c+32>>2]=0;y[c+36>>2]=0;y[c+40>>2]=0;y[c+44>>2]=0;y[c+48>>2]=0;y[c+52>>2]=0;a=0;w[c+55|0]=0;w[c+56|0]=0;w[c+57|0]=0;w[c+58|0]=0;y[c+60>>2]=0;w[c+59|0]=1;_[y[y[d>>2]+24>>2]](d,c+4|0,e,1,0);b:{switch(y[c+40>>2]){case 0:a=y[c+44>>2]==1?y[c+32>>2]==1?y[c+36>>2]==1?y[c+24>>2]:0:0:0;break a;case 1:break b;default:break a}}if(y[c+28>>2]!=1){if(y[c+44>>2]|y[c+32>>2]!=1|y[c+36>>2]!=1){break a}}a=y[c+20>>2]}Y=c- -64|0;return a}function ud(a,b){var c=0,d=0,e=0,f=0,g=0,h=0;d=Y-32|0;Y=d;a:{b:{c:{d:{e:{c=y[a+4>>2];if(c>>>0>>0){g=b-c|0;f=y[a+8>>2];e=f<<5;if(!(g>>>0>e>>>0|c>>>0>e-g>>>0)){y[a+4>>2]=b;e=c&31;b=y[a>>2]+(c>>>3&536870908)|0;break b}y[d+16>>2]=0;y[d+8>>2]=0;y[d+12>>2]=0;if((b|0)<0){break e}c=2147483647;if(e>>>0<=1073741822){e=f<<6;b=b+31&-32;c=b>>>0>>0?e:b;if((c|0)<0){break e}}e=(c-1>>>5|0)+1|0;b=na(e<<2);y[d+24>>2]=0;y[d+28>>2]=0;y[d+8>>2]=b;y[d+20>>2]=0;y[d+16>>2]=e;y[d+24>>2]=0;Ob(d+20|0);e=y[a+4>>2];y[d+12>>2]=e+g;if((e|0)<=0){break d}f=y[a>>2];h=e>>>5|0;c=h<<2;if(!(!h|!c)){p(b,f,c)}b=(h<<2)+b|0;e=e&31;if(!e){break d}f=y[c+f>>2];c=y[b>>2];y[b>>2]=(f^c)&-1>>>32-e^c;break c}y[a+4>>2]=b;break a}Ba();o()}e=0}c=y[a>>2];y[a>>2]=y[d+8>>2];y[d+8>>2]=c;c=y[a+4>>2];y[a+4>>2]=y[d+12>>2];y[d+12>>2]=c;c=y[a+8>>2];y[a+8>>2]=y[d+16>>2];y[d+16>>2]=c;Ob(d+8|0)}Xc(b,e,g);Pb(d+20|0,b,e,g)}Y=d+32|0}function cc(a,b,c,d,e){var f=0,g=0,h=0,i=0;a:{b:{if((c|0)<=(d|0)){break b}h=e-1|0;c:{f=(d|0)/8|0;g=f<<3;if((g|0)!=(d|0)){if(b>>>0<=f>>>0){break a}f=(z[a+f|0]^h)&255>>>d-g;if(f){break c}f=(d+7|0)/8|0}g=(c+7|0)/8|0;d:{if((c|0)<57){break d}i=g-8|0;if((i|0)<=(f|0)){break d}d=e?1718:1726;while(1){if((f|0)>=(i|0)){break d}if(b>>>0>>0|b-f>>>0<=7){break a}e=a+f|0;if((z[d|0]|z[d+1|0]<<8|(z[d+2|0]<<16|z[d+3|0]<<24))!=(z[e|0]|z[e+1|0]<<8|(z[e+2|0]<<16|z[e+3|0]<<24))|(z[e+4|0]|z[e+5|0]<<8|(z[e+6|0]<<16|z[e+7|0]<<24))!=(z[d+4|0]|z[d+5|0]<<8|(z[d+6|0]<<16|z[d+7|0]<<24))){break d}f=f+8|0;continue}}b=b>>>0>>0?f:b;d=(f|0)>(g|0)?f:g;e=h&255;while(1){if((d|0)==(f|0)){break b}if((b|0)==(f|0)){break a}g=z[a+f|0];if((g|0)!=(e|0)){a=z[((g^h)&255)+1734|0]+(f<<3)|0;return(a|0)>(c|0)?c:a}else{f=f+1|0;continue}}}c=g+z[f+1734|0]|0}return c}o()}function dc(a,b){var c=0,d=0,e=0,f=0,g=0;e=a+4|0;d=e+7&-8;c=y[a>>2];if(d+b>>>0<=(c+a|0)-4>>>0){f=y[a+4>>2];g=y[a+8>>2];y[f+8>>2]=g;y[g+4>>2]=f;if((d|0)!=(e|0)){d=d-e|0;f=a-(y[a-4>>2]&-2)|0;e=d+y[f>>2]|0;y[f>>2]=e;y[(f+(e&-4)|0)-4>>2]=e;a=a+d|0;c=c-d|0;y[a>>2]=c}a:{if(b+24>>>0<=c>>>0){e=a+b|0;c=(c-b|0)-8|0;y[e+8>>2]=c;g=e+8|0;y[(g+(c&-4)|0)-4>>2]=c|1;d=y[e+8>>2]-8|0;b:{if(d>>>0<=127){c=(d>>>3|0)-1|0;break b}f=H(d);c=((d>>>29-f^4)-(f<<2)|0)+110|0;if(d>>>0<=4095){break b}c=((d>>>30-f^2)-(f<<1)|0)+71|0;c=c>>>0>=63?63:c}d=c<<4;y[e+12>>2]=d+6080;d=d+6088|0;y[e+16>>2]=y[d>>2];y[d>>2]=g;y[y[e+16>>2]+4>>2]=g;d=y[1778];f=y[1779];e=c&31;if((c&63)>>>0>=32){c=1<>>32-e}y[1778]=g|d;y[1779]=c|f;c=b+8|0;y[a>>2]=c;b=(c&-4)+a|0;break a}b=a+c|0}y[b-4>>2]=c;a=a+4|0}else{a=0}return a}function ma(a){a=a|0;var b=0,c=0,d=0,e=0,f=0;if(a){b=a-4|0;f=y[b>>2];c=f;d=b;e=y[a-8>>2];a=e&-2;if((a|0)!=(e|0)){d=b-a|0;c=y[d+4>>2];e=y[d+8>>2];y[c+8>>2]=e;y[e+4>>2]=c;c=a+f|0}a=b+f|0;b=y[a>>2];if((b|0)!=y[(a+b|0)-4>>2]){f=y[a+4>>2];a=y[a+8>>2];y[f+8>>2]=a;y[a+4>>2]=f;c=b+c|0}y[d>>2]=c;y[((c&-4)+d|0)-4>>2]=c|1;b=y[d>>2]-8|0;a:{if(b>>>0<=127){a=(b>>>3|0)-1|0;break a}c=H(b);a=((b>>>29-c^4)-(c<<2)|0)+110|0;if(b>>>0<=4095){break a}a=((b>>>30-c^2)-(c<<1)|0)+71|0;a=a>>>0>=63?63:a}b=a<<4;y[d+4>>2]=b+6080;b=b+6088|0;y[d+8>>2]=y[b>>2];y[b>>2]=d;y[y[d+8>>2]+4>>2]=d;b=y[1778];c=y[1779];d=a&31;if((a&63)>>>0>=32){a=1<>>32-d}y[1778]=e|b;y[1779]=a|c}}function ob(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;d=Y-32|0;Y=d;g=(b<<3)+a|0;b=a;while(1){if((b|0)==(g|0)){a:{b=f+1|0;i=md(d+20|0,b);j=md(d+8|0,b);h=y[i>>2];b=a;while(1)if((b|0)==(g|0)){y[h>>2]=0;k=((f|0)>0?f:0)+1|0;l=y[j>>2];c=1;while(1){if((c|0)==(k|0)){c=k;break a}b=c<<2;e=b-4|0;y[d+4>>2]=y[e+l>>2];w[d|0]=1;if(z[Tb(Na(d,y[e+h>>2]))|0]!=1){break a}e=y[d+4>>2];y[b+l>>2]=e;b=a;while(1){if((b|0)!=(g|0)){if(y[b>>2]==(c|0)){y[b+4>>2]=e;e=e+1|0}b=b+8|0;continue}break}c=c+1|0;continue}}else{c=(y[b>>2]<<2)+h|0;y[c>>2]=y[c>>2]+1;b=b+8|0;continue}}}else{c=y[b>>2];f=(c|0)>(f|0)?c:f;b=b+8|0;continue}break}ya(j);ya(i);Y=d+32|0;return(c|0)>(f|0)}function za(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0,j=0;e=Y-16|0;Y=e;y[e+12>>2]=0;w[e+8|0]=1;h=-1;a:{b:while(1){if((wb(y[a>>2],e+4|0)|0)==-1){break a}d=Tb(e+8|0);if(z[d|0]!=1){break a}g=y[e+12>>2]|y[e+4>>2];f=(g|0)>=0;y[d>>2]=f;g=f?g:0;y[d+4>>2]=g;if(f){i=i+1|0;f=y[b+4>>2];d=0;while(1){if((d|0)==(f|0)){continue b}j=y[b+8>>2]+(d<<3)|0;if(y[j>>2]!=(i|0)|y[j+4>>2]!=(g|0)){d=d+1|0;continue}else{if((f-1|0)==(d|0)){h=1;if(w[b+1|0]&1){break a}}h=-1;f=y[a>>2];a=d<<2;if((Ia(f,y[a+y[b+20>>2]>>2],e+4|0)|0)==-1){break a}h=0;f=y[a+y[b+32>>2]>>2];a=y[e+4>>2];y[c>>2]=f+((y[b+4>>2]+(z[b+1|0]?-3:-2)|0)==(d|0)?0-a|0:a);break a}}}break}o()}Y=e+16|0;return h}function lb(a,b,c,d,e){var f=0,g=0,h=0;a:{d=(d|0)>0?d:0;f=(e|0)>=0?(c|0)<(e|0)?c:e:0;if((d|0)>=(f|0)){break a}b:{h=d>>>3|0;if(h>>>0>=b>>>0){break b}e=d&7;c=a+h|0;g=f-1|0;f=g>>>3|0;if((f|0)==(h|0)){a=g&7;while(1){if(a>>>0>>0){break a}w[c|0]=z[c|0]+(-1<<7-e);e=e+1|0;continue}}d=z[c|0];while(1)if((e|0)==8){w[c|0]=d;if(b>>>0<=f>>>0){break b}d=(g&7)+1|0;g=a+f|0;c=z[g|0];e=0;while(1)if((d|0)==(e|0)){w[g|0]=c;c=h+1|0;if(c>>>0>=f>>>0){break a}e=f+(h^-1)|0;if(e>>>0>b-c>>>0){break b}c=a+c|0;while(1){if((e|0)<=0){break a}w[c|0]=0;e=e-1|0;c=c+1|0;continue}}else{c=(-1<<7-e)+c|0;e=e+1|0;continue}}else{d=(-1<<7-e)+d|0;e=e+1|0;continue}}o()}}function ka(a,b){var c=0,d=0,e=0,f=0;c=y[b+4>>2];if(c>>>0<47){c=E(c,6);e=A[c+2080>>1];d=y[a+8>>2]-e|0;y[a+8>>2]=d;c=c+2080|0;f=y[a+4>>2];a:{if(d>>>0>f>>>16>>>0){if(d&32768){return z[b|0]}if(d>>>0>>0){d=z[b|0]^1;e=z[c+3|0];if(z[c+4|0]==1){w[b|0]=d&1}y[b+4>>2]=e;b=d&1;break a}y[b+4>>2]=z[c+2|0];b=z[b|0];break a}y[a+4>>2]=f-(d<<16);b:{if(d>>>0>>0){y[b+4>>2]=z[c+2|0];b=z[b|0];break b}d=z[b|0]^1;f=z[c+3|0];if(z[c+4|0]==1){w[b|0]=d&1}y[b+4>>2]=f;b=d&1}y[a+8>>2]=e}c=y[a+12>>2];while(1){if(!c){Dc(a);c=y[a+12>>2]}c=c-1|0;y[a+12>>2]=c;d=y[a+8>>2];y[a+8>>2]=d<<1;y[a+4>>2]=y[a+4>>2]<<1;if(!(d&16384)){continue}break}return b}o()}function xc(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0;a:{b:{c=y[a+4>>2];e=y[a>>2];g=c-e|0;h=g>>2;c:{if(h>>>0>>0){f=b-h|0;d=y[a+8>>2];if(f>>>0<=d-c>>2>>>0){b=(f<<2)+c|0;while(1){if((b|0)!=(c|0)){y[c>>2]=0;c=c+4|0;continue}break}y[a+4>>2]=b;return}if(b>>>0>=1073741824){break b}c=d-e|0;d=c>>1;d=c>>>0>=2147483644?1073741823:b>>>0>>0?d:b;if(d>>>0>=1073741824){break a}i=na(d<<2);b=i+g|0;f=b+(f<<2)|0;c=b;while(1){if((c|0)!=(f|0)){y[c>>2]=0;c=c+4|0;continue}break}b=b-(h<<2)|0;if(g){p(b,e,g)}y[a+8>>2]=(d<<2)+i;y[a+4>>2]=f;y[a>>2]=b;if(!e){break c}ma(e);return}if(b>>>0>=h>>>0){break c}y[a+4>>2]=(b<<2)+e}return}Ba();o()}jb();o()}function Td(a,b,c,d,e){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;a:{if(Sa(a,y[b+8>>2],e)){if(y[b+28>>2]==1|y[b+4>>2]!=(c|0)){break a}y[b+28>>2]=d;return}if(Sa(a,y[b>>2],e)){if(!(y[b+16>>2]!=(c|0)&y[b+20>>2]!=(c|0))){if((d|0)!=1){break a}y[b+32>>2]=1;return}y[b+32>>2]=d;b:{if(y[b+44>>2]==4){break b}x[b+52>>1]=0;a=y[a+8>>2];_[y[y[a>>2]+20>>2]](a,b,c,c,1,e);if(z[b+53|0]==1){y[b+44>>2]=3;if(!z[b+52|0]){break b}break a}y[b+44>>2]=4}y[b+20>>2]=c;y[b+40>>2]=y[b+40>>2]+1;if(y[b+36>>2]!=1|y[b+24>>2]!=2){break a}w[b+54|0]=1;return}a=y[a+8>>2];_[y[y[a>>2]+24>>2]](a,b,c,d,e)}}function kb(a,b){var c=0,d=0,e=0,f=0,g=0;a:{c=y[a+4>>2];d=y[a>>2];e=c-d>>3;if(e>>>0>>0){g=b-e|0;f=y[a+8>>2];if(g>>>0<=f-c>>3>>>0){b=(g<<3)+c|0;while(1){if((b|0)!=(c|0)){y[c+4>>2]=0;w[c|0]=0;c=c+8|0;continue}break}y[a+4>>2]=b;return}d=Bc(d,f,b);b=y[a+4>>2]-y[a>>2]|0;e=0;if(d){e=yb(d)}b=b+e|0;g=b+(g<<3)|0;c=b;while(1){if((c|0)!=(g|0)){y[c+4>>2]=0;w[c|0]=0;c=c+8|0;continue}break}f=b;b=y[a>>2];c=y[a+4>>2]-b|0;f=f-c|0;if(c){p(f,b,c)}y[a+4>>2]=g;y[a>>2]=f;y[a+8>>2]=(d<<3)+e;if(!b){break a}ma(b);return}if(b>>>0>=e>>>0){break a}y[a+4>>2]=d+(b<<3)}}function ad(a,b){var c=0,d=0,e=0,f=0,g=0,h=0;a:{e=y[a+4>>2];c=y[a>>2];f=e-c|0;d=f>>3;b:{if(d>>>0>>0){d=b-d|0;g=y[a+8>>2];if(d>>>0<=g-e>>3>>>0){ld(a,d);return}if(b>>>0>=536870912){break a}h=d<<3;e=f;c=g-c|0;d=c>>2;f=c>>>0>=2147483640?536870911:b>>>0>>0?d:b;g=yb(f);c=e+g|0;d=h+c|0;b=c;while(1){if((b|0)!=(d|0)){y[b>>2]=0;y[b+4>>2]=0;b=b+8|0;continue}break}e=c;b=y[a>>2];c=y[a+4>>2]-b|0;e=e-c|0;if(c){p(e,b,c)}y[a+4>>2]=d;y[a>>2]=e;y[a+8>>2]=g+(f<<3);if(!b){break b}ma(b);return}if(b>>>0>=d>>>0){break b}y[a+4>>2]=c+(b<<3)}return}Ba();o()}function Gb(a,b){var c=0,d=0,e=0,f=0,g=0,h=0;a:{e=y[a+4>>2];c=y[a>>2];f=e-c|0;d=f>>2;b:{if(d>>>0>>0){d=b-d|0;g=y[a+8>>2];if(d>>>0<=g-e>>2>>>0){kd(a,d);return}if(b>>>0>=1073741824){break a}h=d<<2;e=f;c=g-c|0;d=c>>1;f=c>>>0>=2147483644?1073741823:b>>>0>>0?d:b;g=Lb(f);c=e+g|0;d=h+c|0;b=c;while(1){if((b|0)!=(d|0)){y[b>>2]=0;b=b+4|0;continue}break}e=c;b=y[a>>2];c=y[a+4>>2]-b|0;e=e-c|0;if(c){p(e,b,c)}y[a+4>>2]=d;y[a>>2]=e;y[a+8>>2]=g+(f<<2);if(!b){break b}ma(b);return}if(b>>>0>=d>>>0){break b}y[a+4>>2]=c+(b<<2)}return}Ba();o()}function Rc(a,b,c,d,e,f,g){y[a+8>>2]=0;a:{b:{if(!(b&255)){c:{switch(c|0){case 1:y[a+4>>2]=e;y[a>>2]=d;y[a+8>>2]=f-1;return;case 3:y[a+4>>2]=e;break a;case 0:y[a>>2]=d;y[a+8>>2]=f-1;y[a+4>>2]=(e-g|0)+1;return;case 2:break c;default:break b}}y[a+4>>2]=(e-g|0)+1;break a}d:{switch(c|0){case 1:y[a+4>>2]=d;y[a>>2]=e;y[a+8>>2]=g-1;return;case 3:y[a+4>>2]=d;y[a+8>>2]=g-1;y[a>>2]=(e-f|0)+1;return;case 0:y[a>>2]=e;y[a+4>>2]=(d-g|0)+1;return;case 2:break d;default:break b}}y[a+4>>2]=(d-g|0)+1;y[a>>2]=(e-f|0)+1}return}y[a>>2]=(d-f|0)+1}function bc(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0;k=((d&536870911)<<3|c>>>29)&-8;a:{b:while(1){c:{if(b>>>0<=f>>>0){break c}g=-1;i=z[a+f|0];if((i|0)==255){break a}h=y[e>>2];if(h>>>0>=k>>>0){break a}g=h>>>3|0;if(g>>>0>=d>>>0){break c}g=z[c+g|0];y[e>>2]=h+1;j=g>>>((h^-1)&7)&1|j<<1;f=f+1|0;h=f+E(i,3)|0;while(1){if((f|0)>=(h|0)){continue b}if(b>>>0<=f>>>0){break c}if(z[a+f|0]==(j|0)){c=f+1|0;if(c>>>0>=b>>>0){break c}d=b;b=f+2|0;if(d>>>0<=b>>>0){break c}g=z[a+c|0]|z[a+b|0]<<8;break a}else{f=f+3|0;continue}}}break}o()}return g}function Ia(a,b,c){var d=0,e=0,f=0,g=0;e=-1;g=y[a>>2];a:{f=y[a+8>>2];d=y[a+4>>2];if(f>>>0>=d>>>0){break a}b:{if((d|0)==536870912|d>>>0>536870912){break b}f=y[a+12>>2]+(f<<3)|0;if(f>>>0>(((d&536870911)<<3|g>>>29)&-8)>>>0){break a}y[c>>2]=0;g=y[a>>2];d=y[a+4>>2];if((d|0)==536870912|d>>>0>536870912){break b}d=((d&536870911)<<3|g>>>29)&-8;e=d>>>0>>0?d-f|0:b;while(1){if(!e){e=0;break a}b=y[a+8>>2];if(b>>>0>=B[a+4>>2]){break b}y[c>>2]=z[b+y[a>>2]|0]>>>7-y[a+12>>2]&1|y[c>>2]<<1;_b(a);e=e-1|0;continue}}o()}return e}function Aa(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0;f=Y-16|0;Y=f;i=ka(b,y[a>>2]+8|0);d=i|2;while(1){a:{if((e|0)==5){e=5;break a}g=ka(b,y[a>>2]+(d<<3)|0);d=g|d<<1;if(!g){break a}e=e+1|0;continue}break}g=0;e=e<<3;h=y[e+2364>>2];h=(h|0)>0?h:0;k=e+2364|0;e=0;while(1){if((e|0)!=(h|0)){j=ka(b,y[a>>2]+(d<<3)|0);d=j|d<<1;d=(d|0)>255?d&255|256:d;g=g<<1|j;e=e+1|0;continue}break}w[f+8|0]=1;y[f+12>>2]=y[k+4>>2];d=0;e=0;if(z[Na(f+8|0,g)|0]){a=y[f+12>>2];b=a>>31;d=i?b-(a^b)|0:a;e=!i|(d|0)!=0}y[c>>2]=d;Y=f+16|0;return e}function Sb(a,b){var c=0,d=0,e=0,f=0,g=0;a:{if((a|0)!=(b|0)){f=y[b+4>>2];d=y[b>>2];b=f-d|0;c=y[a>>2];if(b>>>0<=y[a+8>>2]-c>>>0){e=y[a+4>>2];g=e-c|0;if(b>>>0>g>>>0){if((c|0)!=(e|0)){if(g){p(c,d,g)}e=y[a+4>>2]}b=d+g|0;d=f-b|0;if(!(!d|(b|0)==(f|0))){p(e,b,d)}y[a+4>>2]=d+e;return a}if(!(!b|(d|0)==(f|0))){p(c,d,b)}y[a+4>>2]=b+c;return a}jd(a);e=Bc(y[a>>2],y[a+8>>2],b>>3);if(e>>>0>=536870912){break a}c=yb(e);y[a+4>>2]=c;y[a>>2]=c;y[a+8>>2]=c+(e<<3);if(!(!b|(d|0)==(f|0))){p(c,d,b)}y[a+4>>2]=b+c}return a}Ba();o()}function me(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0;d=Y-16|0;Y=d;f=y[y[b>>2]>>2];if(!y[a+48>>2]){Da(d+8|0,f);c=y[d+12>>2];y[a+44>>2]=y[d+8>>2];y[a+48>>2]=c}c=y[a+4>>2];if(c){c=c-1|0;g=c>>>3|0;h=(c&7)+1|0;c=y[a+24>>2];while(1){a:{b:{if(B[a+8>>2]<=c>>>0){c=4}else{if(!ic(a,b,2560,g,h)){c=-1;break b}Kb(a,f);e=y[b+16>>2];c=y[a+24>>2];if(!e|(c>>>0)%50){break a}e=_[y[y[e>>2]+8>>2]](e)|0;c=y[a+24>>2];if(!e){break a}y[a+24>>2]=c+1;c=3}y[a+52>>2]=c}Y=d+16|0;return c|0}c=c+1|0;y[a+24>>2]=c;continue}}o()}function qe(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0;d=Y-16|0;Y=d;e=y[y[b>>2]>>2];if(!y[a+48>>2]){Da(d+8|0,e);c=y[d+12>>2];y[a+44>>2]=y[d+8>>2];y[a+48>>2]=c}c=y[a+4>>2];if(c){f=y[a+8>>2]&2147483647;c=c-1|0;g=c>>>3|0;h=(c&7)+1|0;c=y[a+24>>2];while(1){a:{b:{if(c>>>0>=f>>>0){c=4}else{if(!ic(a,b,2496,g,h)){c=-1;break b}Kb(a,e);c=y[b+16>>2];if(!c){break a}if(!(_[y[y[c>>2]+8>>2]](c)|0)){break a}y[a+24>>2]=y[a+24>>2]+1;c=3}y[a+52>>2]=c}Y=d+16|0;return c|0}c=y[a+24>>2]+1|0;y[a+24>>2]=c;continue}}o()}function zc(a,b,c,d,e){var f=0,g=0;y[a>>2]=0;f=na(24);y[f+8>>2]=0;y[f+12>>2]=0;g=b;b=(c|0)==268435456|c>>>0<268435456;y[f>>2]=b?g:0;y[f+4>>2]=b?c:0;y[f+16>>2]=0;y[f+20>>2]=0;y[a+8>>2]=0;y[a+12>>2]=0;y[a+4>>2]=f;y[a+16>>2]=0;y[a+20>>2]=0;y[a+24>>2]=0;y[a+28>>2]=0;y[a+32>>2]=0;c=na(64);y[a+36>>2]=c;b=c- -64|0;y[a+44>>2]=b;f=0;while(1){if((f|0)!=64){y[c+f>>2]=0;f=f+4|0;continue}break}y[a+52>>2]=10;w[a+49|0]=0;w[a+50|0]=0;w[a+48|0]=e;y[a+40>>2]=b;t(a+56|0,0,49);w[a+112|0]=0;y[a+108>>2]=d;return a}function Wc(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0;a:{c=y[a+4>>2];d=y[a+8>>2];b:{if(c>>>0>>0){e=y[b>>2];y[b>>2]=0;y[c>>2]=e;b=c+4|0;break b}f=y[a>>2];e=c-f|0;h=e>>2;c=h+1|0;if(c>>>0>=1073741824){break a}d=d-f|0;g=d>>1;g=d>>>0>=2147483644?1073741823:c>>>0>>0?g:c;c:{if(!g){c=e;d=0;break c}d=Lb(g);f=y[a>>2];c=y[a+4>>2]-f|0;h=c>>2}i=y[b>>2];y[b>>2]=0;b=d+e|0;y[b>>2]=i;e=b-(h<<2)|0;if(c){p(e,f,c)}y[a+8>>2]=(g<<2)+d;b=b+4|0;y[a+4>>2]=b;y[a>>2]=e;if(!f){break b}ma(f)}y[a+4>>2]=b;return}Ba();o()}function zb(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){ac(c+80|0);ua(c+76|0);ua(c+72|0);Ja(c+60|0);b=y[c+36>>2];if(b){a=y[c+40>>2];while(1){if((a|0)!=(b|0)){a=pa(a-4|0);continue}break}y[c+40>>2]=b;ma(y[c+36>>2])}la(c+32|0);b=y[c+20>>2];if(b){a=y[c+24>>2];while(1){if((a|0)!=(b|0)){a=$b(a-4|0);continue}break}y[c+24>>2]=b;ma(y[c+20>>2])}b=y[c+8>>2];if(b){a=y[c+12>>2];while(1){if((a|0)!=(b|0)){a=ac(a-4|0);continue}break}y[c+12>>2]=b;ma(y[c+8>>2])}a=y[c+4>>2];y[c+4>>2]=0;if(a){ma(a)}ma(Ab(c))}}function oe(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0;d=Y-16|0;Y=d;e=y[y[b>>2]>>2];if(!y[a+48>>2]){Da(d+8|0,e);c=y[d+12>>2];y[a+44>>2]=y[d+8>>2];y[a+48>>2]=c}c=y[a+4>>2];if(c){c=c-1|0;f=c>>>3|0;g=(c&7)+1|0;c=y[a+24>>2];while(1){a:{b:{if(B[a+8>>2]<=c>>>0){c=4}else{if(!ic(a,b,2528,f,g)){c=-1;break b}Kb(a,e);c=y[b+16>>2];if(!c){break a}if(!(_[y[y[c>>2]+8>>2]](c)|0)){break a}y[a+24>>2]=y[a+24>>2]+1;c=3}y[a+52>>2]=c}Y=d+16|0;return c|0}c=y[a+24>>2]+1|0;y[a+24>>2]=c;continue}}o()}function Qd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0;if(y[b+4>>2]==5604){b=y[a+12>>2];a:{if(b){a=5592;if(Bb(b,5480)){break a}}a=5600}y[c>>2]=a;return 1}e=y[a+8>>2];b:{c:{if(!(e&24)){d=Bb(b,5432);if(!d){break c}c=1;if(!Sa(a,b,(y[d+8>>2]&24)!=0)){break c}break b}c=1;if(Sa(a,b,1)){break b}}c=0;b=Bb(b,5532);if(!b){break b}d=y[b+8>>2];if(d&(e^-1)&7|(d^-1)&e&96|y[y[a+12>>2]+4>>2]!=y[y[b+12>>2]+4>>2]){break b}c=y[y[a+16>>2]+4>>2]==y[y[b+16>>2]+4>>2]}return c|0}function Wb(a,b){var c=0,d=0,e=0,f=0,g=0;e=Y-16|0;Y=e;a:{b:{f=y[a+4>>2];if(f-1>>>0<=65534){g=y[a+8>>2];if(g-1>>>0<65535){break b}}y[a+52>>2]=4;a=4;break a}y[a+52>>2]=2;c=y[b>>2];d=y[c>>2];if(!d){d=xa(f,g);y[e+12>>2]=0;oa(c,d);la(e+12|0);d=y[c>>2]}if(!qa(d)){oa(c,0);y[a+52>>2]=-1;a=-1;break a}Qa(y[c>>2],0);y[a+60>>2]=0;x[a+56>>1]=1;y[a+24>>2]=0;y[a+28>>2]=0;y[a+32>>2]=0;y[a+36>>2]=0;y[a+40>>2]=0;y[a+44>>2]=0;y[a+48>>2]=0;a=hd(a,b)}Y=e+16|0;return a}function Ba(){var a=0,b=0,c=0,d=0;c=Fb(8);y[c>>2]=5828;a=4883;b=0;a:{if(!z[4883]){break a}b:{c:{while(1){a=a+1|0;if(!(a&3)){break c}if(z[a|0]){continue}break}break b}while(1){b=a;a=a+4|0;d=y[b>>2];if(((16843008-d|d)&-2139062144)==-2139062144){continue}break}while(1){a=b;b=a+1|0;if(z[a|0]){continue}break}}b=a-4883|0}a=na(b+13|0);y[a+8>>2]=0;y[a+4>>2]=b;y[a>>2]=b;a=a+12|0;b=b+1|0;if(b){p(a,4883,b)}y[c>>2]=5876;y[c+4>>2]=a;P(c|0,5888,1);o()}function Gc(a,b){var c=0,d=0,e=0,f=0,g=0;a:{if((a|0)!=(b|0)){f=y[b+4>>2];e=y[b>>2];d=f-e|0;c=y[a+8>>2];b=y[a>>2];if(d>>>0<=c-b>>>0){g=y[a+4>>2];c=g-b|0;if(c>>>0>>0){if(!(!c|(b|0)==(g|0))){p(b,e,c)}Ec(a,c+e|0,f);return}if(!(!d|(e|0)==(f|0))){p(b,e,d)}y[a+4>>2]=b+d;return}if(b){y[a+4>>2]=b;ma(b);y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;c=0}if((d|0)<0){break a}b=c<<1;jc(a,c>>>0>=1073741823?2147483647:b>>>0>d>>>0?b:d);Ec(a,e,f)}return}Ba();o()}function Dc(a){var b=0,c=0,d=0,e=0,f=0;e=255;b=y[a+16>>2];a:{if(z[a+1|0]==255){f=8;d=y[b+8>>2];c=d+1|0;if(c>>>0>=B[b+4>>2]){break a}c=z[c+y[b>>2]|0];if(c>>>0>143){break a}Cc(b);w[a+1|0]=c;y[a+4>>2]=(y[a+4>>2]-(c<<9)|0)+65024;b=y[a+16>>2];d=y[b+8>>2];f=7;break a}Cc(b);f=8;b=y[a+16>>2];d=y[b+8>>2];c=y[b>>2];e=B[b+4>>2]>d>>>0?z[c+d|0]:e;w[a+1|0]=e;y[a+4>>2]=(y[a+4>>2]-(e<<8)|0)+65280}y[a+12>>2]=f;if(B[b+4>>2]<=d>>>0){w[a|0]=1}}function Ib(a,b,c,d){var e=0,f=0,g=0,h=0,i=0;f=b-1|0;g=y[d>>2];e=y[d+4>>2];a:{if((ia(a,f,g,e)|0)!=(c|0)){break a}if((ia(a,b,g,e)|0)!=(c|0)){break a}h=b+1|0;if((ia(a,h,g,e)|0)!=(c|0)){break a}g=y[d+8>>2];e=y[d+12>>2];if((ia(a,f,g,e)|0)!=(c|0)){break a}if((ia(a,h,g,e)|0)!=(c|0)){break a}e=f;f=y[d+16>>2];d=y[d+20>>2];if((ia(a,e,f,d)|0)!=(c|0)){break a}if((ia(a,b,f,d)|0)!=(c|0)){break a}i=(ia(a,h,f,d)|0)==(c|0)}return i}function ja(a,b,c){var d=0,e=0;d=Y-16|0;Y=d;a:{b:{c:{if(!(!(!qa(b)|(c|0)<0)&y[b+12>>2]>(c|0))){w[d+8|0]=0;break c}e=y[b+16>>2];if((e|0)<0){break b}c=se(e,0,c);if(Z){break b}y[d+8>>2]=c;e=1}w[d+12|0]=e;break a}o()}d:{e:{if(!z[d+12|0]){y[a>>2]=0;y[a+4>>2]=0;break e}Da(d,b);e=y[d+4>>2];c=y[d+8>>2];if(e>>>0>>0){break d}b=y[b+16>>2];if(b>>>0>e-c>>>0){break d}e=y[d>>2];y[a+4>>2]=b;y[a>>2]=c+e}Y=d+16|0;return}o()}function nd(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0;d=a+8|0;t(d,0,36);b=E(b,12);c=y[b+3072>>2];y[a+4>>2]=c;w[a+1|0]=z[b+3064|0];ad(d,c);Gb(a+20|0,c);Gb(a+32|0,c);b=y[b+3068>>2];d=b+(c<<3)|0;g=y[a+32>>2];h=y[a+20>>2];c=y[a+8>>2];while(1){if((b|0)!=(d|0)){y[c+(e<<3)>>2]=z[b|0];f=e<<2;y[f+h>>2]=z[b+1|0];y[g+f>>2]=y[b+4>>2];b=b+8|0;e=e+1|0;continue}break}i=a,j=ob(c,y[a+12>>2]-c>>3),w[i|0]=j;return a}function bd(a,b,c,d,e,f,g,h,i,j){var k=0,l=0;La(y[b+8>>2],d,i,j,e);i=y[a+12>>2];j=y[c>>2];b=ia(y[b+8>>2],d+2|0,g,h);y[c+4>>2]=e;y[c>>2]=b|j<<1&2;e=y[c+8>>2];a=y[a+20>>2];b=(d-i|0)+2|0;k=c,l=ia(y[a+8>>2],b,y[f>>2],y[f+4>>2])|e<<1&2,y[k+8>>2]=l;d=y[c+12>>2];k=c,l=ia(y[a+8>>2],b,y[f+8>>2],y[f+12>>2])|d<<1&6,y[k+12>>2]=l;d=y[c+16>>2];k=c,l=ia(y[a+8>>2],b,y[f+16>>2],y[f+20>>2])|d<<1&6,y[k+16>>2]=l}function Fc(a,b,c,d,e,f){var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;l=((b&536870911)<<3|a>>>29)&-8;h=1;while(1){a:{if(B[c>>2]>=l>>>0){break a}i=h&1;n=i?325:326;g=i?1056:1392;j=0;while(1){k=bc(g,n,a,b,c);if((k|0)<0){while(1){if(B[c>>2]>=l>>>0){break a}if(!Ka(a,b,c)){continue}break}break a}j=j+k|0;if(k>>>0>63){continue}break}g=j+m|0;if(!i){lb(d,e,f,m,g)}h=h^1;m=g;if((f|0)>(g|0)){continue}}break}}function Pd(a,b,c,d,e){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;a:{if(Sa(a,y[b+8>>2],e)){if(y[b+28>>2]==1|y[b+4>>2]!=(c|0)){break a}y[b+28>>2]=d;return}if(!Sa(a,y[b>>2],e)){break a}if(!(y[b+16>>2]!=(c|0)&y[b+20>>2]!=(c|0))){if((d|0)!=1){break a}y[b+32>>2]=1;return}y[b+20>>2]=c;y[b+32>>2]=d;y[b+40>>2]=y[b+40>>2]+1;if(!(y[b+36>>2]!=1|y[b+24>>2]!=2)){w[b+54|0]=1}y[b+44>>2]=4}}function Vd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0;d=Y+-64|0;Y=d;a:{e=1;b:{if(y[a+4>>2]==y[b+4>>2]){break b}b=Bb(b,5384);e=0;if(!b){break b}e=y[c>>2];if(!e){break a}t(d+8|0,0,56);w[d+59|0]=1;y[d+16>>2]=-1;y[d+12>>2]=a;y[d+4>>2]=b;y[d+52>>2]=1;_[y[y[b>>2]+28>>2]](b,d+4|0,e,1);a=y[d+28>>2];if((a|0)==1){y[c>>2]=y[d+20>>2]}e=(a|0)==1}Y=d- -64|0;return e|0}Qc();o()}function Pc(a,b,c,d){w[a+53|0]=1;a:{if(y[a+4>>2]!=(c|0)){break a}w[a+52|0]=1;c=y[a+16>>2];b:{if(!c){y[a+36>>2]=1;y[a+24>>2]=d;y[a+16>>2]=b;if((d|0)!=1){break a}if(y[a+48>>2]==1){break b}break a}if((b|0)==(c|0)){c=y[a+24>>2];if((c|0)==2){y[a+24>>2]=d;c=d}if(y[a+48>>2]!=1){break a}if((c|0)==1){break b}break a}y[a+36>>2]=y[a+36>>2]+1}w[a+54|0]=1}}function Cd(a,b){var c=0,d=0,e=0;d=a,e=Fa(),y[d>>2]=e;d=a,e=Fa(),y[d+4>>2]=e;d=a,e=Fa(),y[d+8>>2]=e;d=a,e=Fa(),y[d+12>>2]=e;d=a,e=Fa(),y[d+16>>2]=e;d=a,e=Fa(),y[d+20>>2]=e;d=a,e=Fa(),y[d+24>>2]=e;d=a,e=Fa(),y[d+28>>2]=e;d=a,e=Fa(),y[d+32>>2]=e;c=na(16);w[c+12|0]=b;y[c+8>>2]=0;y[c>>2]=0;y[c+4>>2]=0;kb(c,1<>2]=c;return a}function kc(a){var b=0,c=0,d=0,e=0;b=Y-16|0;Y=b;c=na(20);w[c+4|0]=0;y[c>>2]=0;y[c+8>>2]=y[a+8>>2];d=y[a+12>>2];y[c+12>>2]=d;e=y[a+16>>2];y[c+16>>2]=e;Da(b+8|0,a);a:{b:{if(!y[b+12>>2]){break b}nb(c,td(e,d));Da(b,c);a=y[b+8>>2];e=0;d=y[b+12>>2];if(d>>>0>B[b+4>>2]){break a}if(!d|!(d|e)){break b}p(y[b>>2],a,d)}Y=b+16|0;return c}o()}function sd(a,b,c,d,e){var f=0,g=0,h=0,i=0;a:{if(!d){break a}if(e&1){f=b;g=d;if(c){e=32-c|0;f=d>>>0>>0?d:e;y[b>>2]=y[b>>2]|-1<>>e-f;g=d-f|0;f=b+4|0}i=g>>>5|0;e=i;h=f;while(1){if(e){y[h>>2]=-1;e=e-1|0;h=h+4|0;continue}break}e=g&31;if(!e){break a}f=(i<<2)+f|0;y[f>>2]=y[f>>2]|-1>>>32-e;break a}Xc(b,c,d)}Pb(a,b,c,d)}function ed(a,b){var c=0,d=0,e=0;c=Y-32|0;Y=c;a:{d=y[a+40>>2];if(d){e=y[a+48>>2];y[c+24>>2]=y[a+44>>2];y[c+28>>2]=e;b=y[b+16>>2];if(b>>>0>d>>>0){break a}a=y[a+36>>2];y[c+20>>2]=b;y[c+16>>2]=a;a=y[c+28>>2];y[c+8>>2]=y[c+24>>2];y[c+12>>2]=a;a=y[c+20>>2];y[c>>2]=y[c+16>>2];y[c+4>>2]=a;Pa(c+8|0,c)}Y=c+32|0;return}o()}function ra(a,b){var c=0,d=0,e=0,f=0,g=0;a:{d=y[a+8>>2];f=d+3|0;c=y[a+4>>2];if(f>>>0>>0){if(c>>>0<=d>>>0){break a}g=d+1|0;if(g>>>0>=c>>>0){break a}e=c;c=d+2|0;if(e>>>0<=c>>>0){break a}e=b;b=y[a>>2];y[e>>2]=z[b+g|0]<<16|z[b+d|0]<<24|z[b+c|0]<<8|z[b+f|0];y[a+8>>2]=y[a+8>>2]+4;a=0}else{a=-1}return a}o()}function dd(a,b,c){var d=0;d=Y-32|0;Y=d;ja(d+24|0,y[b+20>>2],(y[b+16>>2]^-1)+c|0);y[a>>2]=y[d+24>>2];y[a+4>>2]=y[d+28>>2];ja(d+16|0,y[b+20>>2],c-y[b+16>>2]|0);y[a+8>>2]=y[d+16>>2];y[a+12>>2]=y[d+20>>2];ja(d+8|0,y[b+20>>2],(c-y[b+16>>2]|0)+1|0);y[a+16>>2]=y[d+8>>2];y[a+20>>2]=y[d+12>>2];Y=d+32|0}function xa(a,b){var c=0,d=0;c=na(20);y[c+16>>2]=0;y[c+8>>2]=0;y[c+12>>2]=0;w[c+4|0]=0;y[c>>2]=0;a:{if(a-2147483617>>>0<2147483680|(b|0)<=0){break a}d=a+31&2147483616;if(2147483616/(d>>>0)>>>0>>0){break a}y[c+12>>2]=b;y[c+8>>2]=a;a=d>>>3|0;y[c+16>>2]=a;nb(c,td(a,b))}return c}function Sa(a,b,c){var d=0;if(!c){return y[a+4>>2]==y[b+4>>2]}if((a|0)==(b|0)){return 1}c=y[a+4>>2];a=z[c|0];b=y[b+4>>2];d=z[b|0];a:{if(!a|(d|0)!=(a|0)){break a}while(1){d=z[b+1|0];a=z[c+1|0];if(!a){break a}b=b+1|0;c=c+1|0;if((a|0)==(d|0)){continue}break}}return(a|0)==(d|0)}function Pa(a,b){var c=0,d=0,e=0;a:{c=y[a+4>>2];b:{if(!c){break b}if(!y[b+4>>2]){a=y[a>>2];while(1){if((c|0)<=0){break b}w[a|0]=0;c=c-1|0;a=a+1|0;continue}}e=y[b>>2];d=y[b+4>>2];c=y[a>>2];b=0;if(1&B[a+4>>2]>>0){break a}if(!(b|d)|!d){break b}p(c,e,d)}return}o()}function Ad(a){var b=0,c=0,d=0,e=0,f=0,g=0;d=Y-16|0;Y=d;e=lc();f=e+24|0;g=y[a+28>>2];c=y[a+24>>2];while(1){if((c|0)!=(g|0)){b=y[c>>2];if(b){b=kc(b)}else{b=0}y[d+12>>2]=b;b=d+12|0;Wc(f,b);c=c+4|0;la(b);continue}break}c=Sb(e,a);Sb(c+12|0,a+12|0);Y=d+16|0;return c}function Jb(a,b,c){var d=0;d=Y-32|0;Y=d;ja(d+24|0,y[b+20>>2],c-1|0);y[a>>2]=y[d+24>>2];y[a+4>>2]=y[d+28>>2];ja(d+16|0,y[b+20>>2],c);y[a+8>>2]=y[d+16>>2];y[a+12>>2]=y[d+20>>2];ja(d+8|0,y[b+20>>2],c+1|0);y[a+16>>2]=y[d+8>>2];y[a+20>>2]=y[d+12>>2];Y=d+32|0}function Oc(a,b,c){var d=0;d=y[a+36>>2];if(!d){y[a+24>>2]=c;y[a+16>>2]=b;y[a+36>>2]=1;y[a+20>>2]=y[a+56>>2];return}a:{if(!(y[a+20>>2]!=y[a+56>>2]|y[a+16>>2]!=(b|0))){if(y[a+24>>2]!=2){break a}y[a+24>>2]=c;return}w[a+54|0]=1;y[a+24>>2]=2;y[a+36>>2]=d+1}}function Ra(a){var b=0,c=0,d=0,e=0;b=na(20);y[b+16>>2]=a;w[b|0]=0;c=255;d=y[a>>2];e=y[a+4>>2];a=y[a+8>>2];if(e>>>0>a>>>0){c=z[a+d|0]}w[b+1|0]=c;y[b+4>>2]=((c^-1)&255)<<16;Dc(b);y[b+8>>2]=32768;y[b+4>>2]=y[b+4>>2]<<7;y[b+12>>2]=y[b+12>>2]-7;return b}function Xc(a,b,c){var d=0,e=0;if(b){d=32-b|0;e=c>>>0>>0?c:d;y[a>>2]=y[a>>2]&(-1<>>d-e^-1);c=c-e|0;a=a+4|0}e=c>>>5|0;b=e;d=a;while(1){if(b){y[d>>2]=0;b=b-1|0;d=d+4|0;continue}break}b=c&31;if(b){a=(e<<2)+a|0;y[a>>2]=y[a>>2]&(-1>>>32-b^-1)}}function tb(a,b){var c=0;c=1;a:{if(ra(y[a+4>>2],b)){break a}if(ra(y[a+4>>2],b+4|0)){break a}if(ra(y[a+4>>2],b+8|0)){break a}if(ra(y[a+4>>2],b+12|0)){break a}if(va(y[a+4>>2],b+16|0)|(y[b>>2]>4096|y[b+4>>2]>4096)&z[a+112|0]==1){break a}c=0}return c}function zd(a,b,c){var d=0,e=0;d=na(32);e=y[b+12>>2];y[d+16>>2]=y[b+8>>2];y[d+20>>2]=e;e=y[b+4>>2];y[d+8>>2]=y[b>>2];y[d+12>>2]=e;b=y[c>>2];y[c>>2]=0;y[d>>2]=a;y[d+24>>2]=b;b=y[a+4>>2];y[d+4>>2]=b;y[b>>2]=d;y[a+4>>2]=d;y[a+8>>2]=y[a+8>>2]+1}function Ga(a,b,c){var d=0,e=0,f=0,g=0;d=y[b+8>>2];g=(d|0)>0?d:0;d=0;while(1){a:{if((d|0)==(g|0)){e=0;break a}e=Ma(a,y[y[b+12>>2]+(d<<2)>>2]);if(!(!e|(z[e+4|0]&63)!=53)){if((c|0)==(f|0)){break a}f=f+1|0}d=d+1|0;continue}break}return e}function Ub(a,b){var c=0;y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;a:{if(b){if(b>>>0>=1073741824){break a}c=b<<2;b=na(c);y[a>>2]=b;c=b+c|0;y[a+8>>2]=c;while(1){if((b|0)!=(c|0)){y[b>>2]=0;b=b+4|0;continue}break}y[a+4>>2]=c}return a}Ba();o()}function gb(a,b){var c=0;y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;a:{if(b){if(b>>>0>=1073741824){break a}c=Lb(b);y[a>>2]=c;b=(b<<2)+c|0;y[a+8>>2]=b;while(1){if((b|0)!=(c|0)){y[c>>2]=0;c=c+4|0;continue}break}y[a+4>>2]=b}return a}Ba();o()}function od(a,b){var c=0,d=0;c=Y-16|0;Y=c;y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;y[c+8>>2]=a;if(b){jc(a,b);d=b;b=y[a+4>>2];d=d+b|0;while(1){if((b|0)!=(d|0)){w[b|0]=0;b=b+1|0;continue}break}y[a+4>>2]=d}w[c+12|0]=1;fd(c+8|0);Y=c+16|0}function Kc(a,b){var c=0,d=0,e=0;c=Y-16|0;Y=c;d=z[a+4|0];if((d|0)==255){a=Fb(4);y[a>>2]=5288;P(a|0,5300,2);o()}y[c+4>>2]=c+3;e=y[b+4>>2];y[c+8>>2]=y[b>>2];y[c+12>>2]=e;a=_[y[(c+8|0)+(d<<2)>>2]](c+4|0,a)|0;Y=c+16|0;return a}function qb(){var a=0;a=na(80);x[a+56>>1]=0;y[a+12>>2]=0;y[a+24>>2]=0;y[a+28>>2]=0;y[a+32>>2]=0;y[a+36>>2]=0;y[a+40>>2]=0;y[a+44>>2]=0;y[a+48>>2]=0;y[a+60>>2]=0;y[a+64>>2]=0;y[a+68>>2]=0;y[a+72>>2]=0;y[a+76>>2]=0;return a}function Yb(){var a=0;a=na(96);y[a+44>>2]=0;y[a+48>>2]=0;y[a+36>>2]=0;y[a+40>>2]=0;y[a+28>>2]=0;y[a+32>>2]=0;y[a+60>>2]=0;y[a+64>>2]=0;y[a+68>>2]=0;y[a+72>>2]=0;y[a+76>>2]=0;y[a+80>>2]=0;y[a+84>>2]=0;y[a+88>>2]=0;return a}function La(a,b,c,d,e){a:{if(!(!d|(b|0)<0|(a|0)<=(b|0))){a=b>>>3|0;b=1<<((b^-1)&7);if(e){if(a>>>0>=d>>>0){break a}a=a+c|0;w[a|0]=b|z[a|0];return}if(a>>>0>=d>>>0){break a}a=a+c|0;w[a|0]=z[a|0]&(b^-1)}return}o()}function id(a){var b=0,c=0,d=0;y[a+56>>2]=2;c=1;b=4;d=y[a+52>>2];a:{b:{if((d|0)==5){break b}if((d|0)>=3){y[a+52>>2]=5;break b}b=yc(a);c=!b;if(y[a+56>>2]==3){break a}y[a+52>>2]=5;b=b?-1:4}y[a+56>>2]=b}return c}function se(a,b,c){var d=0,e=0,f=0,g=0,h=0;e=c>>>16|0;d=a>>>16|0;h=E(e,d);f=c&65535;a=a&65535;g=E(f,a);d=(g>>>16|0)+E(d,f)|0;a=(d&65535)+E(a,e)|0;Z=h+E(b,c)+(d>>>16)+(a>>>16)|0;return g&65535|a<<16}function Ma(a,b){var c=0,d=0;c=y[a>>2];a:{if(c){c=Ma(c,b);if(c){break a}}d=y[a+12>>2];a=y[a+8>>2];while(1){if((a|0)==(d|0)){return 0}c=y[a>>2];a=a+4|0;if(y[c>>2]!=(b|0)){continue}break}}return c}function xd(a){var b=0;a:{if(z[a+16|0]!=3|z[a+17|0]!=255|(z[a+18|0]!=253|z[a+19|0]!=255)){break a}if(z[a+20|0]!=2|z[a+21|0]!=254|(z[a+22|0]!=254|z[a+23|0]!=254)){break a}b=z[a+2|0]^1}return b&1}function Wa(a,b){var c=0,d=0,e=0;a:{c=y[a+8>>2];e=c+1|0;d=y[a+4>>2];if(e>>>0>>0){if(c>>>0>=d>>>0){break a}d=b;b=y[a>>2];x[d>>1]=z[b+c|0]<<8|z[b+e|0];y[a+8>>2]=c+2;a=0}else{a=-1}return a}o()}function Kb(a,b){var c=0;c=y[a+40>>2];y[a+28>>2]=y[a+36>>2];y[a+32>>2]=c;c=y[a+48>>2];b=y[b+16>>2];if(c>>>0>>0){o()}y[a+40>>2]=c;y[a+48>>2]=c-b;c=y[a+44>>2];y[a+36>>2]=c;y[a+44>>2]=b+c}function cd(a,b,c,d,e,f,g,h){return y[c+16>>2]|y[c+12>>2]<<3|y[c+8>>2]<<6|ia(y[y[a+20>>2]+8>>2],w[a+26|0]+(d-y[a+12>>2]|0)|0,e,f)<<8|y[c+4>>2]<<9|y[c>>2]<<10|ia(b,w[a+24|0]+d|0,g,h)<<12}function sa(a,b){var c=0,d=0,e=0;d=Y-16|0;Y=d;e=b<<2;c=y[e+y[a+36>>2]>>2];if(!c){b=nd(na(44),b);c=y[a+36>>2];y[d+12>>2]=0;vb(c+e|0,b);pa(d+12|0);c=y[y[a+36>>2]+e>>2]}Y=d+16|0;return c}function bb(a,b){var c=0;c=y[a>>2];if(c){y[a+4>>2]=c;ma(c);y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0}y[a>>2]=y[b>>2];y[a+4>>2]=y[b+4>>2];y[a+8>>2]=y[b+8>>2];y[b+8>>2]=0;y[b>>2]=0;y[b+4>>2]=0}function Hc(a,b,c){var d=0,e=0;e=((b&536870911)<<3|a>>>29)&-8;d=y[c>>2];a:{while(1){if(B[c>>2]>=e>>>0){break a}if(!Ka(a,b,c)){continue}break}if(y[c>>2]-d>>>0>11){break a}y[c>>2]=d}}function Ed(a,b){var c=0;y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;a:{if(b){if(b>>>0>=536870912){break a}c=yb(b);y[a+4>>2]=c;y[a>>2]=c;y[a+8>>2]=(b<<3)+c;ld(a,b)}return a}Ba();o()}function Jc(a,b){var c=0,d=0,e=0;c=Y-16|0;Y=c;d=z[a+4|0];if((d|0)!=255){e=y[b+4>>2];y[c+8>>2]=y[b>>2];y[c+12>>2]=e;_[y[(c+8|0)+(d<<2)>>2]](c+7|0,a)}w[a+4|0]=255;Y=c+16|0}function Tc(a,b){var c=0;c=Y-16|0;Y=c;y[c+8>>2]=0;a:{if(z[a+4|0]==1){oa(a,b);break a}mb(a);w[a+4|0]=1;y[a>>2]=b}y[c+12>>2]=0;la(c+12|0);la(c+8|0);Y=c+16|0;return a}function Db(a,b){var c=0;if(z[a|0]==1){c=y[a+4>>2];b=se(c,c>>31,b);c=Z;c=!(b- -2147483648>>>0<2147483648?c+1|0:c);b=c?b:0}else{b=0}y[a>>2]=c;y[a+4>>2]=b;return a}function Ac(a,b,c){var d=0,e=0,f=0;d=1;while(1){e=z[a+12|0];if(e>>>0<=(f&255)>>>0){y[c>>2]=(-1<>2]+(d<<3)|0)|d<<1;f=f+1|0;continue}break}}function Hb(a,b,c,d,e,f,g){var h=0,i=0;h=Y-16|0;Y=h;if(qa(a)){i=y[a+12>>2];y[h+8>>2]=y[a+8>>2];y[h+12>>2]=i;y[h>>2]=0;y[h+4>>2]=0;Zc(a,b,c,d,e,f,g,h)}Y=h+16|0}function Cb(a,b,c,d){var e=0;e=Y-16|0;Y=e;y[e+12>>2]=d;d=1;w[e+8|0]=1;a:{if(!z[Na(e+8|0,b>>c)|0]){d=0;w[a|0]=0;break a}y[a>>2]=y[e+12>>2]}w[a+4|0]=d;Y=e+16|0}function qc(a){var b=0;b=y[a+36>>2];y[a+36>>2]=0;if(b){ma(Ja(b))}Ea(a+32|0);Ea(a+28|0);Ea(a+24|0);Ea(a+20|0);Ea(a+16|0);Ea(a+12|0);Ea(a+8|0);Ea(a+4|0);Ea(a)}function Qa(a,b){var c=0,d=0;c=Y-16|0;Y=c;Da(c+8|0,a);d=0-b|0;a=y[c+12>>2];b=y[c+8>>2];while(1){if((a|0)>0){w[b|0]=d;a=a-1|0;b=b+1|0;continue}break}Y=c+16|0}function md(a,b){var c=0;y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;if(b>>>0>=1073741824){Ba();o()}c=Lb(b);y[a+4>>2]=c;y[a>>2]=c;y[a+8>>2]=(b<<2)+c;kd(a,b);return a}function pb(a,b){var c=0;c=y[a+4>>2];if(b){c=c+1|0;y[a+4>>2]=c}b=y[a+12>>2]-y[a+8>>2]>>3;if(b>>>0<=c>>>0){b=b+16|0;ad(a+8|0,b);Gb(a+20|0,b);Gb(a+32|0,b)}}function jc(a,b){var c=0;a:{if((b|0)>=0){if(b>>>0>2147479550){break a}c=Ya(b);if(!c){break a}y[a+4>>2]=c;y[a>>2]=c;y[a+8>>2]=b+c;return}Ba();o()}rb();o()}function _c(a,b){var c=0;a:{if(!z[a+4|0]){c=y[b>>2];if((c|0)==y[a>>2]){break a}y[b>>2]=0;y[a>>2]=c;return}ub(a);c=y[b>>2];y[b>>2]=0;w[a+4|0]=0;y[a>>2]=c}}function Ud(a,b,c,d,e,f){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;if(Sa(a,y[b+8>>2],f)){Pc(b,c,d,e);return}a=y[a+8>>2];_[y[y[a>>2]+20>>2]](a,b,c,d,e,f)}function Eb(a,b,c){a:{switch(a|0){case 1:return b&c;case 2:return b^c;case 3:return b^c^-1;default:o();case 0:b=b|c;break;case 4:break a}}return b}function wc(a){var b=0,c=0,d=0;c=E(z[a+1|0],y[a+4>>2]+1|0);if(c>>>0<=65535){d=z[a+2|0];b=qb();a=z[a|0];y[b+8>>2]=d;y[b+4>>2]=c;w[b|0]=a}return b}function Tb(a){var b=0,c=0,d=0;a:{if(z[a|0]!=1){break a}b=y[a+4>>2];if((b|0)<0){break a}c=b>>>0<1073741824;d=b<<1}y[a>>2]=c;y[a+4>>2]=d;return a}function Vc(a,b,c){var d=0;if(!(b&1)|z[a|0]!=1){b=0;c=0}else{b=y[a+4>>2];d=b;b=b+c|0;c=((d^b)&(b^c))>=0;b=c?b:0}y[a>>2]=c;y[a+4>>2]=b;return a}function ld(a,b){var c=0;c=y[a+4>>2];b=c+(b<<3)|0;while(1){if((b|0)==(c|0)){y[a+4>>2]=b}else{y[c>>2]=0;y[c+4>>2]=0;c=c+8|0;continue}break}}function fe(a,b,c){a=a|0;b=b|0;c=c|0;var d=0;d=y[a+4>>2];a=y[a+8>>2];b=(a>>1)+b|0;c=y[c>>2];if(a&1){d=y[d+y[b>>2]>>2]}return _[d|0](b,c)|0}function Da(a,b){var c=0,d=0;d=Nb(b);a:{c=y[b+16>>2];if((c|0)>=0){b=y[b+12>>2];b=se(b,b>>31,c);if(!Z){break a}}o()}y[a+4>>2]=b;y[a>>2]=d}function Sd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;if(y[a+4>>2]==y[y[b+8>>2]+4>>2]){Oc(b,c,d);return}a=y[a+8>>2];_[y[y[a>>2]+28>>2]](a,b,c,d)}function vd(a,b,c,d){if(b>>>0<=d>>>0){b=(b<<2)+a|0;while(1){if((a|0)!=(b|0)){y[c>>2]=y[a>>2];c=c+4|0;a=a+4|0;continue}break}return}o()}function ia(a,b,c,d){a:{if(!d|(b|0)<0|(a|0)<=(b|0)){a=0}else{a=b>>>3|0;if(a>>>0>=d>>>0){break a}a=z[a+c|0]>>>((b^-1)&7)&1}return a}o()}function wb(a,b){var c=0,d=0;d=y[a>>2];c=y[a+8>>2];if(c>>>0>2]){y[b>>2]=z[d+c|0]>>>7-y[a+12>>2]&1;_b(a);a=0}else{a=-1}return a}function Yc(a,b){var c=0,d=0;if(z[a|0]==1){c=y[a+4>>2];d=c-b|0;c=((c^d)&(b^c))>=0;b=c?d:0}else{b=0;c=0}y[a>>2]=c;y[a+4>>2]=b;return a}function Na(a,b){var c=0,d=0;if(z[a|0]==1){c=y[a+4>>2];d=c+b|0;c=((d^c)&(b^d))>=0;b=c?d:0}else{b=0;c=0}y[a>>2]=c;y[a+4>>2]=b;return a}function nb(a,b){var c=0;c=Y-16|0;Y=c;a:{if(z[a+4|0]==1){rc(a,b);break a}ub(a);w[a+4|0]=1;y[a>>2]=b}y[c+12>>2]=0;Ha(c+12|0);Y=c+16|0}function $a(a){var b=0,c=0;b=y[a>>2];if(b){c=y[a+4>>2];while(1){if((c|0)!=(b|0)){c=la(c-4|0);continue}break}y[a+4>>2]=b;ma(y[a>>2])}}function va(a,b){var c=0,d=0;d=y[a>>2];c=y[a+8>>2];if(c>>>0>2]){w[b|0]=z[d+c|0];y[a+8>>2]=y[a+8>>2]+1;a=0}else{a=-1}return a}function re(){var a=0,b=0,c=0;while(1){b=a<<4;c=b+6080|0;y[b+6084>>2]=c;y[b+6088>>2]=c;a=a+1|0;if((a|0)!=64){continue}break}Fd(48)}function Sc(a){a=a|0;var b=0,c=0,d=0;y[a>>2]=5828;b=y[a+4>>2];c=b-4|0;d=y[c>>2]-1|0;y[c>>2]=d;if((d|0)<0){ma(b-12|0)}return a|0}function kd(a,b){var c=0;c=y[a+4>>2];b=c+(b<<2)|0;while(1){if((b|0)==(c|0)){y[a+4>>2]=b}else{y[c>>2]=0;c=c+4|0;continue}break}}function Ca(a,b,c){var d=0;d=Y-16|0;Y=d;ja(d+8|0,b,c);b=y[d+8>>2];if(b&3){o()}c=y[d+12>>2];y[a>>2]=b;y[a+4>>2]=c>>>2;Y=d+16|0}function Ec(a,b,c){var d=0;d=y[a+4>>2];while(1){if((b|0)!=(c|0)){w[d|0]=z[b|0];d=d+1|0;b=b+1|0;continue}break}y[a+4>>2]=d}function Pb(a,b,c,d){a:{if((d|0)>=0){d=c+d|0;c=d>>>5|0;break a}d=c+d|0;c=(d-31|0)/32|0}y[a+4>>2]=d&31;y[a>>2]=(c<<2)+b}function Ka(a,b,c){var d=0;d=c;c=y[c>>2];y[d>>2]=c+1;d=b;b=c>>>3|0;if(d>>>0<=b>>>0){o()}return z[a+b|0]>>>((c^-1)&7)&1}function ta(a){var b=0,c=0;if(y[a+12>>2]){b=y[a+8>>2]+1|0;if(b){c=y[a+4>>2];y[a+8>>2]=b>>>0>>0?b:c}y[a+12>>2]=0}}function yd(a,b){var c=0;c=se(b,0,a);if(Z|c>>>0>2147479550){a=0}else{b=E(a,b);a=Ya(b);if(b?a:0){t(a,0,b)}}return a}function ib(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){pa(c+68|0);la(c- -64|0);Zb(c+60|0);Za(c+56|0);Ja(c+12|0);ma(c)}}function Bc(a,b,c){if(c>>>0>=536870912){Ba();o()}a=b-a|0;b=a>>2;return a>>>0>=2147483640?536870911:b>>>0>c>>>0?b:c}function pd(a,b){jd(a);y[a>>2]=y[b>>2];y[a+4>>2]=y[b+4>>2];y[a+8>>2]=y[b+8>>2];y[b+8>>2]=0;y[b>>2]=0;y[b+4>>2]=0}function gd(a){var b=0;a:{b=a;a=y[a+16>>2];if((b|0)==(a|0)){b=16}else{if(!a){break a}b=20}_[y[b+y[a>>2]>>2]](a)}}function Vb(a,b){var c=0;c=y[a+52>>2];if((c|0)==3){if(A[a+56>>1]!=1){y[a+52>>2]=-1;return-1}c=hd(a,b)}return c}function ie(a){a=a|0;var b=0,c=0;b=na(12);y[b>>2]=2600;c=y[a+8>>2];y[b+4>>2]=y[a+4>>2];y[b+8>>2]=c;return b|0}function _b(a){var b=0;b=y[a+12>>2];a:{if((b|0)==7){y[a+8>>2]=y[a+8>>2]+1;b=0;break a}b=b+1|0}y[a+12>>2]=b}function _a(a){var b=0,c=0;b=y[a+4>>2];if(!b){o()}c=y[a>>2];y[a>>2]=c+4;y[a+4>>2]=b-1;return y[c>>2]}function Uc(a,b){a:{b:{if(!z[a+4|0]){if(y[a>>2]!=(b|0)){break b}break a}mb(a);w[a+4|0]=0}y[a>>2]=b}}function Od(a,b,c,d,e,f){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;if(Sa(a,y[b+8>>2],f)){Pc(b,c,d,e)}}function gc(a){var b=0,c=0;b=y[a+4>>2];if(!b){o()}c=y[a>>2];y[a>>2]=c+4;y[a+4>>2]=b-1;return c}function he(a,b){a=a|0;b=b|0;var c=0;y[b>>2]=2600;c=y[a+8>>2];y[b+4>>2]=y[a+4>>2];y[b+8>>2]=c}function rd(a,b){var c=0;c=Y-16|0;Y=c;y[c+12>>2]=b;b=a+24|0;a=c+12|0;Wc(b,a);la(a);Y=c+16|0}function jd(a){var b=0;b=y[a>>2];if(b){y[a+4>>2]=b;ma(b);y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0}}function Cc(a){var b=0,c=0;b=y[a+8>>2]+1|0;if(b){c=a;a=y[a+4>>2];y[c+8>>2]=a>>>0>b>>>0?b:a}}function tc(a,b,c,d,e,f,g,h){a:{if(!qa(a)){break a}if(!qa(f)){break a}Zc(f,a,b,c,d,e,h,g)}}function vb(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){ya(c+32|0);ya(c+20|0);ya(c+8|0);ma(c)}}function te(a,b){var c=0,d=0;c=b&31;d=(-1<>>c|0;c=a;a=0-b&31;return d|(c&-1>>>a)<>2]==y[y[b+8>>2]+4>>2]){Oc(b,c,d)}}function Jd(a){a=a|0;var b=0;b=y[a+60>>2];a=y[a+48>>2]+7>>>3|0;return(a>>>0>b>>>0?b:a)|0}function Ic(a,b){b=b-a|0;while(1){if((b|0)>0){w[a|0]=255;b=b-1|0;a=a+1|0;continue}break}}function Rb(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){$a(c+24|0);Ja(c+12|0);ma(Ja(c))}}function Fa(){var a=0;a=na(12);y[a+8>>2]=0;y[a>>2]=0;y[a+4>>2]=0;kb(a,512);return a}function Nc(a){a=a|0;y[a+36>>2]=0;y[a+40>>2]=0;ya(a+76|0);ya(a- -64|0);return a|0}function td(a,b){if(4294967295/(b>>>0)>>>0<=a>>>0){rb();o()}return ab(E(a,b),1)}function Lc(a,b){var c=0;return(z[a+16|0]!=(b|0)|z[a+17|0]!=255?c:z[a+2|0]^1)&1}function pc(a){var b=0;b=y[a>>2];y[a>>2]=0;if(b){ya(b+40|0);ya(b+28|0);ma(b)}}function Id(a){a=a|0;Ic(y[a+76>>2],y[a+80>>2]);y[a+48>>2]=0;return 1}function Xb(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){$a(c+4|0);ma(c)}}function Ja(a){var b=0;b=y[a>>2];if(b){y[a+4>>2]=b;ma(b)}return a}function oa(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){ub(c);ma(c)}}function $b(a){var b=0;b=y[a>>2];y[a>>2]=0;if(b){ma(b)}return a}function vc(a){var b=0;b=na(16);y[b>>2]=a;gb(b+4|0,a);return b}function ee(a,b){a=a|0;b=b|0;return(y[b+4>>2]==2888?a+4|0:0)|0}function Lb(a){if(a>>>0>=1073741824){jb();o()}return na(a<<2)}function yb(a){if(a>>>0>=536870912){jb();o()}return na(a<<3)}function jb(){var a=0;a=Fb(4);y[a>>2]=5724;P(a|0,5780,2);o()}function Ea(a){var b=0;b=y[a>>2];y[a>>2]=0;if(b){ma(Ja(b))}}function na(a){a=Ya(a>>>0<=1?1:a);if(!a){Qc();o()}return a}function ya(a){var b=0;b=y[a>>2];if(b){y[a+4>>2]=b;ma(b)}}function Va(a,b){var c=0;c=y[a>>2];y[a>>2]=b;if(c){ma(c)}}function fb(a,b,c,d,e,f,g){if(qa(a)){Hb(f,a,b,c,d,e,g)}}function Ua(a){var b=0;b=y[a>>2];y[a>>2]=0;if(b){ma(b)}}function ue(a){if(a){return 31-H(a-1^a)|0}return 32} +function db(){var a=0;a=na(28);y[a+20>>2]=0;return a}function ab(a,b){a=yd(a,b);if(!a){rb();o()}return a}function rc(a,b){var c=0;c=y[a>>2];y[a>>2]=b;ma(c)}function qd(a){var b=0;b=na(4);y[b>>2]=a;return b}function lc(){var a=0;a=na(36);t(a,0,36);return a}function rb(){y[(Y-16|0)+12>>2]=6036;sc();o()}function Wd(a,b,c){a=a|0;b=b|0;c=c|0;return 0}function Oa(a,b,c,d){return(Eb(a,b,c)^c)&d^c}function Mb(a,b){a=a|0;b=b|0;return y[b>>2]}function ge(a){a=a|0;_[y[y[a>>2]+4>>2]](a)}function fd(a){if(!z[a+4|0]){ya(y[a>>2])}}function Zd(a){a=a|0;T();S(a+128|0);o()}function Ob(a){a=y[a>>2];if(a){ma(a)}}function Md(a){a=a|0;return y[a+4>>2]}function Gd(a){a=a|0;return Ya(a)|0}function Fb(a){return Ya(a|80)+80|0}function wa(a){return nd(na(44),a)}function ce(a,b){a=a|0;b=b|0;Ha(b)}function be(a,b){a=a|0;b=b|0;la(b)}function qa(a){return(Nb(a)|0)!=0}function fc(a){return(Xa(a)|0)!=0}function de(a){a=a|0;return 2868}function Yd(a){a=a|0;return 4890}function Xd(a){a=a|0;return 4864}function Xa(a){return Kc(a,4944)}function Nd(a){a=a|0;return 4913}function Nb(a){return Kc(a,2416)}function xb(a){a=a|0;return a|0}function pa(a){vb(a,0);return a}function oc(a){Za(a+24|0);ma(a)}function la(a){oa(a,0);return a}function ac(a){ib(a,0);return a}function Ab(a){zb(a,0);return a}function Mc(a){a=a|0;ma(Sc(a))}function Kd(a){a=a|0;ma(Nc(a))}function wd(a){return Lc(a,3)}function _d(a){a=a|0;sc();o()}function Qb(a){return Lc(a,2)}function $c(a,b){a=a|0;b=b|0}function Ta(a){a=a|0;ma(a)}function ub(a){Jc(a,4856)}function mb(a){Jc(a,4936)}function ua(a){Va(a,0)}function Zb(a){Xb(a,0)}function Za(a){Rb(a,0)}function Qc(){sc();o()}function Ha(a){rc(a,0)}function sc(){U();o()}function hc(a){a=a|0} +// EMSCRIPTEN_END_FUNCS +a=z;m(n);var _=[null,Sc,xb,qe,pe,oe,ne,me,le,ke,je,xb,Nc,Kd,Jd,Id,Hd,Mb,Mb,xb,Ta,ie,he,hc,ge,fe,ee,de,$c,ce,$c,be,Mb,Mb,Zd,_d,Ta,Yd,Ta,Xd,xb,Ta,hc,hc,Wd,Ta,Qd,Ta,Nd,Mc,Md,Mc,Ta,Vd,Od,Pd,Rd,Ta,Ud,Td,Sd];function $(){return v.byteLength>>16}function ea(fa){fa=fa|0;var aa=$()|0;var ba=aa+fa|0;if(aa{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=instantiateSync(wasmBinaryFile,info);return receiveInstance(result[0])}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var __abort_js=()=>abort("");var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};function _createImageData(size){Module.imageData=new Uint8Array(size)}var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};function _setImageData(array_ptr,pitch8,pitch32,height){if(pitch32===pitch8){Module.imageData=new Uint8ClampedArray(HEAPU8.subarray(array_ptr,array_ptr+pitch32*height));return}const destSize=pitch8*height;const imageData=Module.imageData=new Uint8ClampedArray(destSize);for(let srcStart=array_ptr,destStart=0;destStart{HEAP8.set(array,buffer)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["writeArrayToMemory"]=writeArrayToMemory;var _malloc,_free,_jbig2_decode,_ccitt_decode,__emscripten_timeout,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_malloc=Module["_malloc"]=wasmExports["l"];_free=Module["_free"]=wasmExports["m"];_jbig2_decode=Module["_jbig2_decode"]=wasmExports["n"];_ccitt_decode=Module["_ccitt_decode"]=wasmExports["o"];__emscripten_timeout=wasmExports["p"];memory=wasmMemory=wasmExports["j"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,f:__abort_js,e:__emscripten_runtime_keepalive_clear,b:__setitimer_js,h:_createImageData,c:_emscripten_resize_heap,d:_proc_exit,i:_setImageData,g:_setLineData};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=createWasm();run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +;return moduleRtn}export default JBig2; diff --git a/web/public/vendor/pdfjs/wasm/openjpeg.wasm b/web/public/vendor/pdfjs/wasm/openjpeg.wasm new file mode 100644 index 00000000..5acce73f Binary files /dev/null and b/web/public/vendor/pdfjs/wasm/openjpeg.wasm differ diff --git a/web/public/vendor/pdfjs/wasm/openjpeg_nowasm_fallback.js b/web/public/vendor/pdfjs/wasm/openjpeg_nowasm_fallback.js new file mode 100644 index 00000000..0df96aed --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/openjpeg_nowasm_fallback.js @@ -0,0 +1,17 @@ +/* THIS FILE IS GENERATED - DO NOT EDIT */ +async function OpenJPEG(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";var readAsync;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var WebAssembly={Memory:function(opts){this.buffer=new ArrayBuffer(opts["initial"]*65536)},Module:function(binary){},Instance:function(module,info){this.exports=( +// EMSCRIPTEN_START_ASM +function instantiate(Aa){var a;var b=new Uint8Array(123);for(var c=25;c>=0;--c){b[48+c]=52+c;b[65+c]=c;b[97+c]=26+c}b[43]=62;b[47]=63;function i(j,k,l){var d,e,c=0,f=k,g=l.length,h=k+(g*3>>2)-(l[g-2]=="=")-(l[g-1]=="=");for(;c>4;if(f>2;if(f>>0;A=A>>>0;if(z+A>a.length)throw"trap: invalid memory.fill";a.fill(v,z,z+A)}function B(z,C,A){a.copyWithin(z,C,C+A)}function D(){throw new Error("abort")}function za(n){var E=new ArrayBuffer(16777216);var F=new Int8Array(E);var G=new Int16Array(E);var H=new Int32Array(E);var I=new Uint8Array(E);var J=new Uint16Array(E);var K=new Uint32Array(E);var L=new Float32Array(E);var M=new Float64Array(E);var N=Math.imul;var O=Math.fround;var P=Math.abs;var Q=Math.clz32;var R=Math.min;var S=Math.max;var T=Math.floor;var U=Math.ceil;var V=Math.trunc;var W=Math.sqrt;var X=n.a;var Y=X.a;var Z=X.b;var _=X.c;var $=X.d;var aa=X.e;var ba=X.f;var ca=X.g;var da=X.h;var ea=X.i;var fa=X.j;var ga=X.k;var ha=X.l;var ia=X.m;var ja=X.n;var ka=X.o;var la=X.p;var ma=X.q;var na=94240;var oa=0;var pa=0;var qa=0; +// EMSCRIPTEN_START_FUNCS +function Zc(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,z=0,A=0,C=0,D=0,E=0,M=0,P=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=O(0),ha=0,ia=0,ja=0,ka=0,la=0,ma=0,oa=0,pa=0,qa=0,sa=0,ta=0,ua=0,va=0,wa=0;Y=na-96|0;na=Y;C=H[a+8>>2];a:{b:{c:{if(!H[a>>2]){g=N(H[C+16>>2]-H[C+8>>2]|0,H[C+20>>2]-H[C+12>>2]|0)<<2;c=Ia(g);H[C+60>>2]=c;if(!c){Ba(H[a+32>>2],1,8023,0);d=a+28|0;break b}if(!g){break c}y(c,0,g);break c}c=H[C+60>>2];if(!c){break c}Ca(c);H[C+60>>2]=0}if(!H[H[a+28>>2]>>2]){break a}oa=H[a+16>>2];c=H[oa+28>>2]+N(H[oa+24>>2],152)|0;va=H[c-152>>2];wa=H[c-144>>2];pa=H[a+20>>2];qa=H[a+12>>2];ua=H[a+4>>2];d=a+28|0;d:{q=H[b+4>>2];f=0;e:{if((q|0)<=0){break e}k=H[b>>2];c=0;f:{while(1){g=k+N(c,12)|0;if(!H[g>>2]){break f}c=c+1|0;if((q|0)!=(c|0)){continue}break}f=0;break e}f=H[g+4>>2]}if(f){break d}f=Ea(1,156);if(!f){Ba(H[a+32>>2],1,6313,0);break b}H[f+140>>2]=0;c=0;k=H[b+4>>2];g:{if((k|0)==2147483647){break g}g=H[b>>2];if((k|0)>0){while(1){q=g+N(c,12)|0;if(!H[q>>2]){k=H[q+8>>2];if(k){ra[k|0](H[q+4>>2]);g=H[b>>2]}b=g+N(c,12)|0;H[b+8>>2]=15;H[b+4>>2]=f;c=1;break g}c=c+1|0;if((k|0)!=(c|0)){continue}break}}g=Ha(g,N(k,12)+12|0);c=0;if(!g){break g}H[b>>2]=g;c=H[b+4>>2];g=g+N(c,12)|0;H[g+8>>2]=15;H[g+4>>2]=f;H[g>>2]=0;H[b+4>>2]=c+1;c=1}if(c){break d}Ba(H[a+32>>2],1,8338,0);b=H[f+116>>2];if(b){Ca(b);H[f+116>>2]=0}b=H[f+120>>2];if(b){Ca(b);H[f+120>>2]=0}Ca(H[f+148>>2]);Ca(f);break b}H[f+144>>2]=H[a+24>>2];E=H[a+40>>2];_=H[a+36>>2];S=H[a+32>>2];j=H[pa+808>>2];b=H[qa+16>>2];h:{V=H[pa+16>>2];i:{if(V&64){k=na-304|0;na=k;j:{if(j){if(_){Ba(S,1,3219,0);break j}Ba(S,1,3219,0);break j}i=H[f+116>>2];c=H[C+20>>2]-H[C+12>>2]|0;b=H[C+16>>2]-H[C+8>>2]|0;g=N(c,b);k:{l:{if(g>>>0>K[f+132>>2]){Ca(i);j=g<<2;i=Ia(j);H[f+116>>2]=i;if(!i){i=0;break j}H[f+132>>2]=g;break l}if(!i){break k}j=g<<2}if(!j){break k}y(i,0,j)}i=H[f+120>>2];m:{if(K[f+136>>2]>2639){break m}Ca(i);i=Ia(10560);H[f+120>>2]=i;if(i){break m}i=0;break j}H[f+136>>2]=2640;y(i,0,10560);H[f+128>>2]=c;H[f+124>>2]=b;n=H[C+24>>2];if(!n){i=1;break j}q=H[C+28>>2];i=1;n:{o:{p:{q:{j=H[C+52>>2];r:{if(j){g=j&3;c=H[C+4>>2];i=0;s:{if(j>>>0>=4){b=j&-4;while(1){o=c+(i<<3)|0;h=H[o+28>>2]+(H[o+20>>2]+(H[o+12>>2]+(H[o+4>>2]+h|0)|0)|0)|0;i=i+4|0;z=z+4|0;if((b|0)!=(z|0)){continue}break}if(!g){break s}}while(1){h=H[(c+(i<<3)|0)+4>>2]+h|0;i=i+1|0;l=l+1|0;if((g|0)!=(l|0)){continue}break}}if(!H[f+144>>2]&(j|0)==1){break o}if(K[f+152>>2]>=h>>>0){break r}z=Ha(H[f+148>>2],h);if(z){break q}i=0;break j}if(!H[f+144>>2]){break j}}z=H[f+148>>2];if(z){break p}i=0;break j}H[f+152>>2]=h;H[f+148>>2]=z}if(!H[C+52>>2]){h=0;break n}j=H[C+4>>2];h=0;i=0;while(1){g=i<<3;c=g+j|0;b=H[c+4>>2];if(b){B(h+z|0,H[c>>2],b)}j=H[C+4>>2];h=H[(g+j|0)+4>>2]+h|0;i=i+1|0;if(i>>>0>2]){continue}break}break n}z=H[H[C+4>>2]>>2]}i=0;j=0;c=H[C+40>>2];g=0;t:{if(!c){break t}b=H[C>>2];j=H[b+8>>2];g=0;if((c|0)==1){break t}g=H[b+32>>2]}c=n-q|0;j=g+j|0;u:{if(!j){l=0;break u}i=1;b=H[C>>2];p=H[b>>2];l=0;if((j|0)==1){i=0;break u}l=H[b+24>>2]}P=c+1|0;ha=H[f+116>>2];s=H[f+120>>2];X=H[C+12>>2];x=H[C+20>>2];ca=H[C+8>>2];D=H[C+16>>2];v:{w:{x:{y:{z:{A:{B:{C:{if(!(!i|l)){if(!_){break C}Ba(S,2,10806,0);j=1;break B}if(j>>>0<4){break B}if(_){H[k+112>>2]=j;Ba(S,1,9590,k+112|0);break v}H[k+96>>2]=j;Ba(S,1,9590,k+96|0);i=0;break j}Ba(S,2,10806,0);i=H[C+24>>2];if(i>>>0>30){break A}e=1;if(i>>>0>=P>>>0){break y}break w}i=H[C+24>>2];if(i>>>0<=30){break z}if(!_){break A}H[k+32>>2]=H[C+24>>2];Ba(S,1,12302,k+32|0);break v}H[k>>2]=i;Ba(S,1,12302,k);i=0;break j}if(i>>>0

>>0){break x}if(j>>>0<2){e=j;break y}if((i|0)!=(P|0)){e=j;break y}e=1;if(I[26384]){break y}if(!_){F[26384]=1;H[k+64>>2]=j;Ba(S,2,10299,k- -64|0);break y}if(!I[26384]){F[26384]=1;H[k+80>>2]=j;Ba(S,2,10299,k+80|0)}}if(!(!(p>>>0<2|h>>>0

>>0)&l+p>>>0<=h>>>0)){if(_){i=0;Ba(S,1,9532,0);break j}i=0;Ba(S,1,9532,0);break j}U=p+z|0;b=I[U-1|0];i=b<<4|I[U-2|0]&15;if(!(!(i>>>0<2|(b|0)==255)&(i|0)<=(p|0))){if(_){i=0;Ba(S,1,15305,0);break j}i=0;Ba(S,1,15305,0);break j}sa=H[C+28>>2];H[k+272>>2]=0;H[k+280>>2]=0;H[k+264>>2]=0;H[k+268>>2]=0;H[k+296>>2]=0;H[k+300>>2]=0;H[k+284>>2]=0;H[k+288>>2]=0;b=i-1|0;H[k+276>>2]=b;o=(p+z|0)-i|0;H[k+256>>2]=o;q=I[o|0];c=8;H[k+272>>2]=8;j=o+1|0;H[k+256>>2]=j;g=i-2|0;H[k+276>>2]=g;n=(b|0)==1?q|15:q;b=0;q=b;H[k+264>>2]=n;H[k+268>>2]=b;H[k+280>>2]=!b&(n|0)==255;u=o&3;D:{E:{if((u|0)==3){break E}v=0;if(!((n|0)!=255|(b|0)!=0|I[j|0]<=143)){break D}b=255;b=i>>>0>=3?I[j|0]:b;h=i-3|0;H[k+276>>2]=h;o=!q&(n|0)==255;c=o?15:16;H[k+272>>2]=c;R=j+(i>>>0>2)|0;H[k+256>>2]=R;b=(g|0)==1?b|15:b;g=0;H[k+280>>2]=!g&(b|0)==255;g=b;j=n;b=o?7:8;o=b&31;if((b&63)>>>0>=32){m=j<>>32-o|q<>2]=n;H[k+268>>2]=b;if((u|0)==2){break E}o=255;v=0;if(!((g|0)!=255|(r|0)!=0|I[R|0]<=143)){break D}o=i>>>0>=4?I[R|0]:o;j=i-4|0;H[k+276>>2]=j;R=R+(i>>>0>3)|0;H[k+256>>2]=R;b=(h|0)==1?o|15:o;o=0;m=o;H[k+280>>2]=!m&(b|0)==255;o=!r&(g|0)==255;c=(o?7:8)+c|0;H[k+272>>2]=c;g=b;h=n;b=o?7:8;r=b&31;if((b&63)>>>0>=32){o=h<>>32-r|q<>2]=n;H[k+268>>2]=b;if((u|0)==1){break E}v=0;if(!((g|0)!=255|(m|0)!=0|I[R|0]<=143)){break D}b=255;b=i>>>0>=5?I[R|0]:b;H[k+276>>2]=i-5;H[k+256>>2]=R+(i>>>0>4);o=0;b=(j|0)==1?b|15:b;H[k+280>>2]=1&(b|0)==255;g=!m&(g|0)==255;c=(g?7:8)+c|0;H[k+272>>2]=c;j=n;g=g?7:8;h=g&31;if((g&63)>>>0>=32){E=j<>>32-h|q<>2]=n;H[k+268>>2]=b}b=n;c=64-c|0;g=c&31;if((c&63)>>>0>=32){w=b<>>32-g|q<>2]=b;H[k+268>>2]=w;v=1}if(!v){if(_){i=0;Ba(S,1,11470,0);break j}i=0;Ba(S,1,11470,0);break j}A=D-ca|0;o=0;m=i;r=i-2|0;H[k+244>>2]=r;c=p+z|0;b=c-3|0;H[k+224>>2]=b;g=I[c-2|0];c=g>>>0>143;H[k+248>>2]=c;q=0;n=g>>>4|0;H[k+232>>2]=n;H[k+236>>2]=0;h=(n&7)==7?3:4;H[k+240>>2]=h;g=(b&3)+1|0;R=g>>>0>>0?g:r;F:{G:{if(r){while(1){i=c;u=b;b=b-1|0;H[k+224>>2]=b;g=I[u|0];c=g>>>0>143;H[k+248>>2]=c;j=h&31;if((h&63)>>>0>=32){E=g<>>32-j;j=g<>2]=n;q=q|E;H[k+236>>2]=q;h=(i&1?(g&127)==127?7:8:8)+h|0;H[k+240>>2]=h;o=o+1|0;if((R|0)!=(o|0)){continue}break}j=r-R|0;H[k+244>>2]=j;if(h>>>0>32){break F}if((j|0)>=4){o=H[u-4>>2];H[k+224>>2]=u-5;H[k+244>>2]=j-4;break G}if((j|0)<=0){o=0;break G}H:{if((R|0)==(m-3|0)){i=24;o=0;break H}E=j&1;r=j&2147483646;i=24;o=0;g=0;while(1){u=b-1|0;H[k+224>>2]=u;R=I[b|0];b=b-2|0;H[k+224>>2]=b;H[k+244>>2]=j-1;u=I[u|0];j=j-2|0;H[k+244>>2]=j;o=R<>2]=b-1;b=I[b|0];H[k+244>>2]=j-1;o=b<>2]=r-R}u=o&255;H[k+248>>2]=u>>>0>143;g=c?(o&2130706432)==2130706432?7:8:8;c=g+(o>>>0<=2415919103?8:(o&8323072)==8323072?7:8)|0;i=o>>>16&255;b=c+(i>>>0<=143?8:(o&32512)==32512?7:8)|0;j=o>>>8&255;H[k+240>>2]=b+((j>>>0<=143?8:(o&127)==127?7:8)+h|0);b=i<>>24|j<>>0>=32){o=b<>>32-c;b=b<>2]=b|n;H[k+236>>2]=o|q}Zb(k+192|0,z,p-m|0,255);b=0;I:{if(e>>>0<2){break I}Zb(k+160|0,U,l,0);b=0;if((e|0)==2){break I}n=0;q=0;o=0;H[k+152>>2]=1;H[k+144>>2]=0;H[k+136>>2]=0;H[k+140>>2]=0;c=l;i=c-1|0;H[k+148>>2]=i;b=(p+z|0)+c|0;g=b-1|0;H[k+128>>2]=g;R=g&3;J:{if((c|0)<=0){b=g;break J}b=b-2|0;H[k+128>>2]=b;n=I[g|0]}H[k+136>>2]=n;H[k+140>>2]=0;p=n>>>0>143;H[k+152>>2]=p;l=(n&127)==127?7:8;H[k+144>>2]=l;K:{if(!R){break K}u=c-2|0;H[k+148>>2]=u;L:{if((c|0)<2){g=b;break L}g=b-1|0;H[k+128>>2]=g;o=I[b|0]}p=o>>>0>143;H[k+152>>2]=p;b=l&31;if((l&63)>>>0>=32){m=o<>>32-b;b=o<>2]=q;H[k+140>>2]=m;l=(n>>>0<=143?8:(o&127)==127?7:8)+l|0;H[k+144>>2]=l;if((R|0)==1){b=g;n=q;q=m;c=i;i=u;break K}h=c-3|0;H[k+148>>2]=h;M:{if((c|0)<3){j=g;break M}j=g-1|0;H[k+128>>2]=j;aa=I[g|0]}p=aa>>>0>143;H[k+152>>2]=p;b=l&31;if((l&63)>>>0>=32){E=aa<>>32-b;b=aa<>2]=n;H[k+140>>2]=b;l=(o>>>0<=143?8:(aa&127)==127?7:8)+l|0;H[k+144>>2]=l;if((R|0)==2){b=j;c=u;i=h;break K}i=c-4|0;H[k+148>>2]=i;o=0;N:{if((c|0)<4){b=j;break N}b=j-1|0;H[k+128>>2]=b;o=I[j|0]}p=o>>>0>143;H[k+152>>2]=p;c=l&31;if((l&63)>>>0>=32){w=o<>>32-c;c=o<>2]=n;H[k+140>>2]=c;l=(aa>>>0<=143?8:(o&127)==127?7:8)+l|0;H[k+144>>2]=l;c=h}if(l>>>0<=32){O:{if((c|0)>=5){g=H[b-3>>2];H[k+148>>2]=c-5;H[k+128>>2]=b-4;break O}g=0;if((c|0)<2){break O}c=24;while(1){j=b-1|0;H[k+128>>2]=j;b=I[b|0];o=i-1|0;H[k+148>>2]=o;g=b<>>0>1;b=j;c=c-8|0;i=o;if(h){continue}break}}h=g&255;H[k+152>>2]=h>>>0>143;i=p?(g&2130706432)==2130706432?7:8:8;c=i+(g>>>0<=2415919103?8:(g&8323072)==8323072?7:8)|0;o=g>>>16&255;b=c+(o>>>0<=143?8:(g&32512)==32512?7:8)|0;j=g>>>8&255;H[k+144>>2]=b+((j>>>0<=143?8:(g&127)==127?7:8)+l|0);b=o<>>24|j<>>0>=32){m=b<>>32-c;b=b<>2]=b|n;H[k+140>>2]=m|q}b=1}aa=b;$=x-X|0;ta=P+1|0;F[s+2112|0]=0;u=s+2112|0;g=Ya(k+256|0);if((A|0)>0){P=sa-1|0;l=s;j=u;b=0;c=ha;z=0;while(1){R=z;h=J[(b<<8|(lb(k+224|0)&127)<<1)+16656>>1];P:{if(b){break P}b=g-2|0;h=(b|0)==-1?h:0;if((g|0)>1){g=b;break P}g=Ya(k+256|0)}q=H[k+236>>2];n=H[k+232>>2];i=H[k+240>>2];x=h>>>4|0;p=H[l>>2]|(x&3|h>>>2&48)<>2]=p;E=h&16;b=h>>>5&7|E>>>4;o=i;i=h&7;z=o-i|0;n=((1<>>i;q=q>>>i|0;o=n;i=0;if((A|0)>(R|2)){i=J[(b<<8|(n&127)<<1)+16656>>1];Q:{if(b){break Q}b=g-2|0;i=(b|0)==-1?i:0;if((g|0)>1){g=b;break Q}g=Ya(k+256|0)}b=i&7;z=z-b|0;n=((1<>>b;q=q>>>b|0;o=n;b=i>>>4&1|i>>>5&7}H[l>>2]=p|(i<<2&768|i&48)<>>2&2|h>>>3&1;R:{if((p|0)!=3){break R}m=g-2|0;p=(m|0)==-1?4:3;if((g|0)>1){g=m;break R}g=Ya(k+256|0)}S:{if(!p){H[k+120>>2]=1;H[k+124>>2]=1;o=0;break S}if(p>>>0<=2){m=I[(o&7)+20804|0];w=m>>>2&7;r=m&3;m=(((-1<>>r)+(m>>>5|0)|0)+1|0;o=(p|0)==1;H[k+124>>2]=o?1:m;H[k+120>>2]=o?m:1;o=r+w|0;break S}v=o;o=I[(o&7)+20804|0];U=o&3;m=v>>>U|0;if((p|0)==3){X=(o>>>5|0)+1|0;if((U|0)==3){H[k+124>>2]=m&1|2;o=o>>>2&7;H[k+120>>2]=X+((-1<>>1);o=o+4|0;break S}r=I[(m&7)+20804|0];p=r&3;m=m>>>p|0;w=o>>>2&7;H[k+120>>2]=X+(m&(-1<>>2&7;H[k+124>>2]=(((-1<>>w)+(r>>>5|0)|0)+1;o=o+(p+(w+U|0)|0)|0;break S}r=I[(m&7)+20804|0];p=r&3;m=m>>>p|0;w=o>>>2&7;H[k+120>>2]=((m&(-1<>>5|0)|0)+3;o=r>>>2&7;H[k+124>>2]=(((-1<>>w)+(r>>>5|0)|0)+3;o=o+(w+(p+U|0)|0)|0}T:{X=H[k+120>>2];if(X>>>0<=ta>>>0){w=H[k+124>>2];if(w>>>0<=ta>>>0){break T}}if(_){i=0;Ba(S,1,15756,0);break j}i=0;Ba(S,1,15756,0);break j}H[k+240>>2]=z-o;m=o&31;if((o&63)>>>0>=32){o=0;q=q>>>m|0}else{o=q>>>m|0;q=((1<>>m}H[k+232>>2]=q;H[k+236>>2]=o;o=i&240|x&15;z=R+4|0;q=(z|0)<=(A|0)?255:255>>>(z-A<<1)|0;x=($|0)>1?q:q&85;if(o&(x^-1)){if(_){i=0;Ba(S,1,12194,0);break j}i=0;Ba(S,1,12194,0);break j}U:{V:{if(E){n=Ma(k+192|0);p=X+(h<<19>>31)|0;H[k+208>>2]=H[k+208>>2]-p;m=H[k+204>>2];q=H[k+200>>2];r=p&31;if((p&63)>>>0>=32){o=0;q=m>>>r|0}else{o=m>>>r|0;q=((1<>>r}H[k+200>>2]=q;H[k+204>>2]=o;T=(n&(-1<>>8&1)<>2]=T}W:{if(h&32){n=Ma(k+192|0);p=X+(h<<18>>31)|0;H[k+208>>2]=H[k+208>>2]-p;m=H[k+204>>2];q=H[k+200>>2];r=p&31;if((p&63)>>>0>=32){o=0;q=m>>>r|0}else{o=m>>>r|0;q=((1<>>r}H[k+200>>2]=q;H[k+204>>2]=o;q=n&(-1<>>9&1)<>2]=q+2<>>0>q>>>0?n:q)|128;break W}if(!(x&2)){break W}H[(A<<2)+c>>2]=0}p=c+4|0;X:{Y:{if(h&64){n=Ma(k+192|0);r=X+(h<<17>>31)|0;H[k+208>>2]=H[k+208>>2]-r;m=H[k+204>>2];q=H[k+200>>2];E=r&31;if((r&63)>>>0>=32){o=0;q=m>>>E|0}else{o=m>>>E|0;q=((1<>>E}H[k+200>>2]=q;H[k+204>>2]=o;q=(n&(-1<>>10&1)<>2]=q}F[j+1|0]=0;Z:{if(h&128){n=Ma(k+192|0);r=X-(h>>>15|0)|0;H[k+208>>2]=H[k+208>>2]-r;m=H[k+204>>2];q=H[k+200>>2];E=r&31;if((r&63)>>>0>=32){o=0;q=m>>>E|0}else{o=m>>>E|0;q=((1<>>E}H[k+200>>2]=q;H[k+204>>2]=o;q=n&(-1<>>11&1)<>2]=q+2<>2]=0}m=c+8|0;_:{$:{if(i&16){n=Ma(k+192|0);p=w+(i<<19>>31)|0;H[k+208>>2]=H[k+208>>2]-p;h=H[k+204>>2];q=H[k+200>>2];r=p&31;if((p&63)>>>0>=32){o=0;q=h>>>r|0}else{o=h>>>r|0;q=((1<>>r}H[k+200>>2]=q;H[k+204>>2]=o;p=(n&(-1<>>8&1)<>2]=p}aa:{if(i&32){n=Ma(k+192|0);p=w+(i<<18>>31)|0;H[k+208>>2]=H[k+208>>2]-p;h=H[k+204>>2];q=H[k+200>>2];r=p&31;if((p&63)>>>0>=32){o=0;q=h>>>r|0}else{o=h>>>r|0;q=((1<>>r}H[k+200>>2]=q;H[k+204>>2]=o;q=n&(-1<>>9&1)<>2]=q+2<>>0>q>>>0?n:q)|128;break aa}if(!(x&32)){break aa}H[m+(A<<2)>>2]=0}m=c+12|0;ba:{ca:{if(i&64){n=Ma(k+192|0);p=w+(i<<17>>31)|0;H[k+208>>2]=H[k+208>>2]-p;h=H[k+204>>2];q=H[k+200>>2];r=p&31;if((p&63)>>>0>=32){o=0;q=h>>>r|0}else{o=h>>>r|0;q=((1<>>r}H[k+200>>2]=q;H[k+204>>2]=o;p=(n&(-1<>>10&1)<>2]=p}j=j+2|0;F[j|0]=0;da:{if(i&128){n=Ma(k+192|0);p=w-(i>>>15|0)|0;H[k+208>>2]=H[k+208>>2]-p;h=H[k+204>>2];q=H[k+200>>2];r=p&31;if((p&63)>>>0>=32){o=0;q=h>>>r|0}else{o=h>>>r|0;q=((1<>>r}H[k+200>>2]=q;H[k+204>>2]=o;i=n&(-1<>>11&1)<>2]=i+2<>>0<128){break da}H[m+(A<<2)>>2]=0}Z=Z^16;l=(R&4)+l|0;c=c+16|0;if((z|0)<(A|0)){continue}break}}ia=V&8;ja=s+1584|0;ka=s+1056|0;fa=s+528|0;if(($|0)>=3){la=N(A,12);ma=A<<3;da=sa-1|0;b=sa-2|0;t=3<>>1&2147483644)+4|0;r=2;while(1){R=r;T=I[u|0];F[u|0]=0;Z=Z&-17^2;ea:{if((A|0)<=0){r=r+2|0;break ea}b=R&4?fa:s;r=R+2|0;j=ha+(N(A,R)<<2)|0;p=0;c=u;E=0;while(1){V=p;P=T&255;i=I[c+1|0]>>>5&4|(P>>>7|E);h=J[(i<<8|(lb(k+224|0)&127)<<1)+18704>>1];fa:{if(i){break fa}i=g-2|0;h=(i|0)==-1?h:0;if((g|0)>1){g=i;break fa}g=Ya(k+256|0)}q=H[k+236>>2];n=H[k+232>>2];i=H[k+240>>2];l=H[b>>2]|(h>>>4&3|h>>>2&48)<>2]=l;U=h&64;X=h&128;E=U>>>5|X>>>6;o=i;i=h&7;x=o-i|0;n=((1<>>i;q=q>>>i|0;p=n;i=0;if((A|0)>(V|2)){o=I[c+2|0]>>>5&4|I[c+1|0]>>>7|E;i=J[(o<<8|(n&127)<<1)+18704>>1];ga:{if(o){break ga}o=g-2|0;i=(o|0)==-1?i:0;if((g|0)>1){g=o;break ga}g=Ya(k+256|0)}o=i&7;x=x-o|0;E=(i>>>5|i>>>6)&2;n=((1<>>o;p=n;q=q>>>o|0}H[b>>2]=l|(i<<2&768|i&48)<>>2&2|h>>>3&1;switch(z|0){case 0:break ha;case 3:break ia;default:break ja}}o=I[(p&7)+20804|0];w=o>>>2&7;v=p;p=o&3;l=(((-1<>>p)+(o>>>5|0)|0)+1|0;m=(z|0)==1;o=m?1:l;l=m?l:1;z=p+w|0;break ha}v=I[(p&7)+20804|0];m=v&3;o=p>>>m|0;w=I[(o&7)+20804|0];l=w&3;p=v>>>2&7;M=p+(l+m|0)|0;m=w>>>2&7;z=M+m|0;o=o>>>l|0;l=((o&(-1<>>5|0)|0)+1|0;o=(((-1<>>p)+(w>>>5|0)|0)+1|0}H[k+240>>2]=x-z;m=z&31;if((z&63)>>>0>=32){w=0;q=q>>>m|0}else{w=q>>>m|0;q=((1<>>m}H[k+232>>2]=q;H[k+236>>2]=w;p=h&240;if(p-1&p){n=P&127;q=I[c+1|0]&127;n=n>>>0>q>>>0?n:q;q=n-2|0;l=(n>>>0>=q>>>0?q:0)+l|0}m=i&240;if(m-1&m){n=I[c+1|0]&127;q=I[c+2|0]&127;q=n>>>0>q>>>0?n:q;o=(q>>>0>2?q-2|0:0)+o|0}if(!(l>>>0<=ta>>>0&o>>>0<=ta>>>0)){if(_){i=0;Ba(S,1,15856,0);break j}i=0;Ba(S,1,15856,0);break j}T=I[c+2|0];F[c+1|0]=0;F[c+2|0]=0;n=m|p>>>4;p=V+4|0;q=(p|0)<=(A|0)?255:255>>>(p-A<<1)|0;P=(r|0)>($|0)?q&85:q;if(n&(P^-1)){if(_){i=0;Ba(S,1,12194,0);break j}i=0;Ba(S,1,12194,0);break j}ka:{la:{if(h&16){n=Ma(k+192|0);x=(h<<19>>31)+l|0;H[k+208>>2]=H[k+208>>2]-x;m=H[k+204>>2];q=H[k+200>>2];z=x&31;if((x&63)>>>0>=32){w=0;q=m>>>z|0}else{w=m>>>z|0;q=((1<>>z}H[k+200>>2]=q;H[k+204>>2]=w;x=(n&(-1<>>8&1)<>2]=x}ma:{if(h&32){n=Ma(k+192|0);x=(h<<18>>31)+l|0;H[k+208>>2]=H[k+208>>2]-x;m=H[k+204>>2];q=H[k+200>>2];z=x&31;if((x&63)>>>0>=32){w=0;q=m>>>z|0}else{w=m>>>z|0;q=((1<>>z}H[k+200>>2]=q;H[k+204>>2]=w;q=n&(-1<>>9&1)<>2]=q+2<>>0>q>>>0?n:q)|128;break ma}if(!(P&2)){break ma}H[(A<<2)+j>>2]=0}x=j+4|0;na:{oa:{if(U){n=Ma(k+192|0);z=(h<<17>>31)+l|0;H[k+208>>2]=H[k+208>>2]-z;m=H[k+204>>2];q=H[k+200>>2];U=z&31;if((z&63)>>>0>=32){w=0;q=m>>>U|0}else{w=m>>>U|0;q=((1<>>U}H[k+200>>2]=q;H[k+204>>2]=w;m=(n&(-1<>>10&1)<>2]=m}pa:{if(X){n=Ma(k+192|0);l=l-(h>>>15|0)|0;H[k+208>>2]=H[k+208>>2]-l;m=H[k+204>>2];q=H[k+200>>2];z=l&31;if((l&63)>>>0>=32){w=0;q=m>>>z|0}else{w=m>>>z|0;q=((1<>>z}H[k+200>>2]=q;H[k+204>>2]=w;q=n&(-1<>>11&1)<>2]=q+2<>2]=0}m=j+8|0;qa:{ra:{if(i&16){n=Ma(k+192|0);l=(i<<19>>31)+o|0;H[k+208>>2]=H[k+208>>2]-l;h=H[k+204>>2];q=H[k+200>>2];x=l&31;if((l&63)>>>0>=32){w=0;q=h>>>x|0}else{w=h>>>x|0;q=((1<>>x}H[k+200>>2]=q;H[k+204>>2]=w;l=(n&(-1<>>8&1)<>2]=l}sa:{if(i&32){n=Ma(k+192|0);l=(i<<18>>31)+o|0;H[k+208>>2]=H[k+208>>2]-l;h=H[k+204>>2];q=H[k+200>>2];x=l&31;if((l&63)>>>0>=32){w=0;q=h>>>x|0}else{w=h>>>x|0;q=((1<>>x}H[k+200>>2]=q;H[k+204>>2]=w;q=n&(-1<>>9&1)<>2]=q+2<>>0>q>>>0?n:q)|128;break sa}if(!(P&32)){break sa}H[m+(A<<2)>>2]=0}m=j+12|0;ta:{ua:{if(i&64){n=Ma(k+192|0);l=(i<<17>>31)+o|0;H[k+208>>2]=H[k+208>>2]-l;h=H[k+204>>2];q=H[k+200>>2];x=l&31;if((l&63)>>>0>=32){w=0;q=h>>>x|0}else{w=h>>>x|0;q=((1<>>x}H[k+200>>2]=q;H[k+204>>2]=w;l=(n&(-1<>>10&1)<>2]=l}c=c+2|0;va:{if(i&128){n=Ma(k+192|0);h=o-(i>>>15|0)|0;H[k+208>>2]=H[k+208>>2]-h;o=H[k+204>>2];q=H[k+200>>2];l=h&31;if((h&63)>>>0>=32){w=0;o=o>>>l|0}else{w=o>>>l|0;o=((1<>>l}H[k+200>>2]=o;H[k+204>>2]=w;i=n&(-1<>>11&1)<>2]=i+2<>>0<128){break va}H[m+(A<<2)>>2]=0}Z=Z^16;b=(V&4)+b|0;j=j+16|0;if((p|0)<(A|0)){continue}break}}wa:{if(!(R&2)|e>>>0<2){break wa}m=r&4;xa:{ya:{za:{Aa:{Ba:{if(aa){x=m?s:fa;z=0;if((A|0)<=0){break Ba}q=ha+(N(A,R-2|0)<<2)|0;while(1){i=lb(k+128|0);h=0;j=H[x>>2];if(j){h=q+(z<<2)|0;o=0;c=15;while(1){Ca:{if(!(c&j)){break Ca}n=c&286331153;if(n&j){H[h>>2]=W|H[h>>2]^((i^-1)&1)<>>1|0}if(j&n<<1){b=(A<<2)+h|0;H[b>>2]=W|H[b>>2]^((i^-1)&1)<>>1|0}if(j&n<<2){b=h+ma|0;H[b>>2]=W|H[b>>2]^((i^-1)&1)<>>1|0}if(!(j&n<<3)){break Ca}b=h+la|0;H[b>>2]=W|H[b>>2]^((i^-1)&1)<>>1|0}h=h+4|0;c=c<<4;o=o+1|0;if((o|0)!=8){continue}break}h=xe(j)}x=x+4|0;H[k+144>>2]=H[k+144>>2]-h;c=H[k+140>>2];b=H[k+136>>2];i=h&31;if((h&63)>>>0>=32){w=0;b=c>>>i|0}else{w=c>>>i|0;b=((1<>>i}H[k+136>>2]=b;H[k+140>>2]=w;z=z+8|0;if((A|0)>(z|0)){continue}break}}l=0;i=0;ba=m?ka:ja;h=ba;x=m?s:fa;c=x;if((A|0)>0){break za}b=!m;break Aa}ba=m?ka:ja;b=!m}if(R>>>0<=5){break wa}q=b?s:fa;if((A|0)<=0){break xa}c=b?ka:ja;break ya}while(1){b=i>>>28|0;i=H[c>>2];b=i|(b|i<<4|i>>>4);H[h>>2]=b;b=b|H[c+4>>2]<<28;H[h>>2]=(b>>>1&2004318071|b<<1&-286331154|b)&(i^-1);h=h+4|0;c=c+4|0;l=l+8|0;if((A|0)>(l|0)){continue}break}if(R>>>0<6){break wa}q=m?fa:s;c=m?ja:ka}b=0;o=0;h=x;m=c;i=c;c=q;while(1){j=h+4|0;n=H[i>>2];h=H[h>>2];if(!ia){n=n|(h|(h<<4|b>>>28|h>>>4|H[j>>2]<<28))<<3&-2004318072}H[i>>2]=n&(H[c>>2]^-1);c=c+4|0;i=i+4|0;b=h;h=j;o=o+8|0;if((A|0)>(o|0)){continue}break}if((A|0)<=0){break xa}U=ha+(N(A,R-6|0)<<2)|0;T=0;b=q;while(1){j=0;c=H[m>>2];if(c){X=A-T|0;R=(T<<2)+U|0;i=0;E=0;while(1){n=i;i=Ma(k+160|0);o=E+4|0;P=(A|0)>(o+T|0)?o:X;Da:{if((P|0)<=(E|0)){h=0;break Da}M=H[b>>2]^-1;w=E<<2;z=R+w|0;h=0;o=E;p=15<>>1|0}V=v<<1;if(V&c){if(i&1){j=j|V;c=M&116<<(o<<2)|c}h=h+1|0;i=i>>>1|0}V=v<<2;if(V&c){if(i&1){j=j|V;c=M&232<<(o<<2)|c}h=h+1|0;i=i>>>1|0}V=v<<3;if(!(V&c)){break Ea}if(i&1){j=j|V;c=M&192<<(o<<2)|c}h=h+1|0;i=i>>>1|0}l=l<<4;o=o+1|0;if((P|0)>(o|0)){continue}break}if(!(j>>>w&65535)){break Da}while(1){Fa:{if(!(j&p)){break Fa}l=p&286331153;if(l&j){H[z>>2]=t|(H[z>>2]|i<<31);h=h+1|0;i=i>>>1|0}if(l<<1&j){o=(A<<2)+z|0;H[o>>2]=t|(H[o>>2]|i<<31);h=h+1|0;i=i>>>1|0}if(l<<2&j){o=z+ma|0;H[o>>2]=t|(H[o>>2]|i<<31);h=h+1|0;i=i>>>1|0}if(!(l<<3&j)){break Fa}o=z+la|0;H[o>>2]=t|(H[o>>2]|i<<31);h=h+1|0;i=i>>>1|0}p=p<<4;z=z+4|0;E=E+1|0;if((P|0)>(E|0)){continue}break}}H[k+176>>2]=H[k+176>>2]-h;o=H[k+172>>2];i=H[k+168>>2];l=h&31;if((h&63)>>>0>=32){w=0;i=o>>>l|0}else{w=o>>>l|0;i=((1<>>l}H[k+168>>2]=i;H[k+172>>2]=w;i=1;E=4;if(!(n&1)){continue}break}H[m+4>>2]=H[m+4>>2]|(j>>>27&14|j>>>29|j>>>28)&(H[b+4>>2]^-1)}n=H[b>>2]|j;o=n>>>3&286331153;i=o>>>4|o<<4|o;if(T){c=ba-4|0;H[c>>2]=H[c>>2]|(H[x-4>>2]^-1)&o<<28}H[ba>>2]=H[ba>>2]|i&(H[x>>2]^-1);H[ba+4>>2]=H[ba+4>>2]|(H[x+4>>2]^-1)&n>>>31;m=m+4|0;b=b+4|0;ba=ba+4|0;x=x+4|0;T=T+8|0;if((A|0)>(T|0)){continue}break}}if(!ea){break wa}y(q,0,ea)}if((r|0)<($|0)){continue}break}}Ga:{if(e>>>0<2){break Ga}e=($&3)-1|0;Ha:{if(aa&e>>>0<2){if((A|0)<=0){break Ha}p=1<>2];if(u){h=j+(E<<2)|0;c=15;o=0;while(1){Ia:{if(!(c&u)){break Ia}m=c&286331153;if(m&u){H[h>>2]=p|H[h>>2]^((i^-1)&1)<>>1|0}if(u&m<<1){b=(A<<2)+h|0;H[b>>2]=p|H[b>>2]^((i^-1)&1)<>>1|0}if(u&m<<2){b=h+g|0;H[b>>2]=p|H[b>>2]^((i^-1)&1)<>>1|0}if(!(u&m<<3)){break Ia}b=h+n|0;H[b>>2]=p|H[b>>2]^((i^-1)&1)<>>1|0}h=h+4|0;c=c<<4;o=o+1|0;if((o|0)!=8){continue}break}h=xe(u)}q=q+4|0;H[k+144>>2]=H[k+144>>2]-h;c=H[k+140>>2];b=H[k+136>>2];i=h&31;if((h&63)>>>0>=32){w=0;b=c>>>i|0}else{w=c>>>i|0;b=((1<>>i}H[k+136>>2]=b;H[k+140>>2]=w;E=E+8|0;if((A|0)>(E|0)){continue}break}}if((A|0)<=0|e>>>0>1){break Ha}b=$&4;h=b?fa:s;c=b?ja:ka;l=0;i=0;while(1){b=i>>>28|0;i=H[h>>2];b=i|(b|i<<4|i>>>4);H[c>>2]=b;b=b|H[h+4>>2]<<28;H[c>>2]=(b>>>1&2004318071|b<<1&-286331154|b)&(i^-1);c=c+4|0;h=h+4|0;l=l+8|0;if((A|0)>(l|0)){continue}break}}b=($|0)>6?($-($+1&3)|0)-3|0:0;if(($|0)<=(b|0)){break Ga}r=N(A,12);R=A<<3;ea=3<>>0>=3){Z=-1;if((g|0)<5){break La}if(X){break Ja}g=b&4;h=g?fa:s;i=g?ja:ka;c=0;if(!ia){c=g?s:fa;l=0;j=0;while(1){g=j>>>28|0;Z=-1;j=H[c>>2];H[i>>2]=(H[i>>2]|(j|(g|j<<4|j>>>4|H[c+4>>2]<<28))<<3&-2004318072)&(H[h>>2]^-1);h=h+4|0;i=i+4|0;c=c+4|0;l=l+8|0;if((A|0)>(l|0)){continue}break}break Ka}while(1){Z=-1;H[i>>2]=H[i>>2]&(H[h>>2]^-1);h=h+4|0;i=i+4|0;c=c+8|0;if((A|0)>(c|0)){continue}break}break Ka}Z=H[(c<<2)+20812>>2]}if(X){break Ja}}c=b&4;q=c?fa:s;g=c?ja:ka;x=c?s:fa;T=c?ka:ja;V=ha+(N(b,A)<<2)|0;p=0;while(1){j=0;c=H[g>>2]&Z;if(c){aa=A-p|0;u=V+(p<<2)|0;i=0;e=0;while(1){n=i;i=Ma(k+160|0);o=e+4|0;w=(A|0)>(o+p|0)?o:aa;Ma:{if((w|0)<=(e|0)){h=0;break Ma}E=e<<2;z=E+u|0;v=(H[q>>2]^-1)&Z;h=0;o=e;P=15<>>1|0}m=U<<1;if(m&c){if(i&1){j=j|m;c=v&116<<(o<<2)|c}h=h+1|0;i=i>>>1|0}m=U<<2;if(m&c){if(i&1){j=j|m;c=v&232<<(o<<2)|c}h=h+1|0;i=i>>>1|0}m=U<<3;if(!(m&c)){break Na}if(i&1){j=j|m;c=v&192<<(o<<2)|c}h=h+1|0;i=i>>>1|0}l=l<<4;o=o+1|0;if((w|0)>(o|0)){continue}break}if(!(j>>>E&65535)){break Ma}while(1){Oa:{if(!(j&P)){break Oa}m=P&286331153;if(m&j){H[z>>2]=ea|(H[z>>2]|i<<31);h=h+1|0;i=i>>>1|0}if(m<<1&j){o=(A<<2)+z|0;H[o>>2]=ea|(H[o>>2]|i<<31);h=h+1|0;i=i>>>1|0}if(m<<2&j){o=z+R|0;H[o>>2]=ea|(H[o>>2]|i<<31);h=h+1|0;i=i>>>1|0}if(!(m<<3&j)){break Oa}o=r+z|0;H[o>>2]=ea|(H[o>>2]|i<<31);h=h+1|0;i=i>>>1|0}P=P<<4;z=z+4|0;e=e+1|0;if((w|0)>(e|0)){continue}break}}H[k+176>>2]=H[k+176>>2]-h;o=H[k+172>>2];i=H[k+168>>2];e=h&31;if((h&63)>>>0>=32){w=0;i=o>>>e|0}else{w=o>>>e|0;i=((1<>>e}H[k+168>>2]=i;H[k+172>>2]=w;i=1;e=4;if(!(n&1)){continue}break}H[g+4>>2]=H[g+4>>2]|(j>>>27&14|j>>>29|j>>>28)&(H[q+4>>2]^-1)}n=H[q>>2]|j;o=n>>>3&286331153;i=o>>>4|o<<4|o;if(p){c=T-4|0;H[c>>2]=H[c>>2]|(H[x-4>>2]^-1)&o<<28}H[T>>2]=H[T>>2]|i&(H[x>>2]^-1);H[T+4>>2]=H[T+4>>2]|(H[x+4>>2]^-1)&n>>>31;g=g+4|0;q=q+4|0;T=T+4|0;x=x+4|0;p=p+8|0;if((A|0)>(p|0)){continue}break}}b=b+4|0;if(($|0)>(b|0)){continue}break}}i=1;if(($|0)<=0|(A|0)<=0){break j}n=A&2147483644;o=A&3;q=ca-D>>>0>4294967292;b=0;while(1){i=ha+(N(b,A)<<2)|0;h=0;Pa:{if(!q){while(1){g=H[i>>2];c=g&2147483647;H[i>>2]=(g|0)<0?0-c|0:c;g=H[i+4>>2];c=g&2147483647;H[i+4>>2]=(g|0)<0?0-c|0:c;g=H[i+8>>2];c=g&2147483647;H[i+8>>2]=(g|0)<0?0-c|0:c;g=H[i+12>>2];c=g&2147483647;H[i+12>>2]=(g|0)<0?0-c|0:c;i=i+16|0;h=h+4|0;if((n|0)!=(h|0)){continue}break}if(!o){break Pa}}h=0;while(1){g=H[i>>2];c=g&2147483647;H[i>>2]=(g|0)<0?0-c|0:c;i=i+4|0;h=h+1|0;if((o|0)!=(h|0)){continue}break}}i=1;b=b+1|0;if(($|0)!=(b|0)){continue}break}break j}if(!_){break w}H[k+52>>2]=H[C+24>>2];H[k+48>>2]=P;Ba(S,1,9686,k+48|0);break v}H[k+20>>2]=i;H[k+16>>2]=P;Ba(S,1,9686,k+16|0);i=0;break j}i=0}na=k+304|0;if(i){break i}break b}H[f+108>>2]=(b<<9)+22336;c=0;b=H[f+116>>2];Qa:{Ra:{m=H[C+16>>2]-H[C+8>>2]|0;e=H[C+20>>2]-H[C+12>>2]|0;g=N(m,e);Sa:{Ta:{Ua:{if(g>>>0>K[f+132>>2]){Ca(b);b=Ia(g<<2);H[f+116>>2]=b;if(!b){break Sa}H[f+132>>2]=g;break Ua}if(!b){break Ta}}g=g<<2;if(!g){break Ta}y(b,0,g)}b=H[f+120>>2];l=m+2|0;o=e+3>>>2|0;g=N(l,o+2|0);if(g>>>0<=K[f+136>>2]){z=g<<2;break Ra}Ca(b);z=g<<2;b=Ia(z);H[f+120>>2]=b;if(b){break Ra}}b=0;break Qa}H[f+136>>2]=g;if(z){y(b,0,z)}Va:{if(!l){break Va}k=l&7;n=H[f+120>>2];b=n;q=m+1|0;Wa:{if(q>>>0>=7){g=l&-8;while(1){H[b+24>>2]=1226833920;H[b+28>>2]=1226833920;H[b+16>>2]=1226833920;H[b+20>>2]=1226833920;H[b+8>>2]=1226833920;H[b+12>>2]=1226833920;H[b>>2]=1226833920;H[b+4>>2]=1226833920;b=b+32|0;c=c+8|0;if((g|0)!=(c|0)){continue}break}if(!k){break Wa}}c=0;while(1){H[b>>2]=1226833920;b=b+4|0;c=c+1|0;if((k|0)!=(c|0)){continue}break}}k=l&7;b=n+(N(l,o+1|0)<<2)|0;Xa:{if(q>>>0>=7){g=l&-8;c=0;while(1){H[b+24>>2]=1226833920;H[b+28>>2]=1226833920;H[b+16>>2]=1226833920;H[b+20>>2]=1226833920;H[b+8>>2]=1226833920;H[b+12>>2]=1226833920;H[b>>2]=1226833920;H[b+4>>2]=1226833920;b=b+32|0;c=c+8|0;if((g|0)!=(c|0)){continue}break}if(!k){break Xa}}c=0;while(1){H[b>>2]=1226833920;b=b+4|0;c=c+1|0;if((k|0)!=(c|0)){continue}break}}b=e&3;if(!b){break Va}k=(b|0)==1?1224736768:(b|0)==2?1207959552:1073741824;g=l&7;b=n+(N(l,o)<<2)|0;if(q>>>0>=7){c=l&-8;z=0;while(1){H[b+28>>2]=k;H[b+24>>2]=k;H[b+20>>2]=k;H[b+16>>2]=k;H[b+12>>2]=k;H[b+8>>2]=k;H[b+4>>2]=k;H[b>>2]=k;b=b+32|0;z=z+8|0;if((c|0)!=(z|0)){continue}break}if(!g){break Va}}z=0;while(1){H[b>>2]=k;b=b+4|0;z=z+1|0;if((g|0)!=(z|0)){continue}break}}H[f+128>>2]=e;H[f+124>>2]=m;b=1}if(!b){break b}z=j+H[C+28>>2]|0;if((z|0)>=31){if(!_){break h}H[Y+16>>2]=z;Ba(S,2,8716,Y+16|0);break b}Yb(f);Xa(f,18,46);Xa(f,17,3);Xa(f,0,4);if(H[C+64>>2]){break i}c=H[C+52>>2];Ya:{if(!(c>>>0<=1&(!H[f+144>>2]|(c|0)!=1))){k=c&3;b=H[C+4>>2];g=0;Za:{if(c-1>>>0>=3){c=c&-4;while(1){q=(t<<3)+b|0;g=H[q+28>>2]+(H[q+20>>2]+(H[q+12>>2]+(H[q+4>>2]+g|0)|0)|0)|0;t=t+4|0;h=h+4|0;if((c|0)!=(h|0)){continue}break}if(!k){break Za}}while(1){g=H[((t<<3)+b|0)+4>>2]+g|0;t=t+1|0;i=i+1|0;if((k|0)!=(i|0)){continue}break}}ia=H[f+148>>2];c=g+2|0;if(c>>>0>K[f+152>>2]){b=Ha(ia,c);if(!b){break b}H[f+148>>2]=b;b=b+g|0;F[b|0]=0;F[b+1|0]=0;H[f+152>>2]=c;ia=H[f+148>>2];if(!H[C+52>>2]){break Ya}b=H[C+4>>2]}g=0;t=0;while(1){k=t<<3;c=k+b|0;b=H[c+4>>2];if(b){B(g+ia|0,H[c>>2],b)}b=H[C+4>>2];g=H[(k+b|0)+4>>2]+g|0;t=t+1|0;if(t>>>0>2]){continue}break}break Ya}if((c|0)!=1){break i}ia=H[H[C+4>>2]>>2]}b=H[C+60>>2];if(b){Z=H[f+116>>2];H[f+116>>2]=b}if(H[C+44>>2]){P=V&8;ca=f+28|0;ha=!(V&2);la=2;while(1){k=U+ia|0;ma=H[C>>2]+N(X,24)|0;c=H[ma>>2];ea=V&(la>>>0<2&(H[C+28>>2]-4|0)>=(z|0));_a:{if(ea){H[f+20>>2]=k;b=c+k|0;H[f+24>>2]=b;G[f+112>>1]=I[b|0]|I[b+1|0]<<8;F[b|0]=255;F[H[f+24>>2]+1|0]=255;H[f+8>>2]=0;H[f>>2]=0;H[f+16>>2]=k;break _a}H[f+20>>2]=k;b=c+k|0;H[f+24>>2]=b;G[f+112>>1]=I[b|0]|I[b+1|0]<<8;F[b|0]=255;F[H[f+24>>2]+1|0]=255;H[f+104>>2]=f+28;H[f+16>>2]=k;H[f+12>>2]=0;b=c?I[k|0]<<16:16711680;H[f>>2]=b;i=1;c=k+1|0;g=I[k+1|0];$a:{if(I[k|0]==255){if(g>>>0>=144){H[f+12>>2]=1;b=b|65280;break $a}H[f+16>>2]=c;i=0;b=b+(g<<9)|0;break $a}H[f+16>>2]=c;b=b|g<<8}H[f+8>>2]=i;H[f+4>>2]=32768;H[f>>2]=b<<7}R=H[ma>>2];ab:{if(!H[ma+8>>2]|(z|0)<=0){break ab}aa=ea|ha;ba=0;while(1){bb:{cb:{db:{eb:{switch(la-1|0){default:if(!ea){break db}b=1<>>1|b;e=H[f+124>>2];d=e<<2;b=(d+H[f+120>>2]|0)+12|0;g=H[f+116>>2];l=0;c=H[f+128>>2];if(c>>>0>=4){if(!e){break bb}o=N(e,12);n=e<<3;j=0-m|0;while(1){c=0;while(1){k=b;b=H[b>>2];fb:{if(!b){break fb}if(!(!(b&495)|b&2097168)){b=H[f>>2];i=H[f+8>>2];gb:{if(i){break gb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];hb:{if(i){i=8}else{if(b>>>0>143){break hb}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break gb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;ib:{if(!(b>>>i&1)){break ib}jb:{if(i){break jb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];kb:{if(i){i=8}else{if(b>>>0>143){break kb}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break jb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;q=b>>>i&1;H[g>>2]=q?j:m;i=H[f+124>>2];b=k-4|0;H[b>>2]=H[b>>2]|32;H[k+4>>2]=H[k+4>>2]|8;H[k>>2]=H[k>>2]|q<<19|16;if(P){break ib}b=k+(-2-i<<2)|0;H[b+4>>2]=H[b+4>>2]|32768;H[b>>2]=H[b>>2]|q<<31|65536;b=b-4|0;H[b>>2]=H[b>>2]|131072}b=H[k>>2]|2097152;H[k>>2]=b}if(!(!(b&3960)|b&16777344)){b=H[f>>2];i=H[f+8>>2];lb:{if(i){break lb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];mb:{if(i){i=8}else{if(b>>>0>143){break mb}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break lb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;if(b>>>i&1){nb:{if(i){break nb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];ob:{if(i){i=8}else{if(b>>>0>143){break ob}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break nb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;i=b>>>i&1;H[d+g>>2]=i?j:m;b=k-4|0;H[b>>2]=H[b>>2]|256;H[k+4>>2]=H[k+4>>2]|64;b=H[k>>2]|i<<22|128}else{b=H[k>>2]}b=b|16777216;H[k>>2]=b}if(!(!(b&31680)|b&134218752)){b=H[f>>2];i=H[f+8>>2];pb:{if(i){break pb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];qb:{if(i){i=8}else{if(b>>>0>143){break qb}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break pb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;if(b>>>i&1){rb:{if(i){break rb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];sb:{if(i){i=8}else{if(b>>>0>143){break sb}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break rb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;i=b>>>i&1;H[g+n>>2]=i?j:m;b=k-4|0;H[b>>2]=H[b>>2]|2048;H[k+4>>2]=H[k+4>>2]|512;b=H[k>>2]|i<<25|1024}else{b=H[k>>2]}b=b|134217728;H[k>>2]=b}if(!(b&253440)|b&1073750016){break fb}b=H[f>>2];i=H[f+8>>2];tb:{if(i){break tb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];ub:{if(i){i=8}else{if(b>>>0>143){break ub}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break tb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;if(b>>>i&1){vb:{if(i){break vb}i=(b|0)!=255;q=H[f+16>>2];b=I[q|0];wb:{if(i){i=8}else{if(b>>>0>143){break wb}i=7}H[f>>2]=b;H[f+16>>2]=q+1;break vb}i=8;b=255}i=i-1|0;H[f+8>>2]=i;q=b>>>i&1;H[g+o>>2]=q?j:m;i=H[f+124>>2];b=k-4|0;H[b>>2]=H[b>>2]|16384;H[k+4>>2]=H[k+4>>2]|4096;H[k>>2]=H[k>>2]|q<<28|8192;b=k+(i<<2)|0;H[b+4>>2]=H[b+4>>2]|4;H[b+12>>2]=H[b+12>>2]|1;H[b+8>>2]=H[b+8>>2]|q<<18|2}H[k>>2]=H[k>>2]|1073741824}g=g+4|0;b=k+4|0;c=c+1|0;if((e|0)!=(c|0)){continue}break}g=g+o|0;b=k+12|0;l=l+4|0;c=H[f+128>>2];if(l>>>0<(c&-4)>>>0){continue}break}}if(!e|c>>>0<=l>>>0){break cb}p=0;n=0-m|0;i=c;while(1){xb:{if((i|0)==(l|0)){i=l;break xb}d=b-4|0;j=H[b>>2];t=0;while(1){o=N(t,3);k=j>>>o|0;if(!(k&2097168|!(k&495))){c=H[f>>2];h=H[f+8>>2];yb:{if(h){break yb}i=(c|0)!=255;k=H[f+16>>2];c=I[k|0];zb:{if(i){h=8}else{if(c>>>0>143){break zb}h=7}H[f>>2]=c;H[f+16>>2]=k+1;break yb}h=8;c=255}h=h-1|0;H[f+8>>2]=h;Ab:{if(!(c>>>h&1)){break Ab}q=(N(e,t)<<2)+g|0;Bb:{if(h){break Bb}i=(c|0)!=255;k=H[f+16>>2];c=I[k|0];Cb:{if(i){h=8}else{if(c>>>0>143){break Cb}h=7}H[f>>2]=c;H[f+16>>2]=k+1;break Bb}h=8;c=255}k=h-1|0;H[f+8>>2]=k;i=c>>>k&1;H[q>>2]=i?n:m;k=H[f+124>>2];H[d>>2]=H[d>>2]|32<>2]=H[b>>2]|(i<<19|16)<>2]=H[b+4>>2]|8<>2]=H[c+4>>2]|32768;H[c>>2]=H[c>>2]|i<<31|65536;c=c-4|0;H[c>>2]=H[c>>2]|131072}if((t|0)!=3){break Ab}c=(k<<2)+b|0;H[c+4>>2]=H[c+4>>2]|4;H[c+12>>2]=H[c+12>>2]|1;H[c+8>>2]=H[c+8>>2]|i<<18|2}j=H[b>>2]|2097152<>2]=j;c=H[f+128>>2]}i=c;t=t+1|0;if(t>>>0>>0){continue}break}}g=g+4|0;b=b+4|0;p=p+1|0;if((e|0)!=(p|0)){continue}break};break cb;case 1:m=0;x=0;Db:{Eb:{Fb:{W=H[f+124>>2];if(!((W|0)!=64|H[f+128>>2]!=64)){b=1<>>1|b;u=0-s|0;q=f+100|0;k=f+96|0;w=f+28|0;g=H[f+120>>2]+268|0;e=H[f+8>>2];b=H[f+4>>2];d=H[f>>2];i=H[f+104>>2];c=H[f+116>>2];if(V&8){break Fb}while(1){r=0;while(1){o=c;j=g;g=H[g>>2];Gb:{Hb:{Ib:{if(!g){i=H[k>>2];g=H[i>>2];b=b-g|0;Jb:{if(d>>>16>>>0>>0){n=H[i+4>>2];c=b>>>0>>0;H[k>>2]=H[i+(c?8:12)>>2];while(1){Kb:{if(e){break Kb}i=H[f+16>>2];b=i+1|0;h=I[i+1|0];if(I[i|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Kb}H[f+16>>2]=b;d=(h<<9)+d|0;e=7;break Kb}H[f+16>>2]=b;e=8;d=(h<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;c=c?n:!n;break Jb}d=d-(g<<16)|0;if(!(b&32768)){n=H[i+4>>2];c=b>>>0>>0;H[k>>2]=H[i+(c?12:8)>>2];while(1){Lb:{if(e){break Lb}i=H[f+16>>2];g=i+1|0;h=I[i+1|0];if(I[i|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Lb}H[f+16>>2]=g;d=(h<<9)+d|0;e=7;break Lb}H[f+16>>2]=g;e=8;d=(h<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!n:n;break Jb}c=H[i+4>>2]}if(!c){i=k;break Gb}c=H[q>>2];g=H[c>>2];b=b-g|0;Mb:{if(d>>>16>>>0>>0){h=H[c+4>>2];i=b>>>0>>0;c=H[(i?8:12)+c>>2];H[q>>2]=c;while(1){Nb:{if(e){break Nb}n=H[f+16>>2];b=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Nb}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break Nb}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;n=i?h:!h;break Mb}d=d-(g<<16)|0;if(!(b&32768)){h=H[c+4>>2];g=b>>>0>>0;c=H[(g?12:8)+c>>2];H[q>>2]=c;while(1){Ob:{if(e){break Ob}n=H[f+16>>2];i=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Ob}H[f+16>>2]=i;d=(m<<9)+d|0;e=7;break Ob}H[f+16>>2]=i;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=g?!h:h;break Mb}n=H[c+4>>2]}g=H[c>>2];b=b-g|0;Pb:{if(d>>>16>>>0>>0){h=H[c+4>>2];i=c;c=b>>>0>>0;H[q>>2]=H[i+(c?8:12)>>2];while(1){Qb:{if(e){break Qb}i=H[f+16>>2];b=i+1|0;m=I[i+1|0];if(I[i|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Qb}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break Qb}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;c=c?h:!h;break Pb}d=d-(g<<16)|0;if(!(b&32768)){h=H[c+4>>2];i=c;c=b>>>0>>0;H[q>>2]=H[i+(c?12:8)>>2];while(1){Rb:{if(e){break Rb}i=H[f+16>>2];g=i+1|0;m=I[i+1|0];if(I[i|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Rb}H[f+16>>2]=g;d=(m<<9)+d|0;e=7;break Rb}H[f+16>>2]=g;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break Pb}c=H[c+4>>2]}g=0;i=q;Sb:{Tb:{Ub:{Vb:{Wb:{switch(c|n<<1){case 0:m=j-4|0;i=H[j+4>>2]>>>17&4|H[m>>2]>>>19&1;c=w+(I[i+24384|0]<<2)|0;n=H[c>>2];g=H[n>>2];b=b-g|0;Xb:{if(d>>>16>>>0>>0){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?8:12)>>2];while(1){Yb:{if(e){break Yb}n=H[f+16>>2];b=n+1|0;l=I[n+1|0];if(I[n|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Yb}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Yb}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;n=c?h:!h;break Xb}d=d-(g<<16)|0;if(!(b&32768)){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?12:8)>>2];while(1){Zb:{if(e){break Zb}n=H[f+16>>2];g=n+1|0;l=I[n+1|0];if(I[n|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Zb}H[f+16>>2]=g;d=(l<<9)+d|0;e=7;break Zb}H[f+16>>2]=g;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!h:h;break Xb}n=H[n+4>>2]}g=I[i+24640|0];H[o>>2]=(n|0)==(g|0)?s:u;H[m>>2]=H[m>>2]|32;H[j+4>>2]=H[j+4>>2]|8;c=j-268|0;H[c>>2]=H[c>>2]|131072;c=j-260|0;H[c>>2]=H[c>>2]|32768;c=j-264|0;i=c;v=H[c>>2];c=g^n;H[i>>2]=v|c<<31|65536;i=c<<19;t=H[f+108>>2];c=w+(I[t+2|0]<<2)|0;n=H[c>>2];g=H[n>>2];b=b-g|0;_b:{if(d>>>16>>>0>>0){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?8:12)>>2];while(1){$b:{if(e){break $b}n=H[f+16>>2];b=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break $b}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break $b}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;c=c?h:!h;break _b}d=d-(g<<16)|0;if(!(b&32768)){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?12:8)>>2];while(1){ac:{if(e){break ac}n=H[f+16>>2];g=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break ac}H[f+16>>2]=g;d=(m<<9)+d|0;e=7;break ac}H[f+16>>2]=g;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break _b}c=H[n+4>>2]}g=i|16;if(!c){break Vb}break;case 1:break Wb;case 2:break Ub;case 3:break Sb;default:break Hb}}m=j-4|0;n=H[j+4>>2]>>>20&4|(H[m>>2]>>>22&1|(g>>>15&16|(g>>>19&64|g>>>3&170)));i=w+(I[n+24384|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;bc:{if(d>>>16>>>0>>0){h=H[l+4>>2];v=i;i=b>>>0>>0;H[v>>2]=H[l+(i?8:12)>>2];while(1){cc:{if(e){break cc}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break cc}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break cc}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;i=i?h:!h;break bc}d=d-(c<<16)|0;if(!(b&32768)){h=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){dc:{if(e){break dc}e=H[f+16>>2];i=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break dc}H[f+16>>2]=i;d=(l<<9)+d|0;e=7;break dc}H[f+16>>2]=i;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}i=c?!h:h;break bc}i=H[l+4>>2]}c=I[n+24640|0];H[o+256>>2]=(i|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|256;H[j+4>>2]=H[j+4>>2]|64;t=H[f+108>>2];g=(c^i)<<22|g|128}i=w+(I[(g>>>6&495)+t|0]<<2)|0;n=H[i>>2];c=H[n>>2];b=b-c|0;ec:{if(d>>>16>>>0>>0){h=H[n+4>>2];v=i;i=b>>>0>>0;H[v>>2]=H[n+(i?8:12)>>2];while(1){fc:{if(e){break fc}n=H[f+16>>2];b=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break fc}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break fc}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=i?h:!h;break ec}d=d-(c<<16)|0;if(!(b&32768)){h=H[n+4>>2];c=b>>>0>>0;H[i>>2]=H[n+(c?12:8)>>2];while(1){gc:{if(e){break gc}n=H[f+16>>2];i=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break gc}H[f+16>>2]=i;d=(m<<9)+d|0;e=7;break gc}H[f+16>>2]=i;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break ec}c=H[n+4>>2]}if(!c){break Tb}}m=j-4|0;n=H[j+4>>2]>>>23&4|(H[m>>2]>>>25&1|(g>>>18&16|(g>>>22&64|g>>>6&170)));i=w+(I[n+24384|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;hc:{if(d>>>16>>>0>>0){h=H[l+4>>2];v=i;i=b>>>0>>0;H[v>>2]=H[l+(i?8:12)>>2];while(1){ic:{if(e){break ic}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break ic}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break ic}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;i=i?h:!h;break hc}d=d-(c<<16)|0;if(!(b&32768)){h=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){jc:{if(e){break jc}e=H[f+16>>2];i=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break jc}H[f+16>>2]=i;d=(l<<9)+d|0;e=7;break jc}H[f+16>>2]=i;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}i=c?!h:h;break hc}i=H[l+4>>2]}c=I[n+24640|0];H[o+512>>2]=(i|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;g=(c^i)<<25|g|1024;t=H[f+108>>2]}i=w+(I[(g>>>9&495)+t|0]<<2)|0;m=H[i>>2];c=H[m>>2];b=b-c|0;kc:{if(d>>>16>>>0>>0){h=H[m+4>>2];n=b>>>0>>0;H[i>>2]=H[m+(n?8:12)>>2];while(1){lc:{if(e){break lc}e=H[f+16>>2];b=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break lc}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break lc}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?h:!h;break kc}d=d-(c<<16)|0;if(!(b&32768)){h=H[m+4>>2];c=b>>>0>>0;H[i>>2]=H[m+(c?12:8)>>2];while(1){mc:{if(e){break mc}e=H[f+16>>2];n=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break mc}H[f+16>>2]=n;d=(m<<9)+d|0;e=7;break mc}H[f+16>>2]=n;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break kc}c=H[m+4>>2]}if(!c){break Hb}}m=j-4|0;p=H[j+4>>2]>>>26&4|(H[m>>2]>>>28&1|(g>>>21&16|(g>>>25&64|g>>>9&170)));i=w+(I[p+24384|0]<<2)|0;t=H[i>>2];c=H[t>>2];b=b-c|0;break Ib}nc:{if(g&2097168){break nc}i=w+(I[H[f+108>>2]+(g&495)|0]<<2)|0;m=H[i>>2];c=H[m>>2];b=b-c|0;oc:{if(d>>>16>>>0>>0){h=H[m+4>>2];n=b>>>0>>0;H[i>>2]=H[m+(n?8:12)>>2];while(1){pc:{if(e){break pc}e=H[f+16>>2];b=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break pc}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break pc}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?h:!h;break oc}d=d-(c<<16)|0;if(!(b&32768)){h=H[m+4>>2];c=b>>>0>>0;H[i>>2]=H[m+(c?12:8)>>2];while(1){qc:{if(e){break qc}e=H[f+16>>2];n=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break qc}H[f+16>>2]=n;d=(m<<9)+d|0;e=7;break qc}H[f+16>>2]=n;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break oc}c=H[m+4>>2]}if(!c){break nc}l=j-4|0;h=H[j+4>>2]>>>17&4|(H[l>>2]>>>19&1|(g>>>14&16|(g>>>16&64|g&170)));i=w+(I[h+24384|0]<<2)|0;p=H[i>>2];c=H[p>>2];b=b-c|0;rc:{if(d>>>16>>>0>>0){m=H[p+4>>2];n=b>>>0>>0;H[i>>2]=H[p+(n?8:12)>>2];while(1){sc:{if(e){break sc}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break sc}H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break sc}H[f+16>>2]=b;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;m=n?m:!m;break rc}d=d-(c<<16)|0;if(!(b&32768)){m=H[p+4>>2];c=b>>>0>>0;H[i>>2]=H[p+(c?12:8)>>2];while(1){tc:{if(e){break tc}e=H[f+16>>2];n=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break tc}H[f+16>>2]=n;d=(p<<9)+d|0;e=7;break tc}H[f+16>>2]=n;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}m=c?!m:m;break rc}m=H[p+4>>2]}n=I[h+24640|0];H[o>>2]=(m|0)==(n|0)?s:u;H[l>>2]=H[l>>2]|32;H[j+4>>2]=H[j+4>>2]|8;c=j-268|0;H[c>>2]=H[c>>2]|131072;c=j-260|0;H[c>>2]=H[c>>2]|32768;c=j-264|0;v=c;M=H[c>>2];c=m^n;H[v>>2]=M|c<<31|65536;g=c<<19|g|16}uc:{if(g&16777344){break uc}h=g>>>3|0;i=w+(I[H[f+108>>2]+(h&495)|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;vc:{if(d>>>16>>>0>>0){m=H[l+4>>2];n=b>>>0>>0;H[i>>2]=H[l+(n?8:12)>>2];while(1){wc:{if(e){break wc}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break wc}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break wc}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?m:!m;break vc}d=d-(c<<16)|0;if(!(b&32768)){m=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){xc:{if(e){break xc}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break xc}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break xc}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!m:m;break vc}c=H[l+4>>2]}if(!c){break uc}l=j-4|0;h=H[j+4>>2]>>>20&4|(H[l>>2]>>>22&1|(g>>>15&16|(g>>>19&64|h&170)));i=w+(I[h+24384|0]<<2)|0;p=H[i>>2];c=H[p>>2];b=b-c|0;yc:{if(d>>>16>>>0>>0){m=H[p+4>>2];n=b>>>0>>0;H[i>>2]=H[p+(n?8:12)>>2];while(1){zc:{if(e){break zc}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break zc}H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break zc}H[f+16>>2]=b;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?m:!m;break yc}d=d-(c<<16)|0;if(!(b&32768)){m=H[p+4>>2];c=b>>>0>>0;H[i>>2]=H[p+(c?12:8)>>2];while(1){Ac:{if(e){break Ac}e=H[f+16>>2];n=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Ac}H[f+16>>2]=n;d=(p<<9)+d|0;e=7;break Ac}H[f+16>>2]=n;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!m:m;break yc}n=H[p+4>>2]}c=I[h+24640|0];H[o+256>>2]=(n|0)==(c|0)?s:u;H[l>>2]=H[l>>2]|256;H[j+4>>2]=H[j+4>>2]|64;g=(c^n)<<22|g|128}Bc:{if(g&134218752){break Bc}h=g>>>6|0;i=w+(I[H[f+108>>2]+(h&495)|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;Cc:{if(d>>>16>>>0>>0){m=H[l+4>>2];n=b>>>0>>0;H[i>>2]=H[l+(n?8:12)>>2];while(1){Dc:{if(e){break Dc}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Dc}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Dc}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?m:!m;break Cc}d=d-(c<<16)|0;if(!(b&32768)){m=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){Ec:{if(e){break Ec}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Ec}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break Ec}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!m:m;break Cc}c=H[l+4>>2]}if(!c){break Bc}l=j-4|0;h=H[j+4>>2]>>>23&4|(H[l>>2]>>>25&1|(g>>>18&16|(g>>>22&64|h&170)));i=w+(I[h+24384|0]<<2)|0;p=H[i>>2];c=H[p>>2];b=b-c|0;Fc:{if(d>>>16>>>0>>0){m=H[p+4>>2];n=b>>>0>>0;H[i>>2]=H[p+(n?8:12)>>2];while(1){Gc:{if(e){break Gc}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Gc}H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Gc}H[f+16>>2]=b;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?m:!m;break Fc}d=d-(c<<16)|0;if(!(b&32768)){m=H[p+4>>2];c=b>>>0>>0;H[i>>2]=H[p+(c?12:8)>>2];while(1){Hc:{if(e){break Hc}e=H[f+16>>2];n=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Hc}H[f+16>>2]=n;d=(p<<9)+d|0;e=7;break Hc}H[f+16>>2]=n;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!m:m;break Fc}n=H[p+4>>2]}c=I[h+24640|0];H[o+512>>2]=(n|0)==(c|0)?s:u;H[l>>2]=H[l>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;g=(c^n)<<25|g|1024}if(g&1073750016){break Hb}h=g>>>9|0;i=w+(I[H[f+108>>2]+(h&495)|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;Ic:{if(d>>>16>>>0>>0){m=H[l+4>>2];n=b>>>0>>0;H[i>>2]=H[l+(n?8:12)>>2];while(1){Jc:{if(e){break Jc}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Jc}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Jc}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?m:!m;break Ic}d=d-(c<<16)|0;if(!(b&32768)){m=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){Kc:{if(e){break Kc}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Kc}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break Kc}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!m:m;break Ic}c=H[l+4>>2]}if(!c){break Hb}m=j-4|0;p=H[j+4>>2]>>>26&4|(H[m>>2]>>>28&1|(g>>>21&16|(g>>>25&64|h&170)));i=w+(I[p+24384|0]<<2)|0;t=H[i>>2];c=H[t>>2];b=b-c|0}Lc:{if(d>>>16>>>0>>0){h=H[t+4>>2];n=b>>>0>>0;H[i>>2]=H[(n?8:12)+t>>2];while(1){Mc:{if(e){break Mc}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Mc}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Mc}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?h:!h;break Lc}d=d-(c<<16)|0;if(!(b&32768)){h=H[t+4>>2];c=b>>>0>>0;H[i>>2]=H[(c?12:8)+t>>2];while(1){Nc:{if(e){break Nc}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Nc}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break Nc}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!h:h;break Lc}n=H[t+4>>2]}c=I[p+24640|0];H[o+768>>2]=(n|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|16384;H[j+4>>2]=H[j+4>>2]|4096;H[j+260>>2]=H[j+260>>2]|4;H[j+268>>2]=H[j+268>>2]|1;c=c^n;H[j+264>>2]=H[j+264>>2]|c<<18|2;g=c<<28|g|8192}H[j>>2]=g&-1226833921}g=j+4|0;c=o+4|0;r=r+1|0;if((r|0)!=64){continue}break}g=j+12|0;c=o+772|0;n=x>>>0<60;x=x+4|0;if(n){continue}break}break Eb}b=1<>>1|b;k=H[f+120>>2];c=(k+(W<<2)|0)+12|0;g=H[f+128>>2];e=H[f+8>>2];b=H[f+4>>2];d=H[f>>2];i=H[f+104>>2];o=H[f+116>>2];if(V&8){Oc:{if(g>>>0<4){break Oc}if(W){n=f+100|0;q=f+96|0;r=N(W,12);u=W<<3;v=0-M|0;D=f+28|0;while(1){w=0;while(1){j=c;c=H[c>>2];Pc:{Qc:{Rc:{if(c){Sc:{if(c&2097168){break Sc}i=D+(I[H[f+108>>2]+(c&495)|0]<<2)|0;l=H[i>>2];g=H[l>>2];b=b-g|0;Tc:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[l+4>>2];if(b&32768){break Tc}h=H[l+4>>2];g=b>>>0>>0;H[i>>2]=H[l+(g?12:8)>>2];while(1){Uc:{if(e){break Uc}e=H[f+16>>2];k=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break Uc}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break Uc}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!h:h;break Tc}h=H[l+4>>2];k=b>>>0>>0;H[i>>2]=H[l+(k?8:12)>>2];while(1){Vc:{if(e){break Vc}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Vc}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Vc}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?h:!h}if(!k){break Sc}p=j-4|0;h=H[j+4>>2]>>>17&4|(H[p>>2]>>>19&1|(c>>>14&16|(c>>>16&64|c&170)));i=D+(I[h+24384|0]<<2)|0;s=H[i>>2];g=H[s>>2];b=b-g|0;Wc:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[s+4>>2];if(b&32768){break Wc}l=H[s+4>>2];g=b>>>0>>0;H[i>>2]=H[s+(g?12:8)>>2];while(1){Xc:{if(e){break Xc}e=H[f+16>>2];k=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(s<<8)+d|0;break Xc}if(s>>>0<=143){H[f+16>>2]=k;d=(s<<9)+d|0;e=7;break Xc}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break Wc}l=H[s+4>>2];k=b>>>0>>0;H[i>>2]=H[s+(k?8:12)>>2];while(1){Yc:{if(e){break Yc}e=H[f+16>>2];b=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(s<<8)+d|0;break Yc}if(s>>>0<=143){H[f+16>>2]=b;d=(s<<9)+d|0;e=7;break Yc}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}g=I[h+24640|0];H[o>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|32;H[j+4>>2]=H[j+4>>2]|8;c=(g^k)<<19|c|16}Zc:{if(c&16777344){break Zc}h=c>>>3|0;i=D+(I[H[f+108>>2]+(h&495)|0]<<2)|0;p=H[i>>2];g=H[p>>2];b=b-g|0;_c:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[p+4>>2];if(b&32768){break _c}l=H[p+4>>2];g=b>>>0>>0;H[i>>2]=H[p+(g?12:8)>>2];while(1){$c:{if(e){break $c}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break $c}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break $c}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break _c}l=H[p+4>>2];k=b>>>0>>0;H[i>>2]=H[p+(k?8:12)>>2];while(1){ad:{if(e){break ad}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break ad}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break ad}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}if(!k){break Zc}p=j-4|0;h=H[j+4>>2]>>>20&4|(H[p>>2]>>>22&1|(c>>>15&16|(c>>>19&64|h&170)));i=D+(I[h+24384|0]<<2)|0;s=H[i>>2];g=H[s>>2];b=b-g|0;t=(W<<2)+o|0;bd:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[s+4>>2];if(b&32768){break bd}l=H[s+4>>2];g=b>>>0>>0;H[i>>2]=H[s+(g?12:8)>>2];while(1){cd:{if(e){break cd}e=H[f+16>>2];k=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(s<<8)+d|0;break cd}if(s>>>0<=143){H[f+16>>2]=k;d=(s<<9)+d|0;e=7;break cd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break bd}l=H[s+4>>2];k=b>>>0>>0;H[i>>2]=H[s+(k?8:12)>>2];while(1){dd:{if(e){break dd}e=H[f+16>>2];b=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(s<<8)+d|0;break dd}if(s>>>0<=143){H[f+16>>2]=b;d=(s<<9)+d|0;e=7;break dd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}g=I[h+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|256;H[j+4>>2]=H[j+4>>2]|64;c=(g^k)<<22|c|128}ed:{if(c&134218752){break ed}h=c>>>6|0;i=D+(I[H[f+108>>2]+(h&495)|0]<<2)|0;p=H[i>>2];g=H[p>>2];b=b-g|0;fd:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[p+4>>2];if(b&32768){break fd}l=H[p+4>>2];g=b>>>0>>0;H[i>>2]=H[p+(g?12:8)>>2];while(1){gd:{if(e){break gd}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break gd}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break gd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break fd}l=H[p+4>>2];k=b>>>0>>0;H[i>>2]=H[p+(k?8:12)>>2];while(1){hd:{if(e){break hd}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break hd}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break hd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}if(!k){break ed}p=j-4|0;h=H[j+4>>2]>>>23&4|(H[p>>2]>>>25&1|(c>>>18&16|(c>>>22&64|h&170)));i=D+(I[h+24384|0]<<2)|0;s=H[i>>2];g=H[s>>2];b=b-g|0;t=o+u|0;id:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[s+4>>2];if(b&32768){break id}l=H[s+4>>2];g=b>>>0>>0;H[i>>2]=H[s+(g?12:8)>>2];while(1){jd:{if(e){break jd}e=H[f+16>>2];k=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(s<<8)+d|0;break jd}if(s>>>0<=143){H[f+16>>2]=k;d=(s<<9)+d|0;e=7;break jd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break id}l=H[s+4>>2];k=b>>>0>>0;H[i>>2]=H[s+(k?8:12)>>2];while(1){kd:{if(e){break kd}e=H[f+16>>2];b=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(s<<8)+d|0;break kd}if(s>>>0<=143){H[f+16>>2]=b;d=(s<<9)+d|0;e=7;break kd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}g=I[h+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;c=(g^k)<<25|c|1024}if(c&1073750016){break Qc}h=c>>>9|0;i=D+(I[H[f+108>>2]+(h&495)|0]<<2)|0;p=H[i>>2];g=H[p>>2];b=b-g|0;ld:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[p+4>>2];if(b&32768){break ld}l=H[p+4>>2];g=b>>>0>>0;H[i>>2]=H[p+(g?12:8)>>2];while(1){md:{if(e){break md}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break md}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break md}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break ld}l=H[p+4>>2];k=b>>>0>>0;H[i>>2]=H[p+(k?8:12)>>2];while(1){nd:{if(e){break nd}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break nd}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break nd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}if(!k){break Qc}p=j-4|0;T=H[j+4>>2]>>>26&4|(H[p>>2]>>>28&1|(c>>>21&16|(c>>>25&64|h&170)));i=D+(I[T+24384|0]<<2)|0;t=H[i>>2];g=H[t>>2];b=b-g|0;break Rc}k=H[q>>2];c=H[k>>2];b=b-c|0;od:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;g=H[k+4>>2];if(b&32768){break od}i=H[k+4>>2];c=b>>>0>>0;H[q>>2]=H[k+(c?12:8)>>2];while(1){pd:{if(e){break pd}k=H[f+16>>2];g=k+1|0;h=I[k+1|0];if(I[k|0]!=255){H[f+16>>2]=g;e=8;d=(h<<8)+d|0;break pd}if(h>>>0<=143){H[f+16>>2]=g;d=(h<<9)+d|0;e=7;break pd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}g=c?!i:i;break od}i=H[k+4>>2];g=b>>>0>>0;H[q>>2]=H[k+(g?8:12)>>2];while(1){qd:{if(e){break qd}k=H[f+16>>2];b=k+1|0;h=I[k+1|0];if(I[k|0]!=255){H[f+16>>2]=b;e=8;d=(h<<8)+d|0;break qd}if(h>>>0<=143){H[f+16>>2]=b;d=(h<<9)+d|0;e=7;break qd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;g=g?i:!i}if(!g){i=q;break Pc}g=H[n>>2];c=H[g>>2];b=b-c|0;rd:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;k=H[g+4>>2];if(b&32768){break rd}h=H[g+4>>2];c=b>>>0>>0;g=H[(c?12:8)+g>>2];H[n>>2]=g;while(1){sd:{if(e){break sd}i=H[f+16>>2];k=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break sd}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break sd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=c?!h:h;break rd}h=H[g+4>>2];k=b>>>0>>0;g=H[(k?8:12)+g>>2];H[n>>2]=g;while(1){td:{if(e){break td}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break td}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break td}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;k=k?h:!h}c=H[g>>2];b=b-c|0;ud:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;i=H[g+4>>2];if(b&32768){break ud}h=H[g+4>>2];c=b>>>0>>0;H[n>>2]=H[(c?12:8)+g>>2];while(1){vd:{if(e){break vd}i=H[f+16>>2];g=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=g;e=8;d=(l<<8)+d|0;break vd}if(l>>>0<=143){H[f+16>>2]=g;d=(l<<9)+d|0;e=7;break vd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}i=c?!h:h;break ud}h=H[g+4>>2];i=g;g=b>>>0>>0;H[n>>2]=H[i+(g?8:12)>>2];while(1){wd:{if(e){break wd}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break wd}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break wd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;i=g?h:!h}g=i;c=0;i=n;xd:{yd:{zd:{Ad:{Bd:{switch(g|k<<1){case 0:l=j-4|0;k=H[j+4>>2]>>>17&4|H[l>>2]>>>19&1;g=D+(I[k+24384|0]<<2)|0;i=H[g>>2];c=H[i>>2];b=b-c|0;Cd:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;s=H[i+4>>2];if(b&32768){break Cd}h=H[i+4>>2];c=b>>>0>>0;H[g>>2]=H[i+(c?12:8)>>2];while(1){Dd:{if(e){break Dd}i=H[f+16>>2];g=i+1|0;p=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=g;e=8;d=(p<<8)+d|0;break Dd}if(p>>>0<=143){H[f+16>>2]=g;d=(p<<9)+d|0;e=7;break Dd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=c?!h:h;break Cd}h=H[i+4>>2];s=g;g=b>>>0>>0;H[s>>2]=H[i+(g?8:12)>>2];while(1){Ed:{if(e){break Ed}i=H[f+16>>2];b=i+1|0;p=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break Ed}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Ed}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;s=g?h:!h}g=s;c=I[k+24640|0];H[o>>2]=(g|0)==(c|0)?M:v;H[l>>2]=H[l>>2]|32;H[j+4>>2]=H[j+4>>2]|8;k=(c^g)<<19;t=H[f+108>>2];g=D+(I[t+2|0]<<2)|0;i=H[g>>2];c=H[i>>2];b=b-c|0;Fd:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;s=H[i+4>>2];if(b&32768){break Fd}h=H[i+4>>2];c=b>>>0>>0;H[g>>2]=H[i+(c?12:8)>>2];while(1){Gd:{if(e){break Gd}i=H[f+16>>2];g=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=g;e=8;d=(l<<8)+d|0;break Gd}if(l>>>0<=143){H[f+16>>2]=g;d=(l<<9)+d|0;e=7;break Gd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=c?!h:h;break Fd}h=H[i+4>>2];s=g;g=b>>>0>>0;H[s>>2]=H[i+(g?8:12)>>2];while(1){Hd:{if(e){break Hd}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Hd}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Hd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;s=g?h:!h}g=s;c=k|16;if(!g){break Ad}break;case 1:break Bd;case 2:break zd;case 3:break xd;default:break Qc}}l=j-4|0;i=H[j+4>>2]>>>20&4|(H[l>>2]>>>22&1|(c>>>15&16|(c>>>19&64|c>>>3&170)));k=D+(I[i+24384|0]<<2)|0;p=H[k>>2];g=H[p>>2];b=b-g|0;t=(W<<2)+o|0;Id:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;s=H[p+4>>2];if(b&32768){break Id}h=H[p+4>>2];g=b>>>0>>0;H[k>>2]=H[p+(g?12:8)>>2];while(1){Jd:{if(e){break Jd}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break Jd}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break Jd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=g?!h:h;break Id}h=H[p+4>>2];s=k;k=b>>>0>>0;H[s>>2]=H[p+(k?8:12)>>2];while(1){Kd:{if(e){break Kd}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break Kd}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Kd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;s=k?h:!h}k=s;g=I[i+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[l>>2]=H[l>>2]|256;H[j+4>>2]=H[j+4>>2]|64;t=H[f+108>>2];c=(g^k)<<22|c|128}k=D+(I[(c>>>6&495)+t|0]<<2)|0;i=H[k>>2];g=H[i>>2];b=b-g|0;Ld:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;s=H[i+4>>2];if(b&32768){break Ld}h=H[i+4>>2];g=b>>>0>>0;H[k>>2]=H[i+(g?12:8)>>2];while(1){Md:{if(e){break Md}i=H[f+16>>2];k=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break Md}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break Md}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=g?!h:h;break Ld}h=H[i+4>>2];s=k;k=b>>>0>>0;H[s>>2]=H[i+(k?8:12)>>2];while(1){Nd:{if(e){break Nd}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Nd}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Nd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;s=k?h:!h}if(!s){break yd}}l=j-4|0;i=H[j+4>>2]>>>23&4|(H[l>>2]>>>25&1|(c>>>18&16|(c>>>22&64|c>>>6&170)));k=D+(I[i+24384|0]<<2)|0;p=H[k>>2];g=H[p>>2];b=b-g|0;t=o+u|0;Od:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;s=H[p+4>>2];if(b&32768){break Od}h=H[p+4>>2];g=b>>>0>>0;H[k>>2]=H[p+(g?12:8)>>2];while(1){Pd:{if(e){break Pd}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break Pd}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break Pd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=g?!h:h;break Od}h=H[p+4>>2];s=k;k=b>>>0>>0;H[s>>2]=H[p+(k?8:12)>>2];while(1){Qd:{if(e){break Qd}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break Qd}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Qd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;s=k?h:!h}k=s;g=I[i+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[l>>2]=H[l>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;c=(g^k)<<25|c|1024;t=H[f+108>>2]}i=D+(I[(c>>>9&495)+t|0]<<2)|0;l=H[i>>2];g=H[l>>2];b=b-g|0;Rd:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[l+4>>2];if(b&32768){break Rd}h=H[l+4>>2];g=b>>>0>>0;H[i>>2]=H[l+(g?12:8)>>2];while(1){Sd:{if(e){break Sd}e=H[f+16>>2];k=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break Sd}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break Sd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!h:h;break Rd}h=H[l+4>>2];k=b>>>0>>0;H[i>>2]=H[l+(k?8:12)>>2];while(1){Td:{if(e){break Td}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Td}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Td}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?h:!h}if(!k){break Qc}}p=j-4|0;T=H[j+4>>2]>>>26&4|(H[p>>2]>>>28&1|(c>>>21&16|(c>>>25&64|c>>>9&170)));i=D+(I[T+24384|0]<<2)|0;t=H[i>>2];g=H[t>>2];b=b-g|0}s=o+r|0;Ud:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[t+4>>2];if(b&32768){break Ud}h=H[t+4>>2];g=b>>>0>>0;H[i>>2]=H[(g?12:8)+t>>2];while(1){Vd:{if(e){break Vd}e=H[f+16>>2];k=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break Vd}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break Vd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!h:h;break Ud}h=H[t+4>>2];k=b>>>0>>0;H[i>>2]=H[(k?8:12)+t>>2];while(1){Wd:{if(e){break Wd}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Wd}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Wd}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?h:!h}g=I[T+24640|0];H[s>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|16384;H[j+4>>2]=H[j+4>>2]|4096;h=j+(H[f+124>>2]<<2)|0;H[h+4>>2]=H[h+4>>2]|4;H[h+12>>2]=H[h+12>>2]|1;g=g^k;H[h+8>>2]=H[h+8>>2]|g<<18|2;c=g<<28|c|8192}H[j>>2]=c&-1226833921}c=j+4|0;o=o+4|0;w=w+1|0;if((W|0)!=(w|0)){continue}break}c=j+12|0;o=o+r|0;x=x+4|0;g=H[f+128>>2];if(x>>>0<(g&-4)>>>0){continue}break}break Oc}x=g&-4;c=(k+(x<<1)|0)+12|0}H[f+8>>2]=e;H[f+4>>2]=b;H[f>>2]=d;H[f+104>>2]=i;if(!W|g>>>0<=x>>>0){break Db}while(1){e=0;if(H[f+128>>2]!=(x|0)){while(1){Wb(f,c,(N(e,W)<<2)+o|0,M,e,1);e=e+1|0;if(e>>>0>2]-x>>>0){continue}break}}H[c>>2]=H[c>>2]&-1226833921;o=o+4|0;c=c+4|0;m=m+1|0;if((W|0)!=(m|0)){continue}break}break Db}Xd:{if(g>>>0<4){break Xd}if(W){n=f+100|0;q=f+96|0;r=N(W,12);u=W<<3;v=0-M|0;D=f+28|0;while(1){w=0;while(1){j=c;c=H[c>>2];Yd:{Zd:{_d:{if(c){$d:{if(c&2097168){break $d}i=D+(I[H[f+108>>2]+(c&495)|0]<<2)|0;l=H[i>>2];g=H[l>>2];b=b-g|0;ae:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[l+4>>2];if(b&32768){break ae}h=H[l+4>>2];g=b>>>0>>0;H[i>>2]=H[l+(g?12:8)>>2];while(1){be:{if(e){break be}e=H[f+16>>2];k=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break be}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break be}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!h:h;break ae}h=H[l+4>>2];k=b>>>0>>0;H[i>>2]=H[l+(k?8:12)>>2];while(1){ce:{if(e){break ce}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break ce}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break ce}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?h:!h}if(!k){break $d}p=j-4|0;h=H[j+4>>2]>>>17&4|(H[p>>2]>>>19&1|(c>>>14&16|(c>>>16&64|c&170)));i=D+(I[h+24384|0]<<2)|0;s=H[i>>2];g=H[s>>2];b=b-g|0;de:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[s+4>>2];if(b&32768){break de}l=H[s+4>>2];g=b>>>0>>0;H[i>>2]=H[s+(g?12:8)>>2];while(1){ee:{if(e){break ee}e=H[f+16>>2];k=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(s<<8)+d|0;break ee}if(s>>>0<=143){H[f+16>>2]=k;d=(s<<9)+d|0;e=7;break ee}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break de}l=H[s+4>>2];k=b>>>0>>0;H[i>>2]=H[s+(k?8:12)>>2];while(1){fe:{if(e){break fe}e=H[f+16>>2];b=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(s<<8)+d|0;break fe}if(s>>>0<=143){H[f+16>>2]=b;d=(s<<9)+d|0;e=7;break fe}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}g=I[h+24640|0];H[o>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|32;H[j+4>>2]=H[j+4>>2]|8;h=j+(-2-H[f+124>>2]<<2)|0;H[h+4>>2]=H[h+4>>2]|32768;k=g^k;H[h>>2]=H[h>>2]|k<<31|65536;g=h-4|0;H[g>>2]=H[g>>2]|131072;c=k<<19|c|16}ge:{if(c&16777344){break ge}h=c>>>3|0;i=D+(I[H[f+108>>2]+(h&495)|0]<<2)|0;p=H[i>>2];g=H[p>>2];b=b-g|0;he:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[p+4>>2];if(b&32768){break he}l=H[p+4>>2];g=b>>>0>>0;H[i>>2]=H[p+(g?12:8)>>2];while(1){ie:{if(e){break ie}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break ie}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break ie}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break he}l=H[p+4>>2];k=b>>>0>>0;H[i>>2]=H[p+(k?8:12)>>2];while(1){je:{if(e){break je}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break je}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break je}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}if(!k){break ge}p=j-4|0;h=H[j+4>>2]>>>20&4|(H[p>>2]>>>22&1|(c>>>15&16|(c>>>19&64|h&170)));i=D+(I[h+24384|0]<<2)|0;s=H[i>>2];g=H[s>>2];b=b-g|0;t=(W<<2)+o|0;ke:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[s+4>>2];if(b&32768){break ke}l=H[s+4>>2];g=b>>>0>>0;H[i>>2]=H[s+(g?12:8)>>2];while(1){le:{if(e){break le}e=H[f+16>>2];k=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(s<<8)+d|0;break le}if(s>>>0<=143){H[f+16>>2]=k;d=(s<<9)+d|0;e=7;break le}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break ke}l=H[s+4>>2];k=b>>>0>>0;H[i>>2]=H[s+(k?8:12)>>2];while(1){me:{if(e){break me}e=H[f+16>>2];b=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(s<<8)+d|0;break me}if(s>>>0<=143){H[f+16>>2]=b;d=(s<<9)+d|0;e=7;break me}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}g=I[h+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|256;H[j+4>>2]=H[j+4>>2]|64;c=(g^k)<<22|c|128}ne:{if(c&134218752){break ne}h=c>>>6|0;i=D+(I[H[f+108>>2]+(h&495)|0]<<2)|0;p=H[i>>2];g=H[p>>2];b=b-g|0;oe:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[p+4>>2];if(b&32768){break oe}l=H[p+4>>2];g=b>>>0>>0;H[i>>2]=H[p+(g?12:8)>>2];while(1){pe:{if(e){break pe}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break pe}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break pe}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break oe}l=H[p+4>>2];k=b>>>0>>0;H[i>>2]=H[p+(k?8:12)>>2];while(1){qe:{if(e){break qe}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break qe}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break qe}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}if(!k){break ne}p=j-4|0;h=H[j+4>>2]>>>23&4|(H[p>>2]>>>25&1|(c>>>18&16|(c>>>22&64|h&170)));i=D+(I[h+24384|0]<<2)|0;s=H[i>>2];g=H[s>>2];b=b-g|0;t=o+u|0;re:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[s+4>>2];if(b&32768){break re}l=H[s+4>>2];g=b>>>0>>0;H[i>>2]=H[s+(g?12:8)>>2];while(1){se:{if(e){break se}e=H[f+16>>2];k=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(s<<8)+d|0;break se}if(s>>>0<=143){H[f+16>>2]=k;d=(s<<9)+d|0;e=7;break se}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break re}l=H[s+4>>2];k=b>>>0>>0;H[i>>2]=H[s+(k?8:12)>>2];while(1){te:{if(e){break te}e=H[f+16>>2];b=e+1|0;s=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(s<<8)+d|0;break te}if(s>>>0<=143){H[f+16>>2]=b;d=(s<<9)+d|0;e=7;break te}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}g=I[h+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;c=(g^k)<<25|c|1024}if(c&1073750016){break Zd}h=c>>>9|0;i=D+(I[H[f+108>>2]+(h&495)|0]<<2)|0;p=H[i>>2];g=H[p>>2];b=b-g|0;ue:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[p+4>>2];if(b&32768){break ue}l=H[p+4>>2];g=b>>>0>>0;H[i>>2]=H[p+(g?12:8)>>2];while(1){ve:{if(e){break ve}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break ve}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break ve}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!l:l;break ue}l=H[p+4>>2];k=b>>>0>>0;H[i>>2]=H[p+(k?8:12)>>2];while(1){we:{if(e){break we}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break we}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break we}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?l:!l}if(!k){break Zd}p=j-4|0;T=H[j+4>>2]>>>26&4|(H[p>>2]>>>28&1|(c>>>21&16|(c>>>25&64|h&170)));i=D+(I[T+24384|0]<<2)|0;t=H[i>>2];g=H[t>>2];b=b-g|0;break _d}k=H[q>>2];c=H[k>>2];b=b-c|0;xe:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;g=H[k+4>>2];if(b&32768){break xe}i=H[k+4>>2];c=b>>>0>>0;H[q>>2]=H[k+(c?12:8)>>2];while(1){ye:{if(e){break ye}k=H[f+16>>2];g=k+1|0;h=I[k+1|0];if(I[k|0]!=255){H[f+16>>2]=g;e=8;d=(h<<8)+d|0;break ye}if(h>>>0<=143){H[f+16>>2]=g;d=(h<<9)+d|0;e=7;break ye}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}g=c?!i:i;break xe}i=H[k+4>>2];g=b>>>0>>0;H[q>>2]=H[k+(g?8:12)>>2];while(1){ze:{if(e){break ze}k=H[f+16>>2];b=k+1|0;h=I[k+1|0];if(I[k|0]!=255){H[f+16>>2]=b;e=8;d=(h<<8)+d|0;break ze}if(h>>>0<=143){H[f+16>>2]=b;d=(h<<9)+d|0;e=7;break ze}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;g=g?i:!i}if(!g){i=q;break Yd}g=H[n>>2];c=H[g>>2];b=b-c|0;Ae:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;k=H[g+4>>2];if(b&32768){break Ae}h=H[g+4>>2];c=b>>>0>>0;g=H[(c?12:8)+g>>2];H[n>>2]=g;while(1){Be:{if(e){break Be}i=H[f+16>>2];k=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break Be}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break Be}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=c?!h:h;break Ae}h=H[g+4>>2];k=b>>>0>>0;g=H[(k?8:12)+g>>2];H[n>>2]=g;while(1){Ce:{if(e){break Ce}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Ce}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Ce}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;k=k?h:!h}c=H[g>>2];b=b-c|0;De:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;i=H[g+4>>2];if(b&32768){break De}h=H[g+4>>2];c=b>>>0>>0;H[n>>2]=H[(c?12:8)+g>>2];while(1){Ee:{if(e){break Ee}i=H[f+16>>2];g=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=g;e=8;d=(l<<8)+d|0;break Ee}if(l>>>0<=143){H[f+16>>2]=g;d=(l<<9)+d|0;e=7;break Ee}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}i=c?!h:h;break De}h=H[g+4>>2];i=g;g=b>>>0>>0;H[n>>2]=H[i+(g?8:12)>>2];while(1){Fe:{if(e){break Fe}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Fe}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Fe}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;i=g?h:!h}g=i;c=0;i=n;Ge:{He:{Ie:{Je:{Ke:{switch(g|k<<1){case 0:l=j-4|0;k=H[j+4>>2]>>>17&4|H[l>>2]>>>19&1;g=D+(I[k+24384|0]<<2)|0;i=H[g>>2];c=H[i>>2];b=b-c|0;Le:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;s=H[i+4>>2];if(b&32768){break Le}h=H[i+4>>2];c=b>>>0>>0;H[g>>2]=H[i+(c?12:8)>>2];while(1){Me:{if(e){break Me}i=H[f+16>>2];g=i+1|0;p=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=g;e=8;d=(p<<8)+d|0;break Me}if(p>>>0<=143){H[f+16>>2]=g;d=(p<<9)+d|0;e=7;break Me}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=c?!h:h;break Le}h=H[i+4>>2];s=g;g=b>>>0>>0;H[s>>2]=H[i+(g?8:12)>>2];while(1){Ne:{if(e){break Ne}i=H[f+16>>2];b=i+1|0;p=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break Ne}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Ne}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;s=g?h:!h}g=s;c=I[k+24640|0];H[o>>2]=(g|0)==(c|0)?M:v;H[l>>2]=H[l>>2]|32;H[j+4>>2]=H[j+4>>2]|8;k=j+(-2-H[f+124>>2]<<2)|0;H[k+4>>2]=H[k+4>>2]|32768;g=c^g;H[k>>2]=H[k>>2]|g<<31|65536;c=k-4|0;H[c>>2]=H[c>>2]|131072;k=g<<19;t=H[f+108>>2];g=D+(I[t+2|0]<<2)|0;i=H[g>>2];c=H[i>>2];b=b-c|0;Oe:{if(d>>>16>>>0>=c>>>0){d=d-(c<<16)|0;s=H[i+4>>2];if(b&32768){break Oe}h=H[i+4>>2];c=b>>>0>>0;H[g>>2]=H[i+(c?12:8)>>2];while(1){Pe:{if(e){break Pe}i=H[f+16>>2];g=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=g;e=8;d=(l<<8)+d|0;break Pe}if(l>>>0<=143){H[f+16>>2]=g;d=(l<<9)+d|0;e=7;break Pe}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=c?!h:h;break Oe}h=H[i+4>>2];s=g;g=b>>>0>>0;H[s>>2]=H[i+(g?8:12)>>2];while(1){Qe:{if(e){break Qe}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break Qe}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Qe}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;s=g?h:!h}g=s;c=k|16;if(!g){break Je}break;case 1:break Ke;case 2:break Ie;case 3:break Ge;default:break Zd}}l=j-4|0;i=H[j+4>>2]>>>20&4|(H[l>>2]>>>22&1|(c>>>15&16|(c>>>19&64|c>>>3&170)));k=D+(I[i+24384|0]<<2)|0;p=H[k>>2];g=H[p>>2];b=b-g|0;t=(W<<2)+o|0;Re:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;s=H[p+4>>2];if(b&32768){break Re}h=H[p+4>>2];g=b>>>0>>0;H[k>>2]=H[p+(g?12:8)>>2];while(1){Se:{if(e){break Se}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break Se}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break Se}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=g?!h:h;break Re}h=H[p+4>>2];s=k;k=b>>>0>>0;H[s>>2]=H[p+(k?8:12)>>2];while(1){Te:{if(e){break Te}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break Te}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Te}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;s=k?h:!h}k=s;g=I[i+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[l>>2]=H[l>>2]|256;H[j+4>>2]=H[j+4>>2]|64;t=H[f+108>>2];c=(g^k)<<22|c|128}k=D+(I[(c>>>6&495)+t|0]<<2)|0;i=H[k>>2];g=H[i>>2];b=b-g|0;Ue:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;s=H[i+4>>2];if(b&32768){break Ue}h=H[i+4>>2];g=b>>>0>>0;H[k>>2]=H[i+(g?12:8)>>2];while(1){Ve:{if(e){break Ve}i=H[f+16>>2];k=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break Ve}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break Ve}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=g?!h:h;break Ue}h=H[i+4>>2];s=k;k=b>>>0>>0;H[s>>2]=H[i+(k?8:12)>>2];while(1){We:{if(e){break We}i=H[f+16>>2];b=i+1|0;l=I[i+1|0];if(I[i|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break We}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break We}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;s=k?h:!h}if(!s){break He}}l=j-4|0;i=H[j+4>>2]>>>23&4|(H[l>>2]>>>25&1|(c>>>18&16|(c>>>22&64|c>>>6&170)));k=D+(I[i+24384|0]<<2)|0;p=H[k>>2];g=H[p>>2];b=b-g|0;t=o+u|0;Xe:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;s=H[p+4>>2];if(b&32768){break Xe}h=H[p+4>>2];g=b>>>0>>0;H[k>>2]=H[p+(g?12:8)>>2];while(1){Ye:{if(e){break Ye}e=H[f+16>>2];k=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(p<<8)+d|0;break Ye}if(p>>>0<=143){H[f+16>>2]=k;d=(p<<9)+d|0;e=7;break Ye}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}s=g?!h:h;break Xe}h=H[p+4>>2];s=k;k=b>>>0>>0;H[s>>2]=H[p+(k?8:12)>>2];while(1){Ze:{if(e){break Ze}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(p<<8)+d|0;break Ze}if(p>>>0<=143){H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Ze}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;s=k?h:!h}k=s;g=I[i+24640|0];H[t>>2]=(k|0)==(g|0)?M:v;H[l>>2]=H[l>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;c=(g^k)<<25|c|1024;t=H[f+108>>2]}i=D+(I[(c>>>9&495)+t|0]<<2)|0;l=H[i>>2];g=H[l>>2];b=b-g|0;_e:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[l+4>>2];if(b&32768){break _e}h=H[l+4>>2];g=b>>>0>>0;H[i>>2]=H[l+(g?12:8)>>2];while(1){$e:{if(e){break $e}e=H[f+16>>2];k=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break $e}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break $e}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!h:h;break _e}h=H[l+4>>2];k=b>>>0>>0;H[i>>2]=H[l+(k?8:12)>>2];while(1){af:{if(e){break af}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break af}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break af}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?h:!h}if(!k){break Zd}}p=j-4|0;T=H[j+4>>2]>>>26&4|(H[p>>2]>>>28&1|(c>>>21&16|(c>>>25&64|c>>>9&170)));i=D+(I[T+24384|0]<<2)|0;t=H[i>>2];g=H[t>>2];b=b-g|0}s=o+r|0;bf:{if(d>>>16>>>0>=g>>>0){d=d-(g<<16)|0;k=H[t+4>>2];if(b&32768){break bf}h=H[t+4>>2];g=b>>>0>>0;H[i>>2]=H[(g?12:8)+t>>2];while(1){cf:{if(e){break cf}e=H[f+16>>2];k=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=k;e=8;d=(l<<8)+d|0;break cf}if(l>>>0<=143){H[f+16>>2]=k;d=(l<<9)+d|0;e=7;break cf}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}k=g?!h:h;break bf}h=H[t+4>>2];k=b>>>0>>0;H[i>>2]=H[(k?8:12)+t>>2];while(1){df:{if(e){break df}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=b;e=8;d=(l<<8)+d|0;break df}if(l>>>0<=143){H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break df}H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;k=k?h:!h}g=I[T+24640|0];H[s>>2]=(k|0)==(g|0)?M:v;H[p>>2]=H[p>>2]|16384;H[j+4>>2]=H[j+4>>2]|4096;h=j+(H[f+124>>2]<<2)|0;H[h+4>>2]=H[h+4>>2]|4;H[h+12>>2]=H[h+12>>2]|1;g=g^k;H[h+8>>2]=H[h+8>>2]|g<<18|2;c=g<<28|c|8192}H[j>>2]=c&-1226833921}c=j+4|0;o=o+4|0;w=w+1|0;if((W|0)!=(w|0)){continue}break}c=j+12|0;o=o+r|0;x=x+4|0;g=H[f+128>>2];if(x>>>0<(g&-4)>>>0){continue}break}break Xd}x=g&-4;c=(k+(x<<1)|0)+12|0}H[f+8>>2]=e;H[f+4>>2]=b;H[f>>2]=d;H[f+104>>2]=i;if(!W|g>>>0<=x>>>0){break Db}while(1){e=0;if(H[f+128>>2]!=(x|0)){while(1){Wb(f,c,(N(e,W)<<2)+o|0,M,e,0);e=e+1|0;if(e>>>0>2]-x>>>0){continue}break}}H[c>>2]=H[c>>2]&-1226833921;o=o+4|0;c=c+4|0;m=m+1|0;if((W|0)!=(m|0)){continue}break}break Db}while(1){r=0;while(1){o=c;j=g;g=H[g>>2];ef:{ff:{gf:{if(!g){i=H[k>>2];g=H[i>>2];b=b-g|0;hf:{if(d>>>16>>>0>>0){n=H[i+4>>2];c=b>>>0>>0;H[k>>2]=H[i+(c?8:12)>>2];while(1){jf:{if(e){break jf}i=H[f+16>>2];b=i+1|0;h=I[i+1|0];if(I[i|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break jf}H[f+16>>2]=b;d=(h<<9)+d|0;e=7;break jf}H[f+16>>2]=b;e=8;d=(h<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;c=c?n:!n;break hf}d=d-(g<<16)|0;if(!(b&32768)){n=H[i+4>>2];c=b>>>0>>0;H[k>>2]=H[i+(c?12:8)>>2];while(1){kf:{if(e){break kf}i=H[f+16>>2];g=i+1|0;h=I[i+1|0];if(I[i|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break kf}H[f+16>>2]=g;d=(h<<9)+d|0;e=7;break kf}H[f+16>>2]=g;e=8;d=(h<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!n:n;break hf}c=H[i+4>>2]}if(!c){i=k;break ef}c=H[q>>2];g=H[c>>2];b=b-g|0;lf:{if(d>>>16>>>0>>0){h=H[c+4>>2];i=b>>>0>>0;c=H[(i?8:12)+c>>2];H[q>>2]=c;while(1){mf:{if(e){break mf}n=H[f+16>>2];b=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break mf}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break mf}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;n=i?h:!h;break lf}d=d-(g<<16)|0;if(!(b&32768)){h=H[c+4>>2];g=b>>>0>>0;c=H[(g?12:8)+c>>2];H[q>>2]=c;while(1){nf:{if(e){break nf}n=H[f+16>>2];i=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break nf}H[f+16>>2]=i;d=(m<<9)+d|0;e=7;break nf}H[f+16>>2]=i;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=g?!h:h;break lf}n=H[c+4>>2]}g=H[c>>2];b=b-g|0;of:{if(d>>>16>>>0>>0){h=H[c+4>>2];i=c;c=b>>>0>>0;H[q>>2]=H[i+(c?8:12)>>2];while(1){pf:{if(e){break pf}i=H[f+16>>2];b=i+1|0;m=I[i+1|0];if(I[i|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break pf}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break pf}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;c=c?h:!h;break of}d=d-(g<<16)|0;if(!(b&32768)){h=H[c+4>>2];i=c;c=b>>>0>>0;H[q>>2]=H[i+(c?12:8)>>2];while(1){qf:{if(e){break qf}i=H[f+16>>2];g=i+1|0;m=I[i+1|0];if(I[i|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break qf}H[f+16>>2]=g;d=(m<<9)+d|0;e=7;break qf}H[f+16>>2]=g;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break of}c=H[c+4>>2]}g=0;i=q;rf:{sf:{tf:{uf:{vf:{switch(c|n<<1){case 0:m=j-4|0;i=H[j+4>>2]>>>17&4|H[m>>2]>>>19&1;c=w+(I[i+24384|0]<<2)|0;n=H[c>>2];g=H[n>>2];b=b-g|0;wf:{if(d>>>16>>>0>>0){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?8:12)>>2];while(1){xf:{if(e){break xf}n=H[f+16>>2];b=n+1|0;l=I[n+1|0];if(I[n|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break xf}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break xf}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;g=c?h:!h;break wf}d=d-(g<<16)|0;if(!(b&32768)){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?12:8)>>2];while(1){yf:{if(e){break yf}n=H[f+16>>2];g=n+1|0;l=I[n+1|0];if(I[n|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break yf}H[f+16>>2]=g;d=(l<<9)+d|0;e=7;break yf}H[f+16>>2]=g;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}g=c?!h:h;break wf}g=H[n+4>>2]}c=I[i+24640|0];H[o>>2]=(g|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|32;H[j+4>>2]=H[j+4>>2]|8;i=(c^g)<<19;t=H[f+108>>2];c=w+(I[t+2|0]<<2)|0;n=H[c>>2];g=H[n>>2];b=b-g|0;zf:{if(d>>>16>>>0>>0){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?8:12)>>2];while(1){Af:{if(e){break Af}n=H[f+16>>2];b=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Af}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break Af}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;g=g<<1;if(g>>>0<32768){continue}break}b=g;c=c?h:!h;break zf}d=d-(g<<16)|0;if(!(b&32768)){h=H[n+4>>2];v=c;c=b>>>0>>0;H[v>>2]=H[n+(c?12:8)>>2];while(1){Bf:{if(e){break Bf}n=H[f+16>>2];g=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Bf}H[f+16>>2]=g;d=(m<<9)+d|0;e=7;break Bf}H[f+16>>2]=g;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break zf}c=H[n+4>>2]}g=i|16;if(!c){break uf}break;case 1:break vf;case 2:break tf;case 3:break rf;default:break ff}}m=j-4|0;n=H[j+4>>2]>>>20&4|(H[m>>2]>>>22&1|(g>>>15&16|(g>>>19&64|g>>>3&170)));i=w+(I[n+24384|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;Cf:{if(d>>>16>>>0>>0){h=H[l+4>>2];v=i;i=b>>>0>>0;H[v>>2]=H[l+(i?8:12)>>2];while(1){Df:{if(e){break Df}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Df}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Df}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;i=i?h:!h;break Cf}d=d-(c<<16)|0;if(!(b&32768)){h=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){Ef:{if(e){break Ef}e=H[f+16>>2];i=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Ef}H[f+16>>2]=i;d=(l<<9)+d|0;e=7;break Ef}H[f+16>>2]=i;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}i=c?!h:h;break Cf}i=H[l+4>>2]}c=I[n+24640|0];H[o+256>>2]=(i|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|256;H[j+4>>2]=H[j+4>>2]|64;t=H[f+108>>2];g=(c^i)<<22|g|128}i=w+(I[(g>>>6&495)+t|0]<<2)|0;n=H[i>>2];c=H[n>>2];b=b-c|0;Ff:{if(d>>>16>>>0>>0){h=H[n+4>>2];v=i;i=b>>>0>>0;H[v>>2]=H[n+(i?8:12)>>2];while(1){Gf:{if(e){break Gf}n=H[f+16>>2];b=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Gf}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break Gf}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=i?h:!h;break Ff}d=d-(c<<16)|0;if(!(b&32768)){h=H[n+4>>2];c=b>>>0>>0;H[i>>2]=H[n+(c?12:8)>>2];while(1){Hf:{if(e){break Hf}n=H[f+16>>2];i=n+1|0;m=I[n+1|0];if(I[n|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Hf}H[f+16>>2]=i;d=(m<<9)+d|0;e=7;break Hf}H[f+16>>2]=i;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break Ff}c=H[n+4>>2]}if(!c){break sf}}m=j-4|0;n=H[j+4>>2]>>>23&4|(H[m>>2]>>>25&1|(g>>>18&16|(g>>>22&64|g>>>6&170)));i=w+(I[n+24384|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;If:{if(d>>>16>>>0>>0){h=H[l+4>>2];v=i;i=b>>>0>>0;H[v>>2]=H[l+(i?8:12)>>2];while(1){Jf:{if(e){break Jf}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Jf}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Jf}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;i=i?h:!h;break If}d=d-(c<<16)|0;if(!(b&32768)){h=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){Kf:{if(e){break Kf}e=H[f+16>>2];i=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Kf}H[f+16>>2]=i;d=(l<<9)+d|0;e=7;break Kf}H[f+16>>2]=i;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}i=c?!h:h;break If}i=H[l+4>>2]}c=I[n+24640|0];H[o+512>>2]=(i|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;g=(c^i)<<25|g|1024;t=H[f+108>>2]}i=w+(I[(g>>>9&495)+t|0]<<2)|0;m=H[i>>2];c=H[m>>2];b=b-c|0;Lf:{if(d>>>16>>>0>>0){h=H[m+4>>2];n=b>>>0>>0;H[i>>2]=H[m+(n?8:12)>>2];while(1){Mf:{if(e){break Mf}e=H[f+16>>2];b=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Mf}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break Mf}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?h:!h;break Lf}d=d-(c<<16)|0;if(!(b&32768)){h=H[m+4>>2];c=b>>>0>>0;H[i>>2]=H[m+(c?12:8)>>2];while(1){Nf:{if(e){break Nf}e=H[f+16>>2];n=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Nf}H[f+16>>2]=n;d=(m<<9)+d|0;e=7;break Nf}H[f+16>>2]=n;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break Lf}c=H[m+4>>2]}if(!c){break ff}}m=j-4|0;p=H[j+4>>2]>>>26&4|(H[m>>2]>>>28&1|(g>>>21&16|(g>>>25&64|g>>>9&170)));i=w+(I[p+24384|0]<<2)|0;t=H[i>>2];c=H[t>>2];b=b-c|0;break gf}Of:{if(g&2097168){break Of}i=w+(I[H[f+108>>2]+(g&495)|0]<<2)|0;m=H[i>>2];c=H[m>>2];b=b-c|0;Pf:{if(d>>>16>>>0>>0){h=H[m+4>>2];n=b>>>0>>0;H[i>>2]=H[m+(n?8:12)>>2];while(1){Qf:{if(e){break Qf}e=H[f+16>>2];b=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Qf}H[f+16>>2]=b;d=(m<<9)+d|0;e=7;break Qf}H[f+16>>2]=b;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?h:!h;break Pf}d=d-(c<<16)|0;if(!(b&32768)){h=H[m+4>>2];c=b>>>0>>0;H[i>>2]=H[m+(c?12:8)>>2];while(1){Rf:{if(e){break Rf}e=H[f+16>>2];n=e+1|0;m=I[e+1|0];if(I[e|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Rf}H[f+16>>2]=n;d=(m<<9)+d|0;e=7;break Rf}H[f+16>>2]=n;e=8;d=(m<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!h:h;break Pf}c=H[m+4>>2]}if(!c){break Of}l=j-4|0;h=H[j+4>>2]>>>17&4|(H[l>>2]>>>19&1|(g>>>14&16|(g>>>16&64|g&170)));i=w+(I[h+24384|0]<<2)|0;p=H[i>>2];c=H[p>>2];b=b-c|0;Sf:{if(d>>>16>>>0>>0){m=H[p+4>>2];n=b>>>0>>0;H[i>>2]=H[p+(n?8:12)>>2];while(1){Tf:{if(e){break Tf}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Tf}H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break Tf}H[f+16>>2]=b;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?m:!m;break Sf}d=d-(c<<16)|0;if(!(b&32768)){m=H[p+4>>2];c=b>>>0>>0;H[i>>2]=H[p+(c?12:8)>>2];while(1){Uf:{if(e){break Uf}e=H[f+16>>2];n=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Uf}H[f+16>>2]=n;d=(p<<9)+d|0;e=7;break Uf}H[f+16>>2]=n;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!m:m;break Sf}n=H[p+4>>2]}c=I[h+24640|0];H[o>>2]=(n|0)==(c|0)?s:u;H[l>>2]=H[l>>2]|32;H[j+4>>2]=H[j+4>>2]|8;g=(c^n)<<19|g|16}Vf:{if(g&16777344){break Vf}h=g>>>3|0;i=w+(I[H[f+108>>2]+(h&495)|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;Wf:{if(d>>>16>>>0>>0){m=H[l+4>>2];n=b>>>0>>0;H[i>>2]=H[l+(n?8:12)>>2];while(1){Xf:{if(e){break Xf}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Xf}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break Xf}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?m:!m;break Wf}d=d-(c<<16)|0;if(!(b&32768)){m=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){Yf:{if(e){break Yf}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break Yf}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break Yf}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!m:m;break Wf}c=H[l+4>>2]}if(!c){break Vf}l=j-4|0;h=H[j+4>>2]>>>20&4|(H[l>>2]>>>22&1|(g>>>15&16|(g>>>19&64|h&170)));i=w+(I[h+24384|0]<<2)|0;p=H[i>>2];c=H[p>>2];b=b-c|0;Zf:{if(d>>>16>>>0>>0){m=H[p+4>>2];n=b>>>0>>0;H[i>>2]=H[p+(n?8:12)>>2];while(1){_f:{if(e){break _f}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break _f}H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break _f}H[f+16>>2]=b;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?m:!m;break Zf}d=d-(c<<16)|0;if(!(b&32768)){m=H[p+4>>2];c=b>>>0>>0;H[i>>2]=H[p+(c?12:8)>>2];while(1){$f:{if(e){break $f}e=H[f+16>>2];n=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break $f}H[f+16>>2]=n;d=(p<<9)+d|0;e=7;break $f}H[f+16>>2]=n;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!m:m;break Zf}n=H[p+4>>2]}c=I[h+24640|0];H[o+256>>2]=(n|0)==(c|0)?s:u;H[l>>2]=H[l>>2]|256;H[j+4>>2]=H[j+4>>2]|64;g=(c^n)<<22|g|128}ag:{if(g&134218752){break ag}h=g>>>6|0;i=w+(I[H[f+108>>2]+(h&495)|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;bg:{if(d>>>16>>>0>>0){m=H[l+4>>2];n=b>>>0>>0;H[i>>2]=H[l+(n?8:12)>>2];while(1){cg:{if(e){break cg}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break cg}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break cg}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?m:!m;break bg}d=d-(c<<16)|0;if(!(b&32768)){m=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){dg:{if(e){break dg}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break dg}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break dg}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!m:m;break bg}c=H[l+4>>2]}if(!c){break ag}l=j-4|0;h=H[j+4>>2]>>>23&4|(H[l>>2]>>>25&1|(g>>>18&16|(g>>>22&64|h&170)));i=w+(I[h+24384|0]<<2)|0;p=H[i>>2];c=H[p>>2];b=b-c|0;eg:{if(d>>>16>>>0>>0){m=H[p+4>>2];n=b>>>0>>0;H[i>>2]=H[p+(n?8:12)>>2];while(1){fg:{if(e){break fg}e=H[f+16>>2];b=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break fg}H[f+16>>2]=b;d=(p<<9)+d|0;e=7;break fg}H[f+16>>2]=b;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?m:!m;break eg}d=d-(c<<16)|0;if(!(b&32768)){m=H[p+4>>2];c=b>>>0>>0;H[i>>2]=H[p+(c?12:8)>>2];while(1){gg:{if(e){break gg}e=H[f+16>>2];n=e+1|0;p=I[e+1|0];if(I[e|0]==255){if(p>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break gg}H[f+16>>2]=n;d=(p<<9)+d|0;e=7;break gg}H[f+16>>2]=n;e=8;d=(p<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!m:m;break eg}n=H[p+4>>2]}c=I[h+24640|0];H[o+512>>2]=(n|0)==(c|0)?s:u;H[l>>2]=H[l>>2]|2048;H[j+4>>2]=H[j+4>>2]|512;g=(c^n)<<25|g|1024}if(g&1073750016){break ff}h=g>>>9|0;i=w+(I[H[f+108>>2]+(h&495)|0]<<2)|0;l=H[i>>2];c=H[l>>2];b=b-c|0;hg:{if(d>>>16>>>0>>0){m=H[l+4>>2];n=b>>>0>>0;H[i>>2]=H[l+(n?8:12)>>2];while(1){ig:{if(e){break ig}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break ig}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break ig}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;c=n?m:!m;break hg}d=d-(c<<16)|0;if(!(b&32768)){m=H[l+4>>2];c=b>>>0>>0;H[i>>2]=H[l+(c?12:8)>>2];while(1){jg:{if(e){break jg}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break jg}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break jg}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}c=c?!m:m;break hg}c=H[l+4>>2]}if(!c){break ff}m=j-4|0;p=H[j+4>>2]>>>26&4|(H[m>>2]>>>28&1|(g>>>21&16|(g>>>25&64|h&170)));i=w+(I[p+24384|0]<<2)|0;t=H[i>>2];c=H[t>>2];b=b-c|0}kg:{if(d>>>16>>>0>>0){h=H[t+4>>2];n=b>>>0>>0;H[i>>2]=H[(n?8:12)+t>>2];while(1){lg:{if(e){break lg}e=H[f+16>>2];b=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break lg}H[f+16>>2]=b;d=(l<<9)+d|0;e=7;break lg}H[f+16>>2]=b;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;c=c<<1;if(c>>>0<32768){continue}break}b=c;n=n?h:!h;break kg}d=d-(c<<16)|0;if(!(b&32768)){h=H[t+4>>2];c=b>>>0>>0;H[i>>2]=H[(c?12:8)+t>>2];while(1){mg:{if(e){break mg}e=H[f+16>>2];n=e+1|0;l=I[e+1|0];if(I[e|0]==255){if(l>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;d=d+65280|0;e=8;break mg}H[f+16>>2]=n;d=(l<<9)+d|0;e=7;break mg}H[f+16>>2]=n;e=8;d=(l<<8)+d|0}e=e-1|0;d=d<<1;b=b<<1;if(b>>>0<32768){continue}break}n=c?!h:h;break kg}n=H[t+4>>2]}c=I[p+24640|0];H[o+768>>2]=(n|0)==(c|0)?s:u;H[m>>2]=H[m>>2]|16384;H[j+4>>2]=H[j+4>>2]|4096;H[j+260>>2]=H[j+260>>2]|4;H[j+268>>2]=H[j+268>>2]|1;c=c^n;H[j+264>>2]=H[j+264>>2]|c<<18|2;g=c<<28|g|8192}H[j>>2]=g&-1226833921}g=j+4|0;c=o+4|0;r=r+1|0;if((r|0)!=64){continue}break}g=j+12|0;c=o+772|0;n=x>>>0<60;x=x+4|0;if(n){continue}break}}H[f+8>>2]=e;H[f+4>>2]=b;H[f>>2]=d;H[f+104>>2]=i}ng:{if(!(V&32)){break ng}H[f+104>>2]=f+100;g=H[f+100>>2];b=H[g>>2];d=H[f+4>>2]-b|0;H[f+4>>2]=d;e=H[f>>2];og:{if(e>>>16>>>0>>0){H[f+4>>2]=b;g=H[(b>>>0>d>>>0?8:12)+g>>2];H[f+100>>2]=g;d=H[f+8>>2];while(1){pg:{if(d){break pg}k=H[f+16>>2];c=k+1|0;i=I[k+1|0];if(I[k|0]==255){if(i>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;d=8;break pg}H[f+16>>2]=c;e=(i<<9)+e|0;d=7;break pg}H[f+16>>2]=c;d=8;e=(i<<8)+e|0}d=d-1|0;H[f+8>>2]=d;e=e<<1;H[f>>2]=e;b=b<<1;H[f+4>>2]=b;if(b>>>0<32768){continue}break}d=b;break og}e=e-(b<<16)|0;H[f>>2]=e;if(d&32768){break og}g=H[(b>>>0>d>>>0?12:8)+g>>2];H[f+100>>2]=g;b=H[f+8>>2];while(1){qg:{if(b){break qg}c=H[f+16>>2];b=c+1|0;k=I[c+1|0];if(I[c|0]==255){if(k>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;b=8;break qg}H[f+16>>2]=b;e=(k<<9)+e|0;b=7;break qg}H[f+16>>2]=b;b=8;e=(k<<8)+e|0}b=b-1|0;H[f+8>>2]=b;e=e<<1;H[f>>2]=e;d=d<<1;H[f+4>>2]=d;if(d>>>0<32768){continue}break}}b=H[g>>2];d=d-b|0;H[f+4>>2]=d;rg:{if(e>>>16>>>0>>0){H[f+4>>2]=b;g=H[(b>>>0>d>>>0?8:12)+g>>2];H[f+100>>2]=g;d=H[f+8>>2];while(1){sg:{if(d){break sg}k=H[f+16>>2];c=k+1|0;i=I[k+1|0];if(I[k|0]==255){if(i>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;d=8;break sg}H[f+16>>2]=c;e=(i<<9)+e|0;d=7;break sg}H[f+16>>2]=c;d=8;e=(i<<8)+e|0}d=d-1|0;H[f+8>>2]=d;e=e<<1;H[f>>2]=e;b=b<<1;H[f+4>>2]=b;if(b>>>0<32768){continue}break}d=b;break rg}e=e-(b<<16)|0;H[f>>2]=e;if(d&32768){break rg}g=H[(b>>>0>d>>>0?12:8)+g>>2];H[f+100>>2]=g;b=H[f+8>>2];while(1){tg:{if(b){break tg}c=H[f+16>>2];b=c+1|0;k=I[c+1|0];if(I[c|0]==255){if(k>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;b=8;break tg}H[f+16>>2]=b;e=(k<<9)+e|0;b=7;break tg}H[f+16>>2]=b;b=8;e=(k<<8)+e|0}b=b-1|0;H[f+8>>2]=b;e=e<<1;H[f>>2]=e;d=d<<1;H[f+4>>2]=d;if(d>>>0<32768){continue}break}}b=H[g>>2];d=d-b|0;H[f+4>>2]=d;ug:{if(e>>>16>>>0>>0){H[f+4>>2]=b;g=H[(b>>>0>d>>>0?8:12)+g>>2];H[f+100>>2]=g;d=H[f+8>>2];while(1){vg:{if(d){break vg}k=H[f+16>>2];c=k+1|0;i=I[k+1|0];if(I[k|0]==255){if(i>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;d=8;break vg}H[f+16>>2]=c;e=(i<<9)+e|0;d=7;break vg}H[f+16>>2]=c;d=8;e=(i<<8)+e|0}d=d-1|0;H[f+8>>2]=d;e=e<<1;H[f>>2]=e;b=b<<1;H[f+4>>2]=b;if(b>>>0<32768){continue}break}d=b;break ug}e=e-(b<<16)|0;H[f>>2]=e;if(d&32768){break ug}g=H[(b>>>0>d>>>0?12:8)+g>>2];H[f+100>>2]=g;b=H[f+8>>2];while(1){wg:{if(b){break wg}c=H[f+16>>2];b=c+1|0;k=I[c+1|0];if(I[c|0]==255){if(k>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;b=8;break wg}H[f+16>>2]=b;e=(k<<9)+e|0;b=7;break wg}H[f+16>>2]=b;b=8;e=(k<<8)+e|0}b=b-1|0;H[f+8>>2]=b;e=e<<1;H[f>>2]=e;d=d<<1;H[f+4>>2]=d;if(d>>>0<32768){continue}break}}b=H[g>>2];d=d-b|0;H[f+4>>2]=d;if(e>>>16>>>0>>0){H[f+4>>2]=b;H[f+100>>2]=H[(b>>>0>d>>>0?8:12)+g>>2];d=H[f+8>>2];while(1){xg:{if(d){break xg}g=H[f+16>>2];c=g+1|0;k=I[g+1|0];if(I[g|0]==255){if(k>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;e=e+65280|0;d=8;break xg}H[f+16>>2]=c;e=(k<<9)+e|0;d=7;break xg}H[f+16>>2]=c;d=8;e=(k<<8)+e|0}d=d-1|0;H[f+8>>2]=d;e=e<<1;H[f>>2]=e;b=b<<1;H[f+4>>2]=b;if(b>>>0<32768){continue}break}break ng}c=e-(b<<16)|0;H[f>>2]=c;if(d&32768){break ng}H[f+100>>2]=H[(b>>>0>d>>>0?12:8)+g>>2];e=H[f+8>>2];while(1){yg:{if(e){break yg}g=H[f+16>>2];b=g+1|0;k=I[g+1|0];if(I[g|0]==255){if(k>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;c=c+65280|0;e=8;break yg}H[f+16>>2]=b;c=(k<<9)+c|0;e=7;break yg}H[f+16>>2]=b;e=8;c=(k<<8)+c|0}e=e-1|0;H[f+8>>2]=e;c=c<<1;H[f>>2]=c;d=d<<1;H[f+4>>2]=d;if(d>>>0<32768){continue}break}}break cb;case 0:break eb}}if(ea){r=1<>>1|0;u=H[f+124>>2];d=u<<2;b=(d+H[f+120>>2]|0)+12|0;g=H[f+116>>2];j=0;c=H[f+128>>2];if(c>>>0>=4){if(!u){break bb}h=N(u,12);n=u<<3;m=0-r|0;while(1){c=0;while(1){k=b;b=H[b>>2];zg:{if(!b){break zg}if((b&2097168)==16){b=H[f>>2];e=H[f+8>>2];Ag:{if(e){break Ag}q=(b|0)!=255;i=H[f+16>>2];b=I[i|0];Bg:{if(q){e=8}else{if(b>>>0>143){break Bg}e=7}H[f>>2]=b;H[f+16>>2]=i+1;break Ag}e=8;b=255}i=e-1|0;H[f+8>>2]=i;i=b>>>i&1;b=H[g>>2];H[g>>2]=((i|0)==(b>>>31|0)?m:r)+b;b=H[k>>2]|1048576;H[k>>2]=b}if((b&16777344)==128){b=H[f>>2];e=H[f+8>>2];Cg:{if(e){break Cg}q=(b|0)!=255;i=H[f+16>>2];b=I[i|0];Dg:{if(q){e=8}else{if(b>>>0>143){break Dg}e=7}H[f>>2]=b;H[f+16>>2]=i+1;break Cg}e=8;b=255}q=e-1|0;H[f+8>>2]=q;i=d+g|0;o=H[i>>2];H[i>>2]=o+((b>>>q&1)==(o>>>31|0)?m:r);b=H[k>>2]|8388608;H[k>>2]=b}if((b&134218752)==1024){b=H[f>>2];e=H[f+8>>2];Eg:{if(e){break Eg}q=(b|0)!=255;i=H[f+16>>2];b=I[i|0];Fg:{if(q){e=8}else{if(b>>>0>143){break Fg}e=7}H[f>>2]=b;H[f+16>>2]=i+1;break Eg}e=8;b=255}q=e-1|0;H[f+8>>2]=q;i=g+n|0;o=H[i>>2];H[i>>2]=o+((b>>>q&1)==(o>>>31|0)?m:r);b=H[k>>2]|67108864;H[k>>2]=b}if((b&1073750016)!=8192){break zg}b=H[f>>2];e=H[f+8>>2];Gg:{if(e){break Gg}q=(b|0)!=255;i=H[f+16>>2];b=I[i|0];Hg:{if(q){e=8}else{if(b>>>0>143){break Hg}e=7}H[f>>2]=b;H[f+16>>2]=i+1;break Gg}e=8;b=255}q=e-1|0;H[f+8>>2]=q;i=h+g|0;o=H[i>>2];H[i>>2]=o+((b>>>q&1)==(o>>>31|0)?m:r);H[k>>2]=H[k>>2]|536870912}g=g+4|0;b=k+4|0;c=c+1|0;if((u|0)!=(c|0)){continue}break}g=h+g|0;b=k+12|0;j=j+4|0;c=H[f+128>>2];if(j>>>0<(c&-4)>>>0){continue}break}}if(!u|c>>>0<=j>>>0){break cb}p=0;q=0-r|0;d=c;while(1){Ig:{if((d|0)==(j|0)){d=j;break Ig}e=H[b>>2];t=0;while(1){d=N(t,3);if((2097168<>2];l=H[f+8>>2];Jg:{if(l){break Jg}i=(c|0)!=255;k=H[f+16>>2];c=I[k|0];Kg:{if(i){l=8}else{if(c>>>0>143){break Kg}l=7}H[f>>2]=c;H[f+16>>2]=k+1;break Jg}l=8;c=255}k=l-1|0;H[f+8>>2]=k;k=c>>>k&1;c=H[n>>2];H[n>>2]=((k|0)==(c>>>31|0)?q:r)+c;e=H[b>>2]|1048576<>2]=e;c=H[f+128>>2]}t=t+1|0;d=c;if(t>>>0>>0){continue}break}}g=g+4|0;b=b+4|0;p=p+1|0;if((u|0)!=(p|0)){continue}break}break cb}k=H[f+120>>2];d=H[f+116>>2];w=H[f+124>>2];c=H[f+128>>2];if(!((w|0)!=64|(c|0)!=64)){c=k+268|0;x=0;p=1<>>1|0;m=0-p|0;t=H[f+8>>2];g=H[f+4>>2];b=H[f>>2];j=H[f+104>>2];while(1){l=0;while(1){q=d;i=c;d=H[c>>2];if(d){k=c;if((d&2097168)==16){j=ca+((d&1048576?16:d&495?15:14)<<2)|0;o=H[j>>2];c=H[o>>2];g=g-c|0;Lg:{if(b>>>16>>>0>>0){e=H[o+4>>2];n=c>>>0>g>>>0;H[j>>2]=H[o+(n?8:12)>>2];while(1){Mg:{if(t){break Mg}o=H[f+16>>2];g=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Mg}H[f+16>>2]=g;b=(h<<9)+b|0;t=7;break Mg}H[f+16>>2]=g;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;n=n?e:!e;break Lg}b=b-(c<<16)|0;if(!(g&32768)){e=H[o+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[o+(c?12:8)>>2];while(1){Ng:{if(t){break Ng}o=H[f+16>>2];n=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Ng}H[f+16>>2]=n;b=(h<<9)+b|0;t=7;break Ng}H[f+16>>2]=n;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}n=c?!e:e;break Lg}n=H[o+4>>2]}c=H[q>>2];H[q>>2]=((n|0)==(c>>>31|0)?m:p)+c;d=d|1048576}if((d&16777344)==128){j=ca+((d&8388608?16:d&3960?15:14)<<2)|0;o=H[j>>2];c=H[o>>2];g=g-c|0;Og:{if(b>>>16>>>0>>0){e=H[o+4>>2];n=c>>>0>g>>>0;H[j>>2]=H[o+(n?8:12)>>2];while(1){Pg:{if(t){break Pg}o=H[f+16>>2];g=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Pg}H[f+16>>2]=g;b=(h<<9)+b|0;t=7;break Pg}H[f+16>>2]=g;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;n=n?e:!e;break Og}b=b-(c<<16)|0;if(!(g&32768)){e=H[o+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[o+(c?12:8)>>2];while(1){Qg:{if(t){break Qg}o=H[f+16>>2];n=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Qg}H[f+16>>2]=n;b=(h<<9)+b|0;t=7;break Qg}H[f+16>>2]=n;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}n=c?!e:e;break Og}n=H[o+4>>2]}c=H[q+256>>2];H[q+256>>2]=((n|0)==(c>>>31|0)?m:p)+c;d=d|8388608}if((d&134218752)==1024){j=ca+((d&67108864?16:d&31680?15:14)<<2)|0;o=H[j>>2];c=H[o>>2];g=g-c|0;Rg:{if(b>>>16>>>0>>0){e=H[o+4>>2];n=c>>>0>g>>>0;H[j>>2]=H[o+(n?8:12)>>2];while(1){Sg:{if(t){break Sg}o=H[f+16>>2];g=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Sg}H[f+16>>2]=g;b=(h<<9)+b|0;t=7;break Sg}H[f+16>>2]=g;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;n=n?e:!e;break Rg}b=b-(c<<16)|0;if(!(g&32768)){e=H[o+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[o+(c?12:8)>>2];while(1){Tg:{if(t){break Tg}o=H[f+16>>2];n=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Tg}H[f+16>>2]=n;b=(h<<9)+b|0;t=7;break Tg}H[f+16>>2]=n;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}n=c?!e:e;break Rg}n=H[o+4>>2]}c=H[q+512>>2];H[q+512>>2]=((n|0)==(c>>>31|0)?m:p)+c;d=d|67108864}if((d&1073750016)==8192){j=ca+((d&536870912?16:d&253440?15:14)<<2)|0;o=H[j>>2];c=H[o>>2];g=g-c|0;Ug:{if(b>>>16>>>0>>0){e=H[o+4>>2];n=c>>>0>g>>>0;H[j>>2]=H[o+(n?8:12)>>2];while(1){Vg:{if(t){break Vg}o=H[f+16>>2];g=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Vg}H[f+16>>2]=g;b=(h<<9)+b|0;t=7;break Vg}H[f+16>>2]=g;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;n=n?e:!e;break Ug}b=b-(c<<16)|0;if(!(g&32768)){e=H[o+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[o+(c?12:8)>>2];while(1){Wg:{if(t){break Wg}o=H[f+16>>2];n=o+1|0;h=I[o+1|0];if(I[o|0]==255){if(h>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8;break Wg}H[f+16>>2]=n;b=(h<<9)+b|0;t=7;break Wg}H[f+16>>2]=n;t=8;b=(h<<8)+b|0}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}n=c?!e:e;break Ug}n=H[o+4>>2]}c=H[q+768>>2];H[q+768>>2]=((n|0)==(c>>>31|0)?m:p)+c;d=d|536870912}H[k>>2]=d}c=i+4|0;d=q+4|0;l=l+1|0;if((l|0)!=64){continue}break}c=i+12|0;d=q+772|0;k=x>>>0<60;x=x+4|0;if(k){continue}break}H[f+8>>2]=t;H[f+4>>2]=g;H[f>>2]=b;H[f+104>>2]=j;break cb}s=1<>>1|0;e=w<<2;h=(e+k|0)+12|0;t=H[f+8>>2];g=H[f+4>>2];b=H[f>>2];j=H[f+104>>2];o=0;Xg:{if(c>>>0<4){break Xg}if(w){p=N(w,12);n=w<<3;r=0-s|0;while(1){l=0;while(1){k=h;i=H[h>>2];if(i){if((i&2097168)==16){j=ca+((i&1048576?16:i&495?15:14)<<2)|0;h=H[j>>2];c=H[h>>2];g=g-c|0;Yg:{if(b>>>16>>>0>=c>>>0){b=b-(c<<16)|0;q=H[h+4>>2];if(g&32768){break Yg}m=H[h+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[h+(c?12:8)>>2];while(1){Zg:{if(t){break Zg}h=H[f+16>>2];q=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=q;t=8;b=(u<<8)+b|0;break Zg}if(u>>>0<=143){H[f+16>>2]=q;b=(u<<9)+b|0;t=7;break Zg}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}q=c?!m:m;break Yg}m=H[h+4>>2];q=c>>>0>g>>>0;H[j>>2]=H[h+(q?8:12)>>2];while(1){_g:{if(t){break _g}h=H[f+16>>2];g=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=g;t=8;b=(u<<8)+b|0;break _g}if(u>>>0<=143){H[f+16>>2]=g;b=(u<<9)+b|0;t=7;break _g}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;q=q?m:!m}c=H[d>>2];H[d>>2]=((q|0)==(c>>>31|0)?r:s)+c;i=i|1048576}if((i&16777344)==128){j=ca+((i&8388608?16:i&3960?15:14)<<2)|0;h=H[j>>2];c=H[h>>2];g=g-c|0;$g:{if(b>>>16>>>0>=c>>>0){b=b-(c<<16)|0;q=H[h+4>>2];if(g&32768){break $g}m=H[h+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[h+(c?12:8)>>2];while(1){ah:{if(t){break ah}h=H[f+16>>2];q=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=q;t=8;b=(u<<8)+b|0;break ah}if(u>>>0<=143){H[f+16>>2]=q;b=(u<<9)+b|0;t=7;break ah}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}q=c?!m:m;break $g}m=H[h+4>>2];q=c>>>0>g>>>0;H[j>>2]=H[h+(q?8:12)>>2];while(1){bh:{if(t){break bh}h=H[f+16>>2];g=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=g;t=8;b=(u<<8)+b|0;break bh}if(u>>>0<=143){H[f+16>>2]=g;b=(u<<9)+b|0;t=7;break bh}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;q=q?m:!m}h=q;c=d+e|0;q=H[c>>2];H[c>>2]=q+((h|0)==(q>>>31|0)?r:s);i=i|8388608}if((i&134218752)==1024){j=ca+((i&67108864?16:i&31680?15:14)<<2)|0;h=H[j>>2];c=H[h>>2];g=g-c|0;ch:{if(b>>>16>>>0>=c>>>0){b=b-(c<<16)|0;q=H[h+4>>2];if(g&32768){break ch}m=H[h+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[h+(c?12:8)>>2];while(1){dh:{if(t){break dh}h=H[f+16>>2];q=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=q;t=8;b=(u<<8)+b|0;break dh}if(u>>>0<=143){H[f+16>>2]=q;b=(u<<9)+b|0;t=7;break dh}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}q=c?!m:m;break ch}m=H[h+4>>2];q=c>>>0>g>>>0;H[j>>2]=H[h+(q?8:12)>>2];while(1){eh:{if(t){break eh}h=H[f+16>>2];g=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=g;t=8;b=(u<<8)+b|0;break eh}if(u>>>0<=143){H[f+16>>2]=g;b=(u<<9)+b|0;t=7;break eh}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;q=q?m:!m}h=q;c=d+n|0;q=H[c>>2];H[c>>2]=q+((h|0)==(q>>>31|0)?r:s);i=i|67108864}if((i&1073750016)==8192){j=ca+((i&536870912?16:i&253440?15:14)<<2)|0;h=H[j>>2];c=H[h>>2];g=g-c|0;fh:{if(b>>>16>>>0>=c>>>0){b=b-(c<<16)|0;q=H[h+4>>2];if(g&32768){break fh}m=H[h+4>>2];c=c>>>0>g>>>0;H[j>>2]=H[h+(c?12:8)>>2];while(1){gh:{if(t){break gh}h=H[f+16>>2];q=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=q;t=8;b=(u<<8)+b|0;break gh}if(u>>>0<=143){H[f+16>>2]=q;b=(u<<9)+b|0;t=7;break gh}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;g=g<<1;if(g>>>0<32768){continue}break}q=c?!m:m;break fh}m=H[h+4>>2];q=c>>>0>g>>>0;H[j>>2]=H[h+(q?8:12)>>2];while(1){hh:{if(t){break hh}h=H[f+16>>2];g=h+1|0;u=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=g;t=8;b=(u<<8)+b|0;break hh}if(u>>>0<=143){H[f+16>>2]=g;b=(u<<9)+b|0;t=7;break hh}H[f+12>>2]=H[f+12>>2]+1;b=b+65280|0;t=8}t=t-1|0;b=b<<1;c=c<<1;if(c>>>0<32768){continue}break}g=c;q=q?m:!m}h=q;c=d+p|0;q=H[c>>2];H[c>>2]=q+((h|0)==(q>>>31|0)?r:s);i=i|536870912}H[k>>2]=i}h=k+4|0;d=d+4|0;l=l+1|0;if((w|0)!=(l|0)){continue}break}h=k+12|0;d=d+p|0;o=o+4|0;c=H[f+128>>2];if(o>>>0<(c&-4)>>>0){continue}break}break Xg}o=c&-4;h=(k+(o<<1)|0)+12|0}H[f+8>>2]=t;H[f+4>>2]=g;H[f>>2]=b;H[f+104>>2]=j;if(!w|c>>>0<=o>>>0){break cb}x=0;k=0-s|0;b=c;while(1){ih:{if((b|0)==(o|0)){b=o;break ih}t=H[h>>2];e=0;while(1){l=N(e,3);if((2097168<>>l|0;i=ca+((b&1048576?16:b&495?15:14)<<2)|0;H[f+104>>2]=i;q=H[i>>2];b=H[q>>2];c=H[f+4>>2]-b|0;H[f+4>>2]=c;g=H[f>>2];jh:{if(g>>>16>>>0>>0){n=H[q+4>>2];H[f+4>>2]=b;c=b>>>0>c>>>0;H[i>>2]=H[q+(c?8:12)>>2];t=H[f+8>>2];while(1){kh:{if(t){break kh}q=H[f+16>>2];i=q+1|0;m=I[q+1|0];if(I[q|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;g=g+65280|0;t=8;break kh}H[f+16>>2]=i;g=(m<<9)+g|0;t=7;break kh}H[f+16>>2]=i;t=8;g=(m<<8)+g|0}t=t-1|0;H[f+8>>2]=t;g=g<<1;H[f>>2]=g;b=b<<1;H[f+4>>2]=b;if(b>>>0<32768){continue}break}c=c?n:!n;break jh}g=g-(b<<16)|0;H[f>>2]=g;if(!(c&32768)){n=H[q+4>>2];b=b>>>0>c>>>0;H[i>>2]=H[q+(b?12:8)>>2];t=H[f+8>>2];while(1){lh:{if(t){break lh}q=H[f+16>>2];i=q+1|0;m=I[q+1|0];if(I[q|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;g=g+65280|0;t=8;break lh}H[f+16>>2]=i;g=(m<<9)+g|0;t=7;break lh}H[f+16>>2]=i;t=8;g=(m<<8)+g|0}t=t-1|0;H[f+8>>2]=t;g=g<<1;H[f>>2]=g;c=c<<1;H[f+4>>2]=c;if(c>>>0<32768){continue}break}c=b?!n:n;break jh}c=H[q+4>>2]}b=H[j>>2];H[j>>2]=((c|0)==(b>>>31|0)?k:s)+b;t=H[h>>2]|1048576<>2]=t;c=H[f+128>>2]}e=e+1|0;b=c;if(e>>>0>>0){continue}break}}h=h+4|0;d=d+4|0;x=x+1|0;if((w|0)!=(x|0)){continue}break}break cb}i=0;x=0;p=0;mh:{nh:{oh:{M=H[f+124>>2];if(!((M|0)!=64|H[f+128>>2]!=64)){b=1<>>1|b;k=0-i|0;s=f+28|0;g=H[f+120>>2]+268|0;h=H[f+8>>2];c=H[f+4>>2];j=H[f>>2];l=H[f+104>>2];b=H[f+116>>2];if(V&8){break oh}while(1){p=0;while(1){q=b;n=g;g=H[g>>2];if(g){ph:{if(g&2097168){break ph}b=g&495;if(!b){break ph}l=s+(I[b+H[f+108>>2]|0]<<2)|0;o=H[l>>2];b=H[o>>2];c=c-b|0;qh:{if(j>>>16>>>0>>0){e=H[o+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[o+(d?8:12)>>2];while(1){rh:{if(h){break rh}o=H[f+16>>2];c=o+1|0;m=I[o+1|0];if(I[o|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break rh}H[f+16>>2]=c;j=(m<<9)+j|0;h=7;break rh}H[f+16>>2]=c;h=8;j=(m<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?e:!e;break qh}j=j-(b<<16)|0;if(!(c&32768)){e=H[o+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[o+(b?12:8)>>2];while(1){sh:{if(h){break sh}o=H[f+16>>2];d=o+1|0;m=I[o+1|0];if(I[o|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break sh}H[f+16>>2]=d;j=(m<<9)+j|0;h=7;break sh}H[f+16>>2]=d;h=8;j=(m<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!e:e;break qh}b=H[o+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>17&4|(H[u>>2]>>>19&1|(g>>>14&16|(g>>>16&64|g&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];th:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){uh:{if(h){break uh}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break uh}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break uh}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?m:!m;break th}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){vh:{if(h){break vh}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break vh}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break vh}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!m:m;break th}d=H[e+4>>2]}H[q>>2]=(o|0)==(d|0)?i:k;H[u>>2]=H[u>>2]|32;H[n+4>>2]=H[n+4>>2]|8;b=n-268|0;H[b>>2]=H[b>>2]|131072;b=n-260|0;H[b>>2]=H[b>>2]|32768;b=n-264|0;u=b;e=H[b>>2];b=d^o;H[u>>2]=e|b<<31|65536;g=b<<19|g|16}g=g|2097152}if(!(!(g&3960)|g&16777344)){o=g>>>3|0;l=s+(I[H[f+108>>2]+(o&495)|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;wh:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){xh:{if(h){break xh}e=H[f+16>>2];c=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break xh}H[f+16>>2]=c;j=(u<<9)+j|0;h=7;break xh}H[f+16>>2]=c;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break wh}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){yh:{if(h){break yh}e=H[f+16>>2];d=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break yh}H[f+16>>2]=d;j=(u<<9)+j|0;h=7;break yh}H[f+16>>2]=d;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break wh}b=H[e+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>20&4|(H[u>>2]>>>22&1|(g>>>15&16|(g>>>19&64|o&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];zh:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Ah:{if(h){break Ah}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Ah}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Ah}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break zh}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Bh:{if(h){break Bh}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Bh}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Bh}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break zh}b=H[e+4>>2]}H[q+256>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|256;H[n+4>>2]=H[n+4>>2]|64;g=(b^o)<<22|g|128}g=g|16777216}if(!(!(g&31680)|g&134218752)){o=g>>>6|0;l=s+(I[H[f+108>>2]+(o&495)|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;Ch:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Dh:{if(h){break Dh}e=H[f+16>>2];c=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Dh}H[f+16>>2]=c;j=(u<<9)+j|0;h=7;break Dh}H[f+16>>2]=c;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Ch}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Eh:{if(h){break Eh}e=H[f+16>>2];d=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Eh}H[f+16>>2]=d;j=(u<<9)+j|0;h=7;break Eh}H[f+16>>2]=d;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Ch}b=H[e+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>23&4|(H[u>>2]>>>25&1|(g>>>18&16|(g>>>22&64|o&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];Fh:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Gh:{if(h){break Gh}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Gh}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Gh}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Fh}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Hh:{if(h){break Hh}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Hh}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Hh}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Fh}b=H[e+4>>2]}H[q+512>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|2048;H[n+4>>2]=H[n+4>>2]|512;g=(b^o)<<25|g|1024}g=g|134217728}if(!(!(g&253440)|g&1073750016)){o=g>>>9|0;l=s+(I[H[f+108>>2]+(o&495)|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;Ih:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Jh:{if(h){break Jh}e=H[f+16>>2];c=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Jh}H[f+16>>2]=c;j=(u<<9)+j|0;h=7;break Jh}H[f+16>>2]=c;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Ih}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Kh:{if(h){break Kh}e=H[f+16>>2];d=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Kh}H[f+16>>2]=d;j=(u<<9)+j|0;h=7;break Kh}H[f+16>>2]=d;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Ih}b=H[e+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>26&4|(H[u>>2]>>>28&1|(g>>>21&16|(g>>>25&64|o&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];Lh:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Mh:{if(h){break Mh}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Mh}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Mh}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Lh}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Nh:{if(h){break Nh}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Nh}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Nh}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Lh}b=H[e+4>>2]}H[q+768>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|16384;H[n+4>>2]=H[n+4>>2]|4096;H[n+260>>2]=H[n+260>>2]|4;H[n+268>>2]=H[n+268>>2]|1;b=b^o;H[n+264>>2]=H[n+264>>2]|b<<18|2;g=b<<28|g|8192}g=g|1073741824}H[n>>2]=g}g=n+4|0;b=q+4|0;p=p+1|0;if((p|0)!=64){continue}break}g=n+12|0;b=q+772|0;q=x>>>0<60;x=x+4|0;if(q){continue}break}break nh}b=1<>>1|b;q=H[f+120>>2];g=(q+(M<<2)|0)+12|0;b=H[f+128>>2];h=H[f+8>>2];c=H[f+4>>2];j=H[f>>2];l=H[f+104>>2];o=H[f+116>>2];Oh:{if(V&8){Ph:{if(b>>>0<4){break Ph}if(M){w=N(M,12);u=M<<3;q=0-k|0;D=f+28|0;while(1){x=0;while(1){n=g;g=H[g>>2];if(g){Qh:{if(g&2097168){break Qh}b=g&495;if(!b){break Qh}l=D+(I[b+H[f+108>>2]|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;Rh:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[e+4>>2];if(c&32768){break Rh}m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Sh:{if(h){break Sh}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break Sh}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Sh}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break Rh}m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Th:{if(h){break Th}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break Th}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Th}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){r=n-4|0;d=H[n+4>>2]>>>17&4|(H[r>>2]>>>19&1|(g>>>14&16|(g>>>16&64|g&170)));l=D+(I[d+24384|0]<<2)|0;s=H[l>>2];b=H[s>>2];c=c-b|0;e=I[d+24640|0];Uh:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;v=H[s+4>>2];if(c&32768){break Uh}m=H[s+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[s+(b?12:8)>>2];while(1){Vh:{if(h){break Vh}h=H[f+16>>2];d=h+1|0;s=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(s<<8)+j|0;break Vh}if(s>>>0<=143){H[f+16>>2]=d;j=(s<<9)+j|0;h=7;break Vh}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}v=b?!m:m;break Uh}m=H[s+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[s+(d?8:12)>>2];while(1){Wh:{if(h){break Wh}h=H[f+16>>2];c=h+1|0;s=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(s<<8)+j|0;break Wh}if(s>>>0<=143){H[f+16>>2]=c;j=(s<<9)+j|0;h=7;break Wh}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;v=d?m:!m}b=v;H[o>>2]=(e|0)==(b|0)?k:q;H[r>>2]=H[r>>2]|32;H[n+4>>2]=H[n+4>>2]|8;g=(b^e)<<19|g|16}g=g|2097152}if(!(!(g&3960)|g&16777344)){e=g>>>3|0;l=D+(I[H[f+108>>2]+(e&495)|0]<<2)|0;r=H[l>>2];b=H[r>>2];c=c-b|0;Xh:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[r+4>>2];if(c&32768){break Xh}m=H[r+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[r+(b?12:8)>>2];while(1){Yh:{if(h){break Yh}h=H[f+16>>2];d=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break Yh}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Yh}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break Xh}m=H[r+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[r+(d?8:12)>>2];while(1){Zh:{if(h){break Zh}h=H[f+16>>2];c=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break Zh}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Zh}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){s=n-4|0;d=H[n+4>>2]>>>20&4|(H[s>>2]>>>22&1|(g>>>15&16|(g>>>19&64|e&170)));l=D+(I[d+24384|0]<<2)|0;v=H[l>>2];b=H[v>>2];c=c-b|0;m=(M<<2)+o|0;e=I[d+24640|0];_h:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[v+4>>2];if(c&32768){break _h}r=H[v+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[v+(b?12:8)>>2];while(1){$h:{if(h){break $h}h=H[f+16>>2];d=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(v<<8)+j|0;break $h}if(v>>>0<=143){H[f+16>>2]=d;j=(v<<9)+j|0;h=7;break $h}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!r:r;break _h}r=H[v+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[v+(d?8:12)>>2];while(1){ai:{if(h){break ai}h=H[f+16>>2];c=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(v<<8)+j|0;break ai}if(v>>>0<=143){H[f+16>>2]=c;j=(v<<9)+j|0;h=7;break ai}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?r:!r}b=d;H[m>>2]=(e|0)==(b|0)?k:q;H[s>>2]=H[s>>2]|256;H[n+4>>2]=H[n+4>>2]|64;g=(b^e)<<22|g|128}g=g|16777216}if(!(!(g&31680)|g&134218752)){e=g>>>6|0;l=D+(I[H[f+108>>2]+(e&495)|0]<<2)|0;r=H[l>>2];b=H[r>>2];c=c-b|0;bi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[r+4>>2];if(c&32768){break bi}m=H[r+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[r+(b?12:8)>>2];while(1){ci:{if(h){break ci}h=H[f+16>>2];d=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break ci}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break ci}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break bi}m=H[r+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[r+(d?8:12)>>2];while(1){di:{if(h){break di}h=H[f+16>>2];c=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break di}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break di}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){s=n-4|0;d=H[n+4>>2]>>>23&4|(H[s>>2]>>>25&1|(g>>>18&16|(g>>>22&64|e&170)));l=D+(I[d+24384|0]<<2)|0;v=H[l>>2];b=H[v>>2];c=c-b|0;m=o+u|0;e=I[d+24640|0];ei:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[v+4>>2];if(c&32768){break ei}r=H[v+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[v+(b?12:8)>>2];while(1){fi:{if(h){break fi}h=H[f+16>>2];d=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(v<<8)+j|0;break fi}if(v>>>0<=143){H[f+16>>2]=d;j=(v<<9)+j|0;h=7;break fi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!r:r;break ei}r=H[v+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[v+(d?8:12)>>2];while(1){gi:{if(h){break gi}h=H[f+16>>2];c=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(v<<8)+j|0;break gi}if(v>>>0<=143){H[f+16>>2]=c;j=(v<<9)+j|0;h=7;break gi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?r:!r}b=d;H[m>>2]=(e|0)==(b|0)?k:q;H[s>>2]=H[s>>2]|2048;H[n+4>>2]=H[n+4>>2]|512;g=(b^e)<<25|g|1024}g=g|134217728}if(!(!(g&253440)|g&1073750016)){e=g>>>9|0;l=D+(I[H[f+108>>2]+(e&495)|0]<<2)|0;r=H[l>>2];b=H[r>>2];c=c-b|0;hi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[r+4>>2];if(c&32768){break hi}m=H[r+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[r+(b?12:8)>>2];while(1){ii:{if(h){break ii}h=H[f+16>>2];d=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break ii}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break ii}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break hi}m=H[r+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[r+(d?8:12)>>2];while(1){ji:{if(h){break ji}h=H[f+16>>2];c=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break ji}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break ji}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){s=n-4|0;d=H[n+4>>2]>>>26&4|(H[s>>2]>>>28&1|(g>>>21&16|(g>>>25&64|e&170)));l=D+(I[d+24384|0]<<2)|0;v=H[l>>2];b=H[v>>2];c=c-b|0;m=o+w|0;e=I[d+24640|0];ki:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[v+4>>2];if(c&32768){break ki}r=H[v+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[v+(b?12:8)>>2];while(1){li:{if(h){break li}h=H[f+16>>2];d=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(v<<8)+j|0;break li}if(v>>>0<=143){H[f+16>>2]=d;j=(v<<9)+j|0;h=7;break li}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!r:r;break ki}r=H[v+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[v+(d?8:12)>>2];while(1){mi:{if(h){break mi}h=H[f+16>>2];c=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(v<<8)+j|0;break mi}if(v>>>0<=143){H[f+16>>2]=c;j=(v<<9)+j|0;h=7;break mi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?r:!r}b=d;H[m>>2]=(e|0)==(b|0)?k:q;H[s>>2]=H[s>>2]|16384;H[n+4>>2]=H[n+4>>2]|4096;d=n+(H[f+124>>2]<<2)|0;H[d+4>>2]=H[d+4>>2]|4;H[d+12>>2]=H[d+12>>2]|1;b=b^e;H[d+8>>2]=H[d+8>>2]|b<<18|2;g=b<<28|g|8192}g=g|1073741824}H[n>>2]=g}g=n+4|0;o=o+4|0;x=x+1|0;if((M|0)!=(x|0)){continue}break}g=n+12|0;o=o+w|0;i=i+4|0;b=H[f+128>>2];if(i>>>0<(b&-4)>>>0){continue}break}break Ph}i=b&-4;g=(q+(i<<1)|0)+12|0}H[f+8>>2]=h;H[f+4>>2]=c;H[f>>2]=j;H[f+104>>2]=l;if(!M|b>>>0<=i>>>0){break Oh}while(1){c=(b|0)==(i|0);h=0;b=i;if(!c){while(1){Xb(f,g,(N(h,M)<<2)+o|0,k,h,H[f+124>>2]+2|0,1);h=h+1|0;b=H[f+128>>2];if(h>>>0>>0){continue}break}}g=g+4|0;o=o+4|0;p=p+1|0;if((M|0)!=(p|0)){continue}break}break Oh}ni:{if(b>>>0<4){break ni}if(M){w=N(M,12);u=M<<3;q=0-k|0;D=f+28|0;while(1){x=0;while(1){n=g;g=H[g>>2];if(g){oi:{if(g&2097168){break oi}b=g&495;if(!b){break oi}l=D+(I[b+H[f+108>>2]|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;pi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[e+4>>2];if(c&32768){break pi}m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){qi:{if(h){break qi}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break qi}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break qi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break pi}m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){ri:{if(h){break ri}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break ri}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break ri}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){r=n-4|0;d=H[n+4>>2]>>>17&4|(H[r>>2]>>>19&1|(g>>>14&16|(g>>>16&64|g&170)));l=D+(I[d+24384|0]<<2)|0;s=H[l>>2];b=H[s>>2];c=c-b|0;e=I[d+24640|0];si:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[s+4>>2];if(c&32768){break si}m=H[s+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[s+(b?12:8)>>2];while(1){ti:{if(h){break ti}h=H[f+16>>2];d=h+1|0;s=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(s<<8)+j|0;break ti}if(s>>>0<=143){H[f+16>>2]=d;j=(s<<9)+j|0;h=7;break ti}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!m:m;break si}m=H[s+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[s+(d?8:12)>>2];while(1){ui:{if(h){break ui}h=H[f+16>>2];c=h+1|0;s=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(s<<8)+j|0;break ui}if(s>>>0<=143){H[f+16>>2]=c;j=(s<<9)+j|0;h=7;break ui}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?m:!m}H[o>>2]=(e|0)==(d|0)?k:q;H[r>>2]=H[r>>2]|32;H[n+4>>2]=H[n+4>>2]|8;b=n+(-2-H[f+124>>2]<<2)|0;H[b+4>>2]=H[b+4>>2]|32768;d=d^e;H[b>>2]=H[b>>2]|d<<31|65536;b=b-4|0;H[b>>2]=H[b>>2]|131072;g=d<<19|g|16}g=g|2097152}if(!(!(g&3960)|g&16777344)){e=g>>>3|0;l=D+(I[H[f+108>>2]+(e&495)|0]<<2)|0;r=H[l>>2];b=H[r>>2];c=c-b|0;vi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[r+4>>2];if(c&32768){break vi}m=H[r+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[r+(b?12:8)>>2];while(1){wi:{if(h){break wi}h=H[f+16>>2];d=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break wi}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break wi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break vi}m=H[r+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[r+(d?8:12)>>2];while(1){xi:{if(h){break xi}h=H[f+16>>2];c=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break xi}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break xi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){s=n-4|0;d=H[n+4>>2]>>>20&4|(H[s>>2]>>>22&1|(g>>>15&16|(g>>>19&64|e&170)));l=D+(I[d+24384|0]<<2)|0;v=H[l>>2];b=H[v>>2];c=c-b|0;m=(M<<2)+o|0;e=I[d+24640|0];yi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[v+4>>2];if(c&32768){break yi}r=H[v+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[v+(b?12:8)>>2];while(1){zi:{if(h){break zi}h=H[f+16>>2];d=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(v<<8)+j|0;break zi}if(v>>>0<=143){H[f+16>>2]=d;j=(v<<9)+j|0;h=7;break zi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!r:r;break yi}r=H[v+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[v+(d?8:12)>>2];while(1){Ai:{if(h){break Ai}h=H[f+16>>2];c=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(v<<8)+j|0;break Ai}if(v>>>0<=143){H[f+16>>2]=c;j=(v<<9)+j|0;h=7;break Ai}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?r:!r}b=d;H[m>>2]=(e|0)==(b|0)?k:q;H[s>>2]=H[s>>2]|256;H[n+4>>2]=H[n+4>>2]|64;g=(b^e)<<22|g|128}g=g|16777216}if(!(!(g&31680)|g&134218752)){e=g>>>6|0;l=D+(I[H[f+108>>2]+(e&495)|0]<<2)|0;r=H[l>>2];b=H[r>>2];c=c-b|0;Bi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[r+4>>2];if(c&32768){break Bi}m=H[r+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[r+(b?12:8)>>2];while(1){Ci:{if(h){break Ci}h=H[f+16>>2];d=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break Ci}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Ci}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break Bi}m=H[r+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[r+(d?8:12)>>2];while(1){Di:{if(h){break Di}h=H[f+16>>2];c=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break Di}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Di}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){s=n-4|0;d=H[n+4>>2]>>>23&4|(H[s>>2]>>>25&1|(g>>>18&16|(g>>>22&64|e&170)));l=D+(I[d+24384|0]<<2)|0;v=H[l>>2];b=H[v>>2];c=c-b|0;m=o+u|0;e=I[d+24640|0];Ei:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[v+4>>2];if(c&32768){break Ei}r=H[v+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[v+(b?12:8)>>2];while(1){Fi:{if(h){break Fi}h=H[f+16>>2];d=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(v<<8)+j|0;break Fi}if(v>>>0<=143){H[f+16>>2]=d;j=(v<<9)+j|0;h=7;break Fi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!r:r;break Ei}r=H[v+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[v+(d?8:12)>>2];while(1){Gi:{if(h){break Gi}h=H[f+16>>2];c=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(v<<8)+j|0;break Gi}if(v>>>0<=143){H[f+16>>2]=c;j=(v<<9)+j|0;h=7;break Gi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?r:!r}b=d;H[m>>2]=(e|0)==(b|0)?k:q;H[s>>2]=H[s>>2]|2048;H[n+4>>2]=H[n+4>>2]|512;g=(b^e)<<25|g|1024}g=g|134217728}if(!(!(g&253440)|g&1073750016)){e=g>>>9|0;l=D+(I[H[f+108>>2]+(e&495)|0]<<2)|0;r=H[l>>2];b=H[r>>2];c=c-b|0;Hi:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;s=H[r+4>>2];if(c&32768){break Hi}m=H[r+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[r+(b?12:8)>>2];while(1){Ii:{if(h){break Ii}h=H[f+16>>2];d=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(r<<8)+j|0;break Ii}if(r>>>0<=143){H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Ii}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}s=b?!m:m;break Hi}m=H[r+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[r+(d?8:12)>>2];while(1){Ji:{if(h){break Ji}h=H[f+16>>2];c=h+1|0;r=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(r<<8)+j|0;break Ji}if(r>>>0<=143){H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Ji}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;s=d?m:!m}if(s){s=n-4|0;d=H[n+4>>2]>>>26&4|(H[s>>2]>>>28&1|(g>>>21&16|(g>>>25&64|e&170)));l=D+(I[d+24384|0]<<2)|0;v=H[l>>2];b=H[v>>2];c=c-b|0;m=o+w|0;e=I[d+24640|0];Ki:{if(j>>>16>>>0>=b>>>0){j=j-(b<<16)|0;d=H[v+4>>2];if(c&32768){break Ki}r=H[v+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[v+(b?12:8)>>2];while(1){Li:{if(h){break Li}h=H[f+16>>2];d=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=d;h=8;j=(v<<8)+j|0;break Li}if(v>>>0<=143){H[f+16>>2]=d;j=(v<<9)+j|0;h=7;break Li}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}d=b?!r:r;break Ki}r=H[v+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[v+(d?8:12)>>2];while(1){Mi:{if(h){break Mi}h=H[f+16>>2];c=h+1|0;v=I[h+1|0];if(I[h|0]!=255){H[f+16>>2]=c;h=8;j=(v<<8)+j|0;break Mi}if(v>>>0<=143){H[f+16>>2]=c;j=(v<<9)+j|0;h=7;break Mi}H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;d=d?r:!r}b=d;H[m>>2]=(e|0)==(b|0)?k:q;H[s>>2]=H[s>>2]|16384;H[n+4>>2]=H[n+4>>2]|4096;d=n+(H[f+124>>2]<<2)|0;H[d+4>>2]=H[d+4>>2]|4;H[d+12>>2]=H[d+12>>2]|1;b=b^e;H[d+8>>2]=H[d+8>>2]|b<<18|2;g=b<<28|g|8192}g=g|1073741824}H[n>>2]=g}g=n+4|0;o=o+4|0;x=x+1|0;if((M|0)!=(x|0)){continue}break}g=n+12|0;o=o+w|0;i=i+4|0;b=H[f+128>>2];if(i>>>0<(b&-4)>>>0){continue}break}break ni}i=b&-4;g=(q+(i<<1)|0)+12|0}H[f+8>>2]=h;H[f+4>>2]=c;H[f>>2]=j;H[f+104>>2]=l;if(!M|b>>>0<=i>>>0){break Oh}while(1){c=(b|0)==(i|0);h=0;b=i;if(!c){while(1){Xb(f,g,(N(h,M)<<2)+o|0,k,h,H[f+124>>2]+2|0,0);h=h+1|0;b=H[f+128>>2];if(h>>>0>>0){continue}break}}g=g+4|0;o=o+4|0;p=p+1|0;if((M|0)!=(p|0)){continue}break}}break mh}while(1){p=0;while(1){q=b;n=g;g=H[g>>2];if(g){Ni:{if(g&2097168){break Ni}b=g&495;if(!b){break Ni}l=s+(I[b+H[f+108>>2]|0]<<2)|0;o=H[l>>2];b=H[o>>2];c=c-b|0;Oi:{if(j>>>16>>>0>>0){e=H[o+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[o+(d?8:12)>>2];while(1){Pi:{if(h){break Pi}o=H[f+16>>2];c=o+1|0;m=I[o+1|0];if(I[o|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Pi}H[f+16>>2]=c;j=(m<<9)+j|0;h=7;break Pi}H[f+16>>2]=c;h=8;j=(m<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?e:!e;break Oi}j=j-(b<<16)|0;if(!(c&32768)){e=H[o+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[o+(b?12:8)>>2];while(1){Qi:{if(h){break Qi}o=H[f+16>>2];d=o+1|0;m=I[o+1|0];if(I[o|0]==255){if(m>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Qi}H[f+16>>2]=d;j=(m<<9)+j|0;h=7;break Qi}H[f+16>>2]=d;h=8;j=(m<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!e:e;break Oi}b=H[o+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>17&4|(H[u>>2]>>>19&1|(g>>>14&16|(g>>>16&64|g&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];Ri:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Si:{if(h){break Si}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Si}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Si}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Ri}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Ti:{if(h){break Ti}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Ti}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Ti}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Ri}b=H[e+4>>2]}H[q>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|32;H[n+4>>2]=H[n+4>>2]|8;g=(b^o)<<19|g|16}g=g|2097152}if(!(!(g&3960)|g&16777344)){o=g>>>3|0;l=s+(I[H[f+108>>2]+(o&495)|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;Ui:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Vi:{if(h){break Vi}e=H[f+16>>2];c=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Vi}H[f+16>>2]=c;j=(u<<9)+j|0;h=7;break Vi}H[f+16>>2]=c;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Ui}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Wi:{if(h){break Wi}e=H[f+16>>2];d=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Wi}H[f+16>>2]=d;j=(u<<9)+j|0;h=7;break Wi}H[f+16>>2]=d;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Ui}b=H[e+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>20&4|(H[u>>2]>>>22&1|(g>>>15&16|(g>>>19&64|o&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];Xi:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){Yi:{if(h){break Yi}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Yi}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break Yi}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break Xi}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){Zi:{if(h){break Zi}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break Zi}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break Zi}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break Xi}b=H[e+4>>2]}H[q+256>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|256;H[n+4>>2]=H[n+4>>2]|64;g=(b^o)<<22|g|128}g=g|16777216}if(!(!(g&31680)|g&134218752)){o=g>>>6|0;l=s+(I[H[f+108>>2]+(o&495)|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;_i:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){$i:{if(h){break $i}e=H[f+16>>2];c=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break $i}H[f+16>>2]=c;j=(u<<9)+j|0;h=7;break $i}H[f+16>>2]=c;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break _i}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){aj:{if(h){break aj}e=H[f+16>>2];d=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break aj}H[f+16>>2]=d;j=(u<<9)+j|0;h=7;break aj}H[f+16>>2]=d;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break _i}b=H[e+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>23&4|(H[u>>2]>>>25&1|(g>>>18&16|(g>>>22&64|o&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];bj:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){cj:{if(h){break cj}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break cj}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break cj}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break bj}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){dj:{if(h){break dj}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break dj}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break dj}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break bj}b=H[e+4>>2]}H[q+512>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|2048;H[n+4>>2]=H[n+4>>2]|512;g=(b^o)<<25|g|1024}g=g|134217728}if(!(!(g&253440)|g&1073750016)){o=g>>>9|0;l=s+(I[H[f+108>>2]+(o&495)|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;ej:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){fj:{if(h){break fj}e=H[f+16>>2];c=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break fj}H[f+16>>2]=c;j=(u<<9)+j|0;h=7;break fj}H[f+16>>2]=c;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break ej}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){gj:{if(h){break gj}e=H[f+16>>2];d=e+1|0;u=I[e+1|0];if(I[e|0]==255){if(u>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break gj}H[f+16>>2]=d;j=(u<<9)+j|0;h=7;break gj}H[f+16>>2]=d;h=8;j=(u<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break ej}b=H[e+4>>2]}if(b){u=n-4|0;d=H[n+4>>2]>>>26&4|(H[u>>2]>>>28&1|(g>>>21&16|(g>>>25&64|o&170)));l=s+(I[d+24384|0]<<2)|0;e=H[l>>2];b=H[e>>2];c=c-b|0;o=I[d+24640|0];hj:{if(j>>>16>>>0>>0){m=H[e+4>>2];d=b>>>0>c>>>0;H[l>>2]=H[e+(d?8:12)>>2];while(1){ij:{if(h){break ij}e=H[f+16>>2];c=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break ij}H[f+16>>2]=c;j=(r<<9)+j|0;h=7;break ij}H[f+16>>2]=c;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;b=b<<1;if(b>>>0<32768){continue}break}c=b;b=d?m:!m;break hj}j=j-(b<<16)|0;if(!(c&32768)){m=H[e+4>>2];b=b>>>0>c>>>0;H[l>>2]=H[e+(b?12:8)>>2];while(1){jj:{if(h){break jj}e=H[f+16>>2];d=e+1|0;r=I[e+1|0];if(I[e|0]==255){if(r>>>0>=144){H[f+12>>2]=H[f+12>>2]+1;j=j+65280|0;h=8;break jj}H[f+16>>2]=d;j=(r<<9)+j|0;h=7;break jj}H[f+16>>2]=d;h=8;j=(r<<8)+j|0}h=h-1|0;j=j<<1;c=c<<1;if(c>>>0<32768){continue}break}b=b?!m:m;break hj}b=H[e+4>>2]}H[q+768>>2]=(o|0)==(b|0)?i:k;H[u>>2]=H[u>>2]|16384;H[n+4>>2]=H[n+4>>2]|4096;H[n+260>>2]=H[n+260>>2]|4;H[n+268>>2]=H[n+268>>2]|1;b=b^o;H[n+264>>2]=H[n+264>>2]|b<<18|2;g=b<<28|g|8192}g=g|1073741824}H[n>>2]=g}g=n+4|0;b=q+4|0;p=p+1|0;if((p|0)!=64){continue}break}g=n+12|0;b=q+772|0;q=x>>>0<60;x=x+4|0;if(q){continue}break}}H[f+8>>2]=h;H[f+4>>2]=c;H[f>>2]=j;H[f+104>>2]=l}}if(aa){break bb}Yb(f);Xa(f,18,46);Xa(f,17,3);Xa(f,0,4)}b=la+1|0;c=(b|0)==3;la=c?0:b;z=z-c|0;ba=ba+1|0;if(ba>>>0>=K[ma+8>>2]){break ab}if((z|0)>0){continue}break}}U=R+U|0;c=H[f+24>>2];b=J[f+112>>1];F[c|0]=b;F[c+1|0]=b>>>8;X=X+1|0;if(X>>>0>2]){continue}break}}kj:{if(!E){break kj}lj:{c=H[f+24>>2];g=H[f+16>>2];if(c>>>0>g+2>>>0){if(!_){break lj}g=H[f+16>>2];c=H[f+24>>2];b=H[f+20>>2];H[Y+56>>2]=c-b;H[Y+52>>2]=g-b;H[Y+48>>2]=(c-g|0)-2;Ba(S,2,15235,Y+48|0);break kj}b=H[f+12>>2];if(b>>>0<3){break kj}if(_){H[Y+80>>2]=H[f+12>>2];Ba(S,2,7107,Y+80|0);break kj}H[Y+64>>2]=b;Ba(S,2,7107,Y- -64|0);break kj}b=H[f+20>>2];H[Y+40>>2]=c-b;H[Y+36>>2]=g-b;H[Y+32>>2]=(c-g|0)-2;Ba(S,2,15235,Y+32|0)}if(!H[C+60>>2]){break i}H[f+116>>2]=Z}k=H[qa+4>>2];g=H[C+12>>2];l=H[C+8>>2]-H[qa>>2]|0;c=H[qa+16>>2];if(c&1){b=H[oa+28>>2]+N(ua,152)|0;l=(H[b-144>>2]+l|0)-H[b-152>>2]|0}i=g-k|0;if(c&2){b=H[oa+28>>2]+N(ua,152)|0;i=(H[b-140>>2]+i|0)-H[b-148>>2]|0}d=H[C+60>>2];t=d?d:H[f+116>>2];p=H[f+128>>2];u=H[f+124>>2];n=H[pa+808>>2];mj:{if(!n){break mj}b=!p|!u;if((n|0)<=30){if(b){break mj}e=0;while(1){k=(N(e,u)<<2)+t|0;b=0;while(1){g=k+(b<<2)|0;q=H[g>>2];c=q>>31;c=(c^q)-c|0;if(c>>>n|0){c=c>>>H[pa+808>>2]|0;H[g>>2]=(q|0)<0?0-c|0:c}b=b+1|0;if((u|0)!=(b|0)){continue}break}e=e+1|0;if((p|0)!=(e|0)){continue}break}break mj}if(b){break mj}b=N(p,u)<<2;if(!b){break mj}y(t,0,b)}if(d){b=N(p,u);if(H[pa+20>>2]==1){if(!b){break a}f=0;if((b|0)!=1){c=b&1;b=b&-2;g=0;while(1){k=(f<<2)+t|0;H[k>>2]=H[k>>2]/2;H[k+4>>2]=H[k+4>>2]/2;f=f+2|0;g=g+2|0;if((b|0)!=(g|0)){continue}break}if(!c){break a}}b=(f<<2)+t|0;H[b>>2]=H[b>>2]/2;break a}if(!b){break a}ga=O(L[qa+32>>2]*O(.5));g=b&3;if(b>>>0>=4){c=b&-4;b=0;while(1){L[t>>2]=ga*O(H[t>>2]);L[t+4>>2]=ga*O(H[t+4>>2]);L[t+8>>2]=ga*O(H[t+8>>2]);L[t+12>>2]=ga*O(H[t+12>>2]);t=t+16|0;b=b+4|0;if((c|0)!=(b|0)){continue}break}if(!g){break a}}b=0;while(1){L[t>>2]=ga*O(H[t>>2]);t=t+4|0;b=b+1|0;if((g|0)!=(b|0)){continue}break}break a}m=wa-va|0;if(H[pa+20>>2]==1){if(!p){break a}j=(H[oa+36>>2]+(N(i,m)<<2)|0)+(l<<2)|0;o=u&-4;i=0;while(1){b=0;if(o){d=j+(N(i,m)<<2)|0;n=(N(i,u)<<2)+t|0;while(1){q=b<<2;e=q+n|0;k=H[e+4>>2];g=H[e+8>>2];c=H[e+12>>2];q=d+q|0;H[q>>2]=H[e>>2]/2;H[q+12>>2]=(c|0)/2;H[q+8>>2]=(g|0)/2;H[q+4>>2]=(k|0)/2;b=b+4|0;if(o>>>0>b>>>0){continue}break}}nj:{if(b>>>0>=u>>>0){break nj}c=b+1|0;k=j+(N(i,m)<<2)|0;g=(N(i,u)<<2)+t|0;if(u-b&1){b=b<<2;H[b+k>>2]=H[b+g>>2]/2;b=c}if((c|0)==(u|0)){break nj}while(1){c=b<<2;H[c+k>>2]=H[c+g>>2]/2;c=c+4|0;H[c+k>>2]=H[c+g>>2]/2;b=b+2|0;if((u|0)!=(b|0)){continue}break}}i=i+1|0;if((p|0)!=(i|0)){continue}break}break a}if(!p|!u){break a}ga=O(L[qa+32>>2]*O(.5));i=(H[oa+36>>2]+(N(i,m)<<2)|0)+(l<<2)|0;g=u&-4;k=u&3;h=0;c=u-1>>>0<3;while(1){b=i;f=0;oj:{if(!c){while(1){L[b>>2]=ga*O(H[t>>2]);L[b+4>>2]=ga*O(H[t+4>>2]);L[b+8>>2]=ga*O(H[t+8>>2]);L[b+12>>2]=ga*O(H[t+12>>2]);b=b+16|0;t=t+16|0;f=f+4|0;if((g|0)!=(f|0)){continue}break}if(!k){break oj}}f=0;while(1){L[b>>2]=ga*O(H[t>>2]);b=b+4|0;t=t+4|0;f=f+1|0;if((k|0)!=(f|0)){continue}break}}i=(m<<2)+i|0;h=h+1|0;if((p|0)!=(h|0)){continue}break}break a}H[Y>>2]=z;Ba(S,2,8716,Y)}H[H[d>>2]>>2]=0}Ca(a);na=Y+96|0} +function ib(a,b,c,d,e,f){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,z=0,A=0,C=0,D=0,E=0,J=0,M=0,Q=0,R=0,S=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=O(0),da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,oa=0,pa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=O(0),Aa=0,Ga=0,Ka=0,La=0,Ma=0,Na=0,Pa=O(0),Ra=0,Ua=0,Wa=0,Xa=0,Ya=0,_a=0,$a=0,ab=0,bb=0,cb=0,fb=0,gb=0,hb=0,ib=0,kb=0,lb=0,mb=0,pb=0,qb=0,rb=0,sb=0,tb=0;Aa=na-16|0;na=Aa;a:{if(!(I[a+8|0]&128)|H[a+228>>2]!=(b|0)){break a}ua=H[a+180>>2]+N(b,5644)|0;E=H[ua+5596>>2];if(!E){jb(ua);break a}g=H[a+100>>2];if(!g){g=H[a+96>>2]}q=H[g>>2];p=H[g+4>>2];k=H[g+8>>2];v=H[g+12>>2];g=H[a+60>>2];h=H[a+64>>2];i=H[ua+5600>>2];ra=na-16|0;na=ra;D=H[a+232>>2];H[D+36>>2]=b;j=H[H[D+28>>2]+76>>2];H[D+64>>2]=1;H[D+60>>2]=v;H[D+56>>2]=k;H[D+52>>2]=p;H[D+48>>2]=q;H[D+32>>2]=j+N(b,5644);Ca(H[D+68>>2]);p=0;H[D+68>>2]=0;b:{if(g){p=Ea(4,H[H[D+24>>2]+16>>2]);if(!p){break b}q=g&3;c:{if(g>>>0>=4){k=g&-4;b=0;while(1){g=h+(X<<2)|0;H[(H[g>>2]<<2)+p>>2]=1;H[(H[g+4>>2]<<2)+p>>2]=1;H[(H[g+8>>2]<<2)+p>>2]=1;H[(H[g+12>>2]<<2)+p>>2]=1;X=X+4|0;b=b+4|0;if((k|0)!=(b|0)){continue}break}if(!q){break c}}while(1){H[(H[h+(X<<2)>>2]<<2)+p>>2]=1;X=X+1|0;s=s+1|0;if((q|0)!=(s|0)){continue}break}}H[D+68>>2]=p}d:{A=H[D+24>>2];m=H[A+16>>2];e:{if(!m){break e}X=0;f:{while(1){g:{if(H[(X<<2)+p>>2]?0:p){break g}h=H[A+24>>2]+N(X,52)|0;b=H[h+4>>2];k=b-1|0;q=H[D+60>>2];v=k+q|0;g=0-!b|0;j=ve(v,q>>>0>v>>>0?g+1|0:g,b,0);h=H[h>>2];l=h-1|0;v=H[D+56>>2];s=l+v|0;q=0-!h|0;r=ve(s,s>>>0>>0?q+1|0:q,h,0);t=k;k=H[D+52>>2];v=t+k|0;k=ve(v,k>>>0>v>>>0?g+1|0:g,b,0);b=H[H[H[D+20>>2]>>2]+20>>2]+N(X,76)|0;g=H[b+20>>2]-H[b+24>>2]|0;if(g>>>0>31){break g}v=H[b+12>>2];s=H[b+8>>2];t=H[b+4>>2];V=l;l=H[D+48>>2];x=V+l|0;h=ve(x,l>>>0>x>>>0?q+1|0:q,h,0);b=h-H[b>>2]|0;h:{if((b>>>0<=h>>>0?b:0)>>>g|0){break h}b=k-t|0;if((b>>>0<=k>>>0?b:0)>>>g|0){break h}b=s-r|0;if((b>>>0<=s>>>0?b:0)>>>g|0){break h}b=v-j|0;if(!((b>>>0<=v>>>0?b:0)>>>g|0)){break g}}H[D+64>>2]=0;break f}X=X+1|0;if((m|0)!=(X|0)){continue}break}if(!H[D+64>>2]){break f}s=0;while(1){b=H[H[H[D+20>>2]>>2]+20>>2]+N(s,76)|0;g=H[b+28>>2]+N(H[b+24>>2],152)|0;h=H[g-148>>2];q=H[g-140>>2];p=H[g-152>>2];g=H[g-144>>2];k=H[D+68>>2];i:{if(H[k+(s<<2)>>2]?0:k){break i}k=q-h|0;g=g-p|0;re(k,0,g);if(!(!qa|(h|0)==(q|0))){X=0;Ba(f,1,2982,0);break b}g=N(g,k);if(g>>>0>=1073741824){X=0;Ba(f,1,2982,0);break b}g=g<<2;H[b+44>>2]=g;j:{k:{l:{h=H[b+36>>2];if(h){if(g>>>0<=K[b+48>>2]){break i}if(H[b+40>>2]){break l}}g=Ia(g);H[b+36>>2]=g;h=g;g=H[b+44>>2];if(!(g?h:1)){break k}H[b+40>>2]=1;H[b+48>>2]=g;break i}Ca(h);g=Ia(H[b+44>>2]);H[b+36>>2]=g;if(g){break j}H[b+48>>2]=0;H[b+40>>2]=0;H[b+44>>2]=0}X=0;Ba(f,1,2982,0);break b}H[b+40>>2]=1;H[b+48>>2]=H[b+44>>2]}s=s+1|0;A=H[D+24>>2];if(s>>>0>2]){continue}break}break e}M=H[A+24>>2];W=H[H[H[D+20>>2]>>2]+20>>2];b=0;while(1){m:{if(H[(b<<2)+p>>2]?0:p){break m}g=W+N(b,76)|0;q=H[g>>2];k=M+N(b,52)|0;h=H[k>>2];l=h-1|0;v=H[D+48>>2];j=l+v|0;s=0-!h|0;v=ve(j,j>>>0>>0?s+1|0:s,h,0);q=q>>>0>v>>>0?q:v;H[g+56>>2]=q;v=H[g+4>>2];k=H[k+4>>2];r=k-1|0;t=H[D+52>>2];x=r+t|0;j=0-!k|0;t=ve(x,t>>>0>x>>>0?j+1|0:j,k,0);v=v>>>0>t>>>0?v:t;H[g+60>>2]=v;t=H[g+8>>2];V=l;l=H[D+56>>2];x=V+l|0;h=ve(x,l>>>0>x>>>0?s+1|0:s,h,0);h=h>>>0>t>>>0?t:h;H[g+64>>2]=h;s=H[g+12>>2];l=H[D+60>>2];r=r+l|0;k=ve(r,l>>>0>r>>>0?j+1|0:j,k,0);k=k>>>0>s>>>0?s:k;H[g+68>>2]=k;if(h>>>0>>0|k>>>0>>0){break d}r=H[g+20>>2];if(!r){break m}t=k-1|0;J=0-!k|0;Z=h-1|0;R=0-!h|0;x=v-1|0;S=0-!v|0;Y=q-1|0;z=0-!q|0;$=H[g+28>>2];k=0;h=0;while(1){v=$+N(k,152)|0;q=r+(k^-1)|0;g=q&31;if((q&63)>>>0>=32){l=1<>>32-g}g=t+s|0;j=l+J|0;w=g>>>0>>0?j+1|0:j;j=q&31;if((q&63)>>>0>=32){g=w>>>j|0}else{g=((1<>>j}H[v+148>>2]=g;g=l+R|0;V=g+1|0;j=g;g=s+Z|0;w=g>>>0>>0?V:j;j=q&31;if((q&63)>>>0>=32){g=w>>>j|0}else{g=((1<>>j}H[v+144>>2]=g;g=l+S|0;V=g+1|0;j=g;g=s+x|0;w=g>>>0>>0?V:j;j=q&31;if((q&63)>>>0>=32){g=w>>>j|0}else{g=((1<>>j}H[v+140>>2]=g;g=l+z|0;j=s+Y|0;s=j>>>0>>0?g+1|0:g;g=q&31;if((q&63)>>>0>=32){g=s>>>g|0}else{g=((1<>>g}H[v+136>>2]=g;k=k+1|0;h=k?h:h+1|0;if(h|(k|0)!=(r|0)){continue}break}}b=b+1|0;if((m|0)!=(b|0)){continue}break}}X=0;H[ra+8>>2]=0;b=H[D+28>>2];Y=Ea(1,8);if(Y){H[Y+4>>2]=b;H[Y>>2]=A}if(!Y){break b}Z=H[H[D+20>>2]>>2];x=na-144|0;na=x;l=H[D+36>>2];b=N(l,5644);h=H[Y+4>>2];R=b+H[h+76>>2]|0;_=H[R+420>>2];k=0;q=0;t=na-32|0;na=t;J=b+H[h+76>>2]|0;$=H[J+420>>2];S=H[Y>>2];m=S;w=H[m+16>>2];v=Fa(N(w,528));n:{if(!v){break n}b=Fa(w<<2);o:{if(!b){b=v;break o}p=H[h+76>>2]+N(l,5644)|0;M=H[p+420>>2];s=M+1|0;g=Ea(s,240);p:{if(g){q:{if(s){r=H[m+16>>2];s=g;while(1){H[s+236>>2]=f;j=Ea(r,16);H[s+200>>2]=j;if(!j){break q}W=H[m+16>>2];H[s+196>>2]=W;r=0;j=0;if(W){while(1){r=H[s+200>>2]+(j<<4)|0;W=H[p+5584>>2]+N(j,1080)|0;z=Ea(H[W+4>>2],16);H[r+12>>2]=z;if(!z){break q}H[r+8>>2]=H[W+4>>2];j=j+1|0;r=H[m+16>>2];if(j>>>0>>0){continue}break}}s=s+240|0;j=(o|0)==(M|0);o=o+1|0;if(!j){continue}break}}break p}p=H[g+4>>2];if(p){Ca(p);H[g+4>>2]=0}s=g;p=0;while(1){j=H[s+200>>2];if(j){r=0;o=H[s+196>>2];if(o){while(1){W=H[j+12>>2];if(W){Ca(W);H[j+12>>2]=0;o=H[s+196>>2]}j=j+16|0;r=r+1|0;if(r>>>0>>0){continue}break}j=H[s+200>>2]}Ca(j);H[s+200>>2]=0}s=s+240|0;j=(p|0)==(M|0);p=p+1|0;if(!j){continue}break}Ca(g)}g=0}if(g){r:{if(!w){break r}j=w&7;p=v;if(w>>>0>=8){r=w&-8;while(1){s=(n<<2)+b|0;H[s+28>>2]=p+3696;H[s+24>>2]=p+3168;H[s+20>>2]=p+2640;H[s+16>>2]=p+2112;H[s+12>>2]=p+1584;H[s+8>>2]=p+1056;H[s+4>>2]=p+528;H[s>>2]=p;n=n+8|0;p=p+4224|0;k=k+8|0;if((r|0)!=(k|0)){continue}break}if(!j){break r}}while(1){H[(n<<2)+b>>2]=p;n=n+1|0;p=p+528|0;u=u+1|0;if((j|0)!=(u|0)){continue}break}}u=b;r=0;s=H[(H[h+76>>2]+N(l,5644)|0)+5584>>2];p=H[m+24>>2];b=H[h+24>>2];k=(l>>>0)/(b>>>0)|0;b=H[h+4>>2]+N(H[h+12>>2],l-N(b,k)|0)|0;n=H[m>>2];H[t+20>>2]=b>>>0>n>>>0?b:n;n=b+H[h+12>>2]|0;b=b>>>0>n>>>0?-1:n;n=H[m+8>>2];H[t+16>>2]=b>>>0>>0?b:n;b=H[h+8>>2]+N(k,H[h+16>>2])|0;k=H[m+4>>2];H[t+12>>2]=b>>>0>k>>>0?b:k;h=b+H[h+16>>2]|0;b=b>>>0>h>>>0?-1:h;h=H[m+12>>2];H[t+8>>2]=b>>>0>>0?b:h;H[t+24>>2]=0;H[t+28>>2]=0;H[t+4>>2]=2147483647;H[t>>2]=2147483647;if(H[m+16>>2]){while(1){h=u?H[u+(r<<2)>>2]:0;b=H[p+4>>2];M=b-1|0;k=H[t+8>>2];j=M+k|0;n=0-!b|0;j=ve(j,k>>>0>j>>>0?n+1|0:n,b,0);k=H[p>>2];W=k-1|0;o=H[t+16>>2];z=W+o|0;l=0-!k|0;o=ve(z,o>>>0>z>>>0?l+1|0:l,k,0);V=M;M=H[t+12>>2];z=V+M|0;n=ve(z,z>>>0>>0?n+1|0:n,b,0);b=H[t+20>>2];M=b+W|0;k=ve(M,b>>>0>M>>>0?l+1|0:l,k,0);b=H[s+4>>2];if(b>>>0>K[t+28>>2]){H[t+28>>2]=b;b=H[s+4>>2]}if(b){da=s+944|0;ka=s+812|0;ba=j-1|0;oa=0-!j|0;aa=o-1|0;la=0-!o|0;ga=n-1|0;ia=0-!n|0;C=k-1|0;ha=0-!k|0;o=0;while(1){k=o<<2;M=H[k+da>>2];W=H[k+ka>>2];n=0;if(h){H[h+4>>2]=M;H[h>>2]=W;n=h+8|0}b=b-1|0;h=W+b|0;s:{if(h>>>0>31){break s}k=H[p>>2];if(k>>>0>-1>>>h>>>0){break s}j=H[t+4>>2];h=k<>2]=h>>>0>j>>>0?j:h}h=b+M|0;t:{if(h>>>0>31){break t}k=H[p+4>>2];if(k>>>0>-1>>>h>>>0){break t}j=H[t>>2];h=k<>2]=h>>>0>j>>>0?j:h}h=0;k=b&31;if((b&63)>>>0>=32){l=1<>>32-k}ea=j;j=ba+ea|0;k=l;l=oa+k|0;A=j>>>0>>0?l+1|0:l;z=b&31;l=M&31;if((M&63)>>>0>=32){l=1<>>32-l}if((b&63)>>>0>=32){A=A>>>z|0}else{A=((1<>>z}j=V+A|0;ma=j-1|0;l=(j>>>0>>0?l+1|0:l)-!j|0;j=M&31;z=k+ia|0;V=ga+ea|0;z=V>>>0>>0?z+1|0:z;if((M&63)>>>0>=32){j=l>>>j|0}else{j=((1<>>j}l=j;j=b&31;if((b&63)>>>0>=32){j=z>>>j|0}else{j=((1<>>j}z=(j|0)!=(A|0)?l-(j>>>M|0)&-1>>>M:0;j=k+la|0;l=j+1|0;V=j;j=aa+ea|0;A=j>>>0>>0?l:V;M=b&31;l=W&31;if((W&63)>>>0>=32){l=1<>>32-l}if((b&63)>>>0>=32){A=A>>>M|0}else{A=((1<>>M}j=V+A|0;ma=j-1|0;l=(j>>>0>>0?l+1|0:l)-!j|0;j=W&31;k=k+ha|0;ea=C+ea|0;M=ea>>>0>>0?k+1|0:k;k=b&31;if((W&63)>>>0>=32){j=l>>>j|0}else{j=((1<>>j}if((b&63)>>>0>=32){k=M>>>k|0}else{k=((1<>>k}k=(k|0)!=(A|0)?j-(k>>>W|0)&-1>>>W:0;if(n){H[n+4>>2]=z;H[n>>2]=k;h=n+8|0}k=N(k,z);if(k>>>0>K[t+24>>2]){H[t+24>>2]=k}o=o+1|0;if(o>>>0>2]){continue}break}}p=p+52|0;s=s+1080|0;r=r+1|0;if(r>>>0>2]){continue}break}}M=$+1|0;r=H[t+28>>2];l=H[t+24>>2];H[g+4>>2]=0;b=H[J+8>>2]+1|0;W=N(l,w);o=N(W,r);re(b,0,o);u:{if(!qa){b=N(b,o);H[g+8>>2]=b;b=Ea(b,2);H[g+4>>2]=b;if(b){break u}}Ca(v);Ca(u);b=H[g+4>>2];if(b){Ca(b);H[g+4>>2]=0}if(!M){b=g;break o}b=0;k=g;while(1){p=H[k+200>>2];if(p){m=0;n=H[k+196>>2];if(n){while(1){h=H[p+12>>2];if(h){Ca(h);H[p+12>>2]=0;n=H[k+196>>2]}p=p+16|0;m=m+1|0;if(n>>>0>m>>>0){continue}break}p=H[k+200>>2]}Ca(p);H[k+200>>2]=0}k=k+240|0;h=(b|0)==($|0);b=b+1|0;if(!h){continue}break}b=g;break o}j=H[m+24>>2];z=H[t+20>>2];H[g+204>>2]=z;ea=H[t+12>>2];H[g+208>>2]=ea;ba=H[t+16>>2];H[g+212>>2]=ba;aa=H[t+8>>2];H[g+216>>2]=aa;H[g+12>>2]=o;H[g+16>>2]=W;H[g+20>>2]=l;q=1;H[g+24>>2]=1;if(w){k=H[g+200>>2];s=0;b=j;while(1){p=H[u+(s<<2)>>2];H[k>>2]=H[b>>2];H[k+4>>2]=H[b+4>>2];h=H[k+8>>2];v:{if(!h){break v}m=H[k+12>>2];if((h|0)!=1){ga=h&1;h=h&-2;n=0;while(1){H[m>>2]=H[p>>2];H[m+4>>2]=H[p+4>>2];H[m+8>>2]=H[p+8>>2];H[m+12>>2]=H[p+12>>2];H[m+16>>2]=H[p+16>>2];H[m+20>>2]=H[p+20>>2];H[m+24>>2]=H[p+24>>2];H[m+28>>2]=H[p+28>>2];m=m+32|0;p=p+32|0;n=n+2|0;if((h|0)!=(n|0)){continue}break}if(!ga){break v}}H[m>>2]=H[p>>2];H[m+4>>2]=H[p+4>>2];H[m+8>>2]=H[p+8>>2];H[m+12>>2]=H[p+12>>2]}b=b+52|0;k=k+16|0;s=s+1|0;if((w|0)!=(s|0)){continue}break}}if(M>>>0>1){h=g;while(1){H[h+456>>2]=aa;H[h+452>>2]=ba;H[h+448>>2]=ea;H[h+444>>2]=z;H[h+264>>2]=1;H[h+260>>2]=l;H[h+256>>2]=W;H[h+252>>2]=o;if(w){k=H[h+440>>2];s=0;b=j;while(1){p=H[u+(s<<2)>>2];H[k>>2]=H[b>>2];H[k+4>>2]=H[b+4>>2];n=H[k+8>>2];w:{if(!n){break w}m=H[k+12>>2];if((n|0)!=1){M=n&1;ga=n&-2;n=0;while(1){H[m>>2]=H[p>>2];H[m+4>>2]=H[p+4>>2];H[m+8>>2]=H[p+8>>2];H[m+12>>2]=H[p+12>>2];H[m+16>>2]=H[p+16>>2];H[m+20>>2]=H[p+20>>2];H[m+24>>2]=H[p+24>>2];H[m+28>>2]=H[p+28>>2];m=m+32|0;p=p+32|0;n=n+2|0;if((ga|0)!=(n|0)){continue}break}if(!M){break w}}H[m>>2]=H[p>>2];H[m+4>>2]=H[p+4>>2];H[m+8>>2]=H[p+8>>2];H[m+12>>2]=H[p+12>>2]}b=b+52|0;k=k+16|0;s=s+1|0;if((w|0)!=(s|0)){continue}break}}b=H[h+8>>2];H[h+244>>2]=H[h+4>>2];H[h+248>>2]=b;b=(q|0)!=($|0);h=h+240|0;q=q+1|0;if(b){continue}break}}Ca(v);Ca(u);b=H[J+420>>2];x:{if(I[J+5640|0]&4){if((b|0)==-1){break x}m=J+424|0;h=H[J+8>>2];n=0;p=g;while(1){q=H[m+36>>2];H[p+44>>2]=1;H[p+84>>2]=q;H[p+48>>2]=H[m>>2];q=H[m+4>>2];H[p+68>>2]=0;H[p+72>>2]=0;H[p+52>>2]=q;H[p+60>>2]=H[m+12>>2];H[p+64>>2]=H[m+16>>2];q=H[m+8>>2];H[p+76>>2]=l;H[p+56>>2]=h>>>0>q>>>0?q:h;m=m+148|0;p=p+240|0;q=(b|0)==(n|0);n=n+1|0;if(!q){continue}break}break x}if((b|0)==-1){break x}h=H[J+8>>2];q=H[J+4>>2];p=g;if(b){k=b+1&-2;u=0;while(1){H[p+68>>2]=0;H[p+72>>2]=0;H[p+52>>2]=0;H[p+44>>2]=1;H[p+48>>2]=0;H[p+84>>2]=q;H[p+60>>2]=r;H[p+324>>2]=q;H[p+76>>2]=l;H[p+56>>2]=h;H[p+308>>2]=0;H[p+312>>2]=0;H[p+292>>2]=0;H[p+284>>2]=1;H[p+288>>2]=0;H[p+300>>2]=r;H[p+296>>2]=h;H[p+316>>2]=l;H[p+64>>2]=H[p+196>>2];H[p+304>>2]=H[p+436>>2];p=p+480|0;u=u+2|0;if((k|0)!=(u|0)){continue}break}if(b&1){break x}}H[p+68>>2]=0;H[p+72>>2]=0;H[p+52>>2]=0;H[p+44>>2]=1;H[p+48>>2]=0;H[p+84>>2]=q;H[p+60>>2]=r;H[p+76>>2]=l;H[p+56>>2]=h;H[p+64>>2]=H[p+196>>2]}q=g;break n}Ca(v)}Ca(b)}na=t+32|0;g=q;y:{z:{if(!g){break z}M=_+1|0;s=E;v=g;A:{B:{while(1){if(H[v+84>>2]==-1){break A}l=Fa(H[S+16>>2]<<2);if(!l){break A}b=H[S+16>>2]<<2;if(b){y(l,1,b)}if(Vb(v)){while(1){k=H[Z+20>>2];C:{D:{if(K[v+40>>2]>=K[R+12>>2]){break D}b=H[v+32>>2];h=N(H[v+28>>2],76)+k|0;if(b>>>0>=K[h+24>>2]){break D}h=H[h+28>>2]+N(b,152)|0;if(!H[h+24>>2]){break D}q=h+28|0;j=0;E:{while(1){p=q+N(j,36)|0;b=H[p+20>>2]+N(H[v+36>>2],40)|0;if(!xb(D,H[v+28>>2],H[v+32>>2],H[p+16>>2],H[b>>2],H[b+4>>2],H[b+8>>2],H[b+12>>2])){j=j+1|0;if(j>>>0>2]){continue}break E}break}H[l+(H[v+28>>2]<<2)>>2]=0;H[x+136>>2]=0;if(!Ub(H[Y+4>>2],H[Z+20>>2],R,v,x+140|0,s,x+136|0,i,f)){break B}j=H[v+32>>2];o=H[v+28>>2];m=H[x+136>>2];if(H[x+140>>2]){H[x+136>>2]=0;W=H[(H[Z+20>>2]+N(o,76)|0)+28>>2]+N(j,152)|0;k=H[W+24>>2];if(k){w=i-m|0;J=i+s|0;o=W+28|0;t=0;r=0;z=m+s|0;u=z;while(1){F:{if(H[o+8>>2]==H[o>>2]|H[o+12>>2]==H[o+4>>2]){break F}b=H[o+20>>2]+N(H[v+36>>2],40)|0;$=N(H[b+20>>2],H[b+16>>2]);if(!$){break F}k=H[b+24>>2];h=0;while(1){p=H[k+36>>2];if(p){G:{if(r|H[k+64>>2]){H[k+52>>2]=0;j=1;b=64;break G}j=H[k>>2];b=H[k+40>>2];H:{if(b){j=N(b,24)+j|0;if(H[j-20>>2]!=H[j-12>>2]){j=j-24|0;break H}b=b+1|0}else{b=1}H[k+40>>2]=b}b=H[j+20>>2];I:{J:{if(b>>>0>(u^-1)>>>0){break J}q=j+20|0;while(1){if(J>>>0>>0){break J}n=H[k+4>>2];r=H[k+52>>2];if((r|0)!=H[k+56>>2]){q=p}else{b=r<<1|1;n=Ha(n,b<<3);if(!n){Ba(f,1,1024,0);break B}H[k+56>>2]=b;H[k+4>>2]=n;r=H[k+52>>2];b=H[q>>2];q=H[k+36>>2]}p=(r<<3)+n|0;H[p+4>>2]=b;H[p>>2]=u;H[k+52>>2]=r+1;H[j>>2]=H[j>>2]+b;n=H[j+16>>2];r=n+H[j+4>>2]|0;H[j+4>>2]=r;p=q-n|0;H[k+36>>2]=p;H[j+8>>2]=r;u=b+u|0;r=0;if((n|0)==(q|0)){break I}H[k+40>>2]=H[k+40>>2]+1;q=j+44|0;b=H[j+44>>2];j=j+24|0;if((u^-1)>>>0>=b>>>0){continue}break}}q=H[v+28>>2];p=H[v+32>>2];n=H[v+36>>2];if(H[H[Y+4>>2]+104>>2]){H[x+120>>2]=q;H[x+116>>2]=p;H[x+112>>2]=t;H[x+108>>2]=n;H[x+104>>2]=h;H[x+100>>2]=w;H[x+96>>2]=b;Ba(f,1,14693,x+96|0);break B}H[x+88>>2]=q;H[x+84>>2]=p;H[x+80>>2]=t;H[x+76>>2]=n;H[x+72>>2]=h;H[x+68>>2]=w;H[x+64>>2]=b;Ba(f,2,14693,x- -64|0);H[k+52>>2]=0;H[k+64>>2]=1;r=1}j=H[k+40>>2];b=44}H[b+k>>2]=j}k=k+68|0;h=h+1|0;if(($|0)!=(h|0)){continue}break}k=H[W+24>>2]}o=o+36|0;t=t+1|0;if(t>>>0>>0){continue}break}j=H[v+32>>2];o=H[v+28>>2];b=r?w:u-z|0}else{b=0}m=b+m|0}h=H[S+24>>2]+N(o,52)|0;b=H[h+36>>2];H[h+36>>2]=b>>>0>>0?j:b;break C}k=H[Z+20>>2]}H[x+136>>2]=0;if(!Ub(H[Y+4>>2],k,R,v,x+140|0,s,x+136|0,i,f)){break B}o=H[v+28>>2];m=H[x+136>>2];if(!H[x+140>>2]){break C}J=H[v+32>>2];b=H[(H[Z+20>>2]+N(o,76)|0)+28>>2]+N(J,152)|0;z=H[b+24>>2];if(!z){break C}w=i-m|0;n=b+28|0;W=H[v+36>>2];j=0;r=0;K:{L:{while(1){M:{if(H[n+8>>2]==H[n>>2]|H[n+12>>2]==H[n+4>>2]){break M}b=H[n+20>>2]+N(W,40)|0;$=N(H[b+20>>2],H[b+16>>2]);if(!$){break M}p=H[b+24>>2];t=0;while(1){b=H[p+36>>2];if(b){k=H[p>>2];h=H[p+40>>2];N:{if(h){k=N(h,24)+k|0;if(H[k-20>>2]!=H[k-12>>2]){k=k-24|0;break N}h=h+1|0}else{h=1}H[p+40>>2]=h}u=H[k+20>>2];j=u+j|0;if(j>>>0>>0|j>>>0>w>>>0){break K}while(1){O:{u=H[k+16>>2];H[k+4>>2]=u+H[k+4>>2];q=b-u|0;if((b|0)==(u|0)){break O}h=h+1|0;H[p+40>>2]=h;u=H[k+44>>2];j=u+j|0;if(j>>>0>>0){break L}k=k+24|0;b=q;if(j>>>0<=w>>>0){continue}break L}break}H[p+36>>2]=q}p=p+68|0;t=t+1|0;if(($|0)!=(t|0)){continue}break}}n=n+36|0;r=r+1|0;if((z|0)!=(r|0)){continue}break}m=j+m|0;break C}H[p+36>>2]=q}if(!H[H[Y+4>>2]+104>>2]){H[x+24>>2]=o;H[x+20>>2]=J;H[x+16>>2]=r;H[x+12>>2]=W;H[x+8>>2]=t;H[x+4>>2]=w;H[x>>2]=u;Ba(f,2,14608,x);o=H[v+28>>2];m=m+w|0;break C}H[x+56>>2]=o;H[x+52>>2]=J;H[x+48>>2]=r;H[x+44>>2]=W;H[x+40>>2]=t;H[x+36>>2]=w;H[x+32>>2]=u;Ba(f,1,14608,x+32|0);break B}P:{if(!H[l+(o<<2)>>2]){break P}b=H[S+24>>2]+N(o,52)|0;if(H[b+36>>2]){break P}H[b+36>>2]=H[(H[Z+20>>2]+N(o,76)|0)+24>>2]-1}i=i-m|0;s=m+s|0;if(Vb(v)){continue}break}}Ca(l);v=v+240|0;Q=Q+1|0;if(Q>>>0<=K[R+420>>2]){continue}break}yb(g,M);H[ra+8>>2]=s-E;b=1;break y}yb(g,M);Ca(l);break z}yb(g,M)}b=0}na=x+144|0;db(Y);if(!b){break b}X=H[H[D+32>>2]+5584>>2];j=H[H[D+20>>2]>>2];C=H[j+20>>2];H[ra+12>>2]=1;s=0;b=H[D+32>>2];l=H[X+16>>2]>>>4&1&H[b+12>>2]==H[b+8>>2];A=H[j+16>>2];Q:{if(!A){break Q}while(1){b=H[D+68>>2];if(!(H[b+(s<<2)>>2]?0:b)){u=ra+12|0;A=0;b=H[C+24>>2];R:{if(!b){break R}m=H[D+44>>2];while(1){q=H[C+28>>2]+N(A,152)|0;p=H[q+24>>2];if(p){o=q+28|0;b=H[q+20>>2];v=H[q+16>>2];r=0;while(1){if(N(b,v)){n=o+N(r,36)|0;k=0;while(1){g=H[n+20>>2]+N(k,40)|0;i=xb(D,H[C+16>>2],A,H[n+16>>2],H[g>>2],H[g+4>>2],H[g+8>>2],H[g+12>>2]);h=H[g+16>>2];p=H[g+20>>2];b=N(h,p);S:{if(i){if(!b){break S}h=0;while(1){i=H[g+24>>2]+N(h,68)|0;T:{if(!xb(D,H[C+16>>2],A,H[n+16>>2],H[i+8>>2],H[i+12>>2],H[i+16>>2],H[i+20>>2])){b=H[i+60>>2];if(!b){break T}Ca(b);H[i+60>>2]=0;break T}if(!H[D+64>>2]){if(H[i+60>>2]|H[i+16>>2]==H[i+8>>2]|H[i+20>>2]==H[i+12>>2]){break T}}b=Ea(1,44);if(!b){H[ra+12>>2]=0;break R}p=H[D+64>>2];H[b+36>>2]=0;H[b+28>>2]=u;H[b+20>>2]=X;H[b+16>>2]=C;H[b+12>>2]=n;H[b+8>>2]=i;H[b+4>>2]=A;H[b>>2]=p;H[b+40>>2]=l;H[b+32>>2]=f;H[b+24>>2]=H[m+4>>2]>1;eb(m,14,b);if(!H[ra+12>>2]){break R}}h=h+1|0;if(h>>>0>2],H[g+16>>2])>>>0){continue}break}break S}if(!b){break S}v=0;while(1){b=H[g+24>>2]+N(v,68)|0;i=H[b+60>>2];if(i){Ca(i);H[b+60>>2]=0;p=H[g+20>>2];h=H[g+16>>2]}v=v+1|0;if(v>>>0>>0){continue}break}}k=k+1|0;b=H[q+20>>2];v=H[q+16>>2];if(k>>>0>>0){continue}break}p=H[q+24>>2]}r=r+1|0;if(r>>>0

>>0){continue}break}b=H[C+24>>2]}A=A+1|0;if(A>>>0>>0){continue}break}}if(!H[ra+12>>2]){break Q}A=H[j+16>>2]}X=X+1080|0;C=C+76|0;s=s+1|0;if(A>>>0>s>>>0){continue}break}}X=0;Sa(H[D+44>>2]);if(!H[ra+12>>2]){break b}U:{if(H[D+64>>2]){break U}s=H[D+24>>2];if(!H[s+16>>2]){break U}C=0;while(1){b=H[H[H[D+20>>2]>>2]+20>>2]+N(C,76)|0;g=H[b+28>>2]+N(H[(H[s+24>>2]+N(C,52)|0)+36>>2],152)|0;i=H[g+136>>2];h=H[g+144>>2];q=H[g+140>>2];g=H[g+148>>2];Ca(H[b+52>>2]);H[b+52>>2]=0;V:{p=H[D+68>>2];if((h|0)==(i|0)|(g|0)==(q|0)|(H[p+(C<<2)>>2]?0:p)){break V}g=g-q|0;i=h-i|0;re(g,0,i);if(qa){Ba(f,1,2982,0);break b}g=N(g,i);if(g>>>0>=1073741824){Ba(f,1,2982,0);break b}i=b;b=Ia(g<<2);H[i+52>>2]=b;if(b){break V}Ba(f,1,2982,0);break b}C=C+1|0;s=H[D+24>>2];if(C>>>0>2]){continue}break}}s=H[D+32>>2];v=H[H[D+20>>2]>>2];if(H[v+16>>2]){C=H[v+20>>2];s=H[s+5584>>2];A=H[H[D+24>>2]+24>>2];p=0;while(1){W:{b=H[D+68>>2];if(H[b+(p<<2)>>2]?0:b){break W}j=H[A+36>>2]+1|0;if(H[s+20>>2]==1){Y=j;b=0;_=na-32|0;na=_;X:{Y:{if(H[D+64>>2]){g=1;if((j|0)==1){break X}k=H[C+28>>2];b=k+N(H[C+24>>2],152)|0;h=H[b-144>>2];q=H[b-152>>2];if((h|0)==(q|0)){break X}n=j-1|0;b=0;m=H[D+44>>2];l=H[m+4>>2];i=k;Z:{if((j|0)!=2){j=n&1;r=n&-2;g=0;while(1){u=H[i+160>>2]-H[i+152>>2]|0;b=b>>>0>u>>>0?b:u;u=H[i+164>>2]-H[i+156>>2]|0;b=b>>>0>u>>>0?b:u;u=H[i+312>>2]-H[i+304>>2]|0;b=b>>>0>u>>>0?b:u;u=H[i+316>>2]-H[i+308>>2]|0;b=b>>>0>u>>>0?b:u;i=i+304|0;g=g+2|0;if((r|0)!=(g|0)){continue}break}if(!j){break Z}}g=H[i+160>>2]-H[i+152>>2]|0;b=b>>>0>g>>>0?b:g;g=H[i+164>>2]-H[i+156>>2]|0;b=b>>>0>g>>>0?b:g}g=0;if(b>>>0>134217727){break X}i=H[k+4>>2];u=H[k+12>>2];t=H[k>>2];E=H[k+8>>2];o=b<<5;j=ob(o);H[_+16>>2]=j;if(!j){break X}r=h-q|0;g=u-i|0;q=E-t|0;H[_>>2]=j;while(1){u=H[C+36>>2];h=g;H[_+8>>2]=g;b=q;H[_+24>>2]=b;g=H[k+156>>2];i=H[k+164>>2];q=H[k+160>>2];t=H[k+152>>2];H[_+28>>2]=(t|0)%2;q=q-t|0;H[_+20>>2]=q-b;x=(l|0)<2;g=i-g|0;_:{if(!(!x&g>>>0>1)){i=0;if(!g){break _}while(1){$b(_+16|0,u+(N(i,r)<<2)|0);i=i+1|0;if((i|0)!=(g|0)){continue}break}break _}t=g>>>0>>0?g:l;Y=t-1|0;E=(g>>>0)/(t>>>0)|0;b=0;while(1){i=Fa(36);if(!i){break Y}w=H[_+28>>2];H[i+8>>2]=H[_+24>>2];H[i+12>>2]=w;w=H[_+20>>2];H[i>>2]=H[_+16>>2];H[i+4>>2]=w;H[i+24>>2]=u;H[i+20>>2]=r;H[i+16>>2]=q;H[i+28>>2]=N(b,E);w=(b|0)==(Y|0);b=b+1|0;H[i+32>>2]=w?g:N(E,b);w=ob(o);H[i>>2]=w;if(!w){g=0;Sa(m);Ca(i);Ca(j);break X}eb(m,10,i);if((b|0)!=(t|0)){continue}break}Sa(m)}H[_+4>>2]=g-h;H[_+12>>2]=H[k+156>>2]%2;$:{if(!(!x&q>>>0>1)){b=8;i=0;if(q>>>0>=8){while(1){nb(_,u+(i<<2)|0,r,8);i=b;b=b+8|0;if(q>>>0>=b>>>0){continue}break}}if(i>>>0>=q>>>0){break $}nb(_,u+(i<<2)|0,r,q-i|0);break $}h=l>>>0>q>>>0?q:l;E=h-1|0;t=(q>>>0)/(h>>>0)|0;b=0;while(1){i=Fa(36);if(!i){break Y}x=H[_+12>>2];H[i+8>>2]=H[_+8>>2];H[i+12>>2]=x;x=H[_+4>>2];H[i>>2]=H[_>>2];H[i+4>>2]=x;H[i+24>>2]=u;H[i+20>>2]=r;H[i+16>>2]=g;H[i+28>>2]=N(b,t);x=(b|0)==(E|0);b=b+1|0;H[i+32>>2]=x?q:N(t,b);x=ob(o);H[i>>2]=x;if(!x){g=0;Sa(m);Ca(i);Ca(j);break X}eb(m,11,i);if((b|0)!=(h|0)){continue}break}Sa(m)}k=k+152|0;n=n-1|0;if(n){continue}break}g=1;Ca(j);break X}g=1;k=H[C+28>>2];ma=k+N(Y,152)|0;gb=ma-152|0;if(H[gb>>2]==H[ma-144>>2]){break X}hb=ma-148|0;if(H[hb>>2]==H[ma-140>>2]){break X}h=H[k+4>>2];q=H[k+12>>2];n=H[k>>2];j=H[k+8>>2];w=H[C+68>>2];M=H[C+64>>2];W=H[C+60>>2];J=H[C+56>>2];oa=_b(C,Y);if(!oa){g=0;break X}aa:{ba:{if((Y|0)!=1){i=k;ca:{if((Y|0)!=2){g=Y-1|0;m=g&1;l=g&-2;g=0;while(1){r=H[i+160>>2]-H[i+152>>2]|0;b=b>>>0>r>>>0?b:r;r=H[i+164>>2]-H[i+156>>2]|0;b=b>>>0>r>>>0?b:r;r=H[i+312>>2]-H[i+304>>2]|0;b=b>>>0>r>>>0?b:r;r=H[i+316>>2]-H[i+308>>2]|0;b=b>>>0>r>>>0?b:r;i=i+304|0;g=g+2|0;if((l|0)!=(g|0)){continue}break}if(!m){break ca}}g=H[i+160>>2]-H[i+152>>2]|0;b=b>>>0>g>>>0?b:g;g=H[i+164>>2]-H[i+156>>2]|0;b=b>>>0>g>>>0?b:g}if(b>>>0>=268435456){break aa}o=ob(b<<4);if(!o){break aa}da:{if(!Y){break da}u=q-h|0;m=j-n|0;ib=o+28|0;$=o+24|0;Ra=o+16|0;ha=o+4|0;sa=1;ea:while(1){b=H[k+156>>2];ja=(b|0)%2|0;g=H[k+152>>2];pa=(g|0)%2|0;Z=H[k+164>>2]-b|0;ka=Z-u|0;R=H[k+160>>2]-g|0;da=R-m|0;i=J;g=i;t=W;q=t;b=M;fa=b;h=w;n=h;j=H[C+20>>2];fa:{if((j|0)==(sa|0)){break fa}r=j-sa|0;q=0;g=0;if(i){b=r&31;if((r&63)>>>0>=32){l=-1<>>32-b}b=i+(g^-1)|0;g=l^-1;i=b>>>0>>0?g+1|0:g;g=r&31;if((r&63)>>>0>=32){g=i>>>g|0}else{g=((1<>>g}}if(W){b=r&31;if((r&63)>>>0>=32){l=-1<>>32-b}b=W+(i^-1)|0;i=l^-1;h=b>>>0>>0?i+1|0:i;i=r&31;if((r&63)>>>0>=32){q=h>>>i|0}else{q=((1<>>i}}h=0;b=0;if(M){b=r&31;if((r&63)>>>0>=32){l=-1<>>32-b}b=M+(i^-1)|0;i=l^-1;n=b>>>0>>0?i+1|0:i;i=r&31;if((r&63)>>>0>=32){b=n>>>i|0}else{b=((1<>>i}}if(w){i=r&31;if((r&63)>>>0>=32){l=-1<>>32-i}i=w+(h^-1)|0;h=l^-1;n=i>>>0>>0?h+1|0:h;h=r&31;if((r&63)>>>0>=32){h=n>>>h|0}else{h=((1<>>h}}fa=0;i=0;E=1<>>0>>0){i=r&31;if((r&63)>>>0>=32){l=-1<>>32-i}n=n^-1;i=n+(J-E|0)|0;j=l^-1;j=i>>>0>>0?j+1|0:j;n=r&31;if((r&63)>>>0>=32){i=j>>>n|0}else{i=((1<>>n}}if(E>>>0>>0){n=r&31;if((r&63)>>>0>=32){l=-1<>>32-n}j=j^-1;n=j+(M-E|0)|0;l=l^-1;l=j>>>0>n>>>0?l+1|0:l;j=r&31;if((r&63)>>>0>=32){fa=l>>>j|0}else{fa=((1<>>j}}n=0;t=0;if(E>>>0>>0){j=r&31;if((r&63)>>>0>=32){l=-1<>>32-j}t=t^-1;j=t+(W-E|0)|0;l=l^-1;t=j>>>0>>0?l+1|0:l;l=r&31;if((r&63)>>>0>=32){t=t>>>l|0}else{t=((1<>>l}}if(w>>>0<=E>>>0){break fa}n=r&31;if((r&63)>>>0>=32){l=-1<>>32-n}j=j^-1;n=j+(w-E|0)|0;l=l^-1;l=j>>>0>n>>>0?l+1|0:l;j=r&31;if((r&63)>>>0>=32){n=l>>>j|0}else{n=((1<>>j}}j=H[k+180>>2];l=fa-j|0;l=l>>>0<=fa>>>0?l:0;r=l+2|0;l=l>>>0>r>>>0?-1:r;la=l>>>0>>0?l:da;l=H[k+216>>2];r=b-l|0;b=b>>>0>=r>>>0?r:0;r=b+2|0;b=b>>>0>r>>>0?-1:r;ia=b>>>0>>0?b:m;b=(pa?la:ia)<<1;r=(pa?ia:la)<<1|1;va=b>>>0>r>>>0?b:r;b=va>>>0>>0;j=i-j|0;i=i>>>0>=j>>>0?j:0;j=i-2|0;r=i>>>0>=j>>>0?j:0;i=g-l|0;g=g>>>0>=i>>>0?i:0;i=g-2|0;i=g>>>0>=i>>>0?i:0;g=(pa?r:i)<<1;l=(pa?i:r)<<1|1;x=g>>>0>>0;E=H[k+184>>2];j=q-E|0;q=j>>>0<=q>>>0?j:0;j=q-2|0;q=j>>>0<=q>>>0?j:0;ea=q;S=H[k+220>>2];j=t-S|0;j=j>>>0<=t>>>0?j:0;t=j-2|0;j=j>>>0>=t>>>0?t:0;ba=j;t=h-E|0;h=h>>>0>=t>>>0?t:0;t=h+2|0;h=h>>>0>t>>>0?-1:t;E=h>>>0>>0?h:u;Q=E;h=n-S|0;h=h>>>0<=n>>>0?h:0;n=h+2|0;h=h>>>0>n>>>0?-1:n;V=h>>>0>>0?h:ka;n=V;if(ja){ea=j;ba=q;Q=n;n=E}wa=b?va:R;l=x?g:l;kb=u+V|0;lb=j+u|0;if(Z){ta=o+(i<<3)|0;b=o+(da<<3)|0;xa=b-4|0;g=(i|0)<(da|0);Ua=g?ta+4|0:xa;ya=m-1|0;Wa=(la|0)<(ya|0)?la:ya;x=0;Xa=(m|0)>1|(da|0)>0;h=pa<<2;Ya=(ha-h|0)+(r<<3)|0;_a=h+ta|0;z=(da|0)>(ia|0)?ia:da;aa=i+1|0;$a=m+la|0;La=m+r|0;ab=o+(l<<2)|0;Ma=!m&(da|0)==1;h=o+(m<<3)|0;ga=h-8|0;Ga=h-4|0;Ka=b-8|0;Na=o+(wa<<2)|0;bb=Na-4|0;cb=(o+((g?i:da)<<3)|0)-4|0;while(1){ga:{if(!(x>>>0>>0&q>>>0<=x>>>0|x>>>0>>0&x>>>0>=lb>>>0)){S=x+1|0;break ga}if(R>>>0>va>>>0){H[bb>>2]=0;H[Na>>2]=0}S=x+1|0;Oa(oa,i,x,ia,S,_a,2,0);Oa(oa,La,x,$a,S,Ya,2,0);ha:{ia:{ja:{if(!pa){if(!Xa){break ha}if((i|0)>=(ia|0)){break ia}ka:{la:{if((i|0)>0){b=H[cb>>2];break la}b=H[ha>>2];g=b;if((i|0)<0){break ka}}g=b;b=H[Ua>>2]}H[ta>>2]=H[ta>>2]-((b+g|0)+2>>2);b=aa;h=b;g=i;if((b|0)>=(z|0)){break ja}while(1){b=g;g=h;h=o+(g<<3)|0;H[h>>2]=H[h>>2]-((H[(o+(b<<3)|0)+4>>2]+H[h+4>>2]|0)+2>>2);h=g+1|0;if((z|0)!=(h|0)){continue}break}b=z;break ja}ma:{if(!Ma){b=i;if((ia|0)<=(b|0)){break ma}while(1){g=o+(b<<3)|0;h=H[g+4>>2];t=Ka;na:{oa:{if((b|0)>=0){fa=H[((b|0)<(da|0)?g:Ka)>>2];b=b+1|0;break oa}fa=H[o>>2];if((b|0)!=-1){b=b+1|0;t=o;break na}b=0}if((b|0)>=(da|0)){break na}t=o+(b<<3)|0}H[g+4>>2]=h-((H[t>>2]+fa|0)+2>>2);if((b|0)<(ia|0)){continue}break}break ma}H[o>>2]=H[o>>2]/2;break ha}b=r;if((la|0)<=(b|0)){break ha}while(1){g=o+(b<<3)|0;h=H[g>>2];pa:{if((b|0)<0){t=H[ha>>2];fa=ha;break pa}t=H[((b|0)<(m|0)?g+4|0:Ga)>>2];fa=ha;if(!b){break pa}fa=(b|0)>(m|0)?Ga:g-4|0}H[g>>2]=h+(H[fa>>2]+t>>1);b=b+1|0;if((la|0)!=(b|0)){continue}break}break ha}if((b|0)>=(ia|0)){break ia}while(1){g=o+(b<<3)|0;t=H[g>>2];qa:{ra:{if((b|0)>0){h=H[(o+(((b|0)<(da|0)?b:da)<<3)|0)-4>>2];break ra}h=H[ha>>2];fa=ha;if((b|0)<0){break qa}}fa=xa;if((b|0)>=(da|0)){break qa}fa=(o+(b<<3)|0)+4|0}H[g>>2]=t-((H[fa>>2]+h|0)+2>>2);b=b+1|0;if((ia|0)!=(b|0)){continue}break}}if((r|0)>=(la|0)){break ha}b=r;if((ya|0)>(b|0)){while(1){g=o+(b<<3)|0;b=b+1|0;H[g+4>>2]=H[g+4>>2]+(H[o+(b<<3)>>2]+H[g>>2]>>1);if((b|0)<(Wa|0)){continue}break}}if((b|0)>=(la|0)){break ha}while(1){g=ga;t=b;sa:{ta:{if((b|0)>=0){h=H[((b|0)<(m|0)?o+(b<<3)|0:g)>>2];b=b+1|0;break ta}h=H[o>>2];if((t|0)!=-1){b=t+1|0;g=o;break sa}b=0}if((m|0)<=(b|0)){break sa}g=o+(b<<3)|0}t=o+(t<<3)|0;H[t+4>>2]=H[t+4>>2]+(H[g>>2]+h>>1);if((b|0)<(la|0)){continue}break}}if(!Za(oa,l,x,wa,S,ab,1,0)){break ba}}x=S;if((Z|0)!=(x|0)){continue}break}}k=k+152|0;b=Q<<1;g=n<<1|1;b=b>>>0>g>>>0?b:g;Ua=b>>>0>>0?b:Z;Q=o+(q<<5)|0;b=o+(ka<<5)|0;va=b-4|0;g=(q|0)<(ka|0);Wa=g?Q+28|0:va;ya=b-8|0;Xa=g?Q+24|0:ya;Ga=b-12|0;Ya=g?Q+20|0:Ga;Ka=b-16|0;_a=(q|0)<0?Ra:g?Q+16|0:Ka;xa=u-1|0;$a=(V|0)<(xa|0)?V:xa;i=o+((g?q:ka)<<5)|0;La=(q|0)<=0;ab=La?Ra:i-16|0;Ma=(ka|0)>0;Na=Ma|(u|0)>1;bb=Q+(ja<<4)|0;cb=(o+(4-(ja<<2)<<2)|0)+(j<<5)|0;n=(E|0)<(ka|0)?E:ka;r=q+1|0;g=ea<<1;h=ba<<1|1;mb=g>>>0>>0?g:h;qb=o+(mb<<4)|0;rb=!u&(ka|0)==1;g=o+(u<<5)|0;S=g-20|0;z=g-24|0;ea=g-28|0;ba=g-32|0;aa=g-4|0;ga=g-8|0;da=g-12|0;la=g-16|0;ia=b-20|0;fa=b-24|0;pa=b-28|0;ta=b-32|0;sb=i-4|0;x=i-8|0;tb=i-12|0;while(1){ua:{va:{wa:{xa:{m=l;if(m>>>0>>0){b=wa-m|0;l=m+(b>>>0>=4?4:b)|0;Oa(oa,m,q,l,E,bb,1,8);Oa(oa,m,lb,l,kb,cb,1,8);if(!ja){if(!Na){break ua}if((q|0)>=(E|0)){break va}H[Q>>2]=H[Q>>2]-((H[ab>>2]+H[_a>>2]|0)+2>>2);ya:{if(!La){b=H[tb>>2];h=x;g=sb;break ya}b=H[o+20>>2];if((q|0)<0){break xa}h=$;g=ib}H[Q+4>>2]=H[Q+4>>2]-((H[Ya>>2]+b|0)+2>>2);H[Q+8>>2]=H[Q+8>>2]-((H[h>>2]+H[Xa>>2]|0)+2>>2);b=H[Wa>>2];g=H[g>>2];break wa}if(rb){H[o>>2]=H[o>>2]/2;H[o+4>>2]=H[o+4>>2]/2;H[o+8>>2]=H[o+8>>2]/2;H[o+12>>2]=H[o+12>>2]/2;break ua}g=q;if((E|0)>(g|0)){while(1){b=o+(g<<5)|0;za:{if((g|0)<0){h=H[o>>2];i=Ma|(g|0)!=-1;H[b+16>>2]=H[b+16>>2]-(((i?h:H[ta>>2])+h|0)+2>>2);h=H[ha>>2];H[b+20>>2]=H[b+20>>2]-(((i?h:H[pa>>2])+h|0)+2>>2);h=H[o+8>>2];H[b+24>>2]=H[b+24>>2]-(((i?h:H[fa>>2])+h|0)+2>>2);h=b;t=H[b+28>>2];b=H[o+12>>2];H[h+28>>2]=t-(((i?b:H[ia>>2])+b|0)+2>>2);g=g+1|0;break za}i=g+1|0;Aa:{if((i|0)<(ka|0)){g=o+(i<<5)|0;H[b+16>>2]=H[b+16>>2]-((H[b>>2]+H[g>>2]|0)+2>>2);H[b+20>>2]=H[b+20>>2]-((H[b+4>>2]+H[g+4>>2]|0)+2>>2);H[b+24>>2]=H[b+24>>2]-((H[b+8>>2]+H[g+8>>2]|0)+2>>2);H[b+28>>2]=H[b+28>>2]-((H[b+12>>2]+H[g+12>>2]|0)+2>>2);break Aa}h=H[b+16>>2];if((g|0)>=(ka|0)){H[b+16>>2]=h-((H[ta>>2]<<1)+2>>2);H[b+20>>2]=H[b+20>>2]-((H[pa>>2]<<1)+2>>2);H[b+24>>2]=H[b+24>>2]-((H[fa>>2]<<1)+2>>2);H[b+28>>2]=H[b+28>>2]-((H[ia>>2]<<1)+2>>2);break Aa}H[b+16>>2]=h-((H[b>>2]+H[ta>>2]|0)+2>>2);H[b+20>>2]=H[b+20>>2]-((H[b+4>>2]+H[pa>>2]|0)+2>>2);H[b+24>>2]=H[b+24>>2]-((H[b+8>>2]+H[fa>>2]|0)+2>>2);H[b+28>>2]=H[b+28>>2]-((H[b+12>>2]+H[ia>>2]|0)+2>>2)}g=i}if((E|0)!=(g|0)){continue}break}}g=j;if((V|0)<=(g|0)){break ua}while(1){b=o+(g<<5)|0;Ba:{if((g|0)<0){H[b>>2]=H[b>>2]+(H[o+16>>2]<<1>>1);H[b+4>>2]=H[b+4>>2]+(H[o+20>>2]<<1>>1);H[b+8>>2]=H[b+8>>2]+(H[o+24>>2]<<1>>1);H[b+12>>2]=H[b+12>>2]+(H[o+28>>2]<<1>>1);break Ba}i=H[b>>2];if(!g){h=(g|0)<(u|0);H[b>>2]=i+(H[o+16>>2]+H[(h?b+16|0:la)>>2]>>1);H[b+4>>2]=H[b+4>>2]+(H[o+20>>2]+H[(h?b+20|0:da)>>2]>>1);H[b+8>>2]=H[b+8>>2]+(H[o+24>>2]+H[(h?b+24|0:ga)>>2]>>1);H[b+12>>2]=H[b+12>>2]+(H[o+28>>2]+H[(h?b+28|0:aa)>>2]>>1);break Ba}if((g|0)<=(u|0)){h=(g|0)<(u|0);H[b>>2]=i+(H[b-16>>2]+H[(h?b+16|0:la)>>2]>>1);H[b+4>>2]=H[b+4>>2]+(H[b-12>>2]+H[(h?b+20|0:da)>>2]>>1);H[b+8>>2]=H[b+8>>2]+(H[b-8>>2]+H[(h?b+24|0:ga)>>2]>>1);H[b+12>>2]=H[b+12>>2]+(H[b-4>>2]+H[(h?b+28|0:aa)>>2]>>1);break Ba}H[b>>2]=i+(H[la>>2]<<1>>1);H[b+4>>2]=H[b+4>>2]+(H[da>>2]<<1>>1);H[b+8>>2]=H[b+8>>2]+(H[ga>>2]<<1>>1);H[b+12>>2]=H[b+12>>2]+(H[aa>>2]<<1>>1)}g=g+1|0;if((V|0)!=(g|0)){continue}break}break ua}m=R;u=Z;sa=sa+1|0;if((Y|0)!=(sa|0)){continue ea}break da}H[Q+4>>2]=H[Q+4>>2]-((b<<1)+2>>2);H[Q+8>>2]=H[Q+8>>2]-((H[$>>2]<<1)+2>>2);b=H[ib>>2];g=b}H[Q+12>>2]=H[Q+12>>2]-((b+g|0)+2>>2);i=q;b=r;g=b;if((b|0)<(n|0)){while(1){g=o+(b<<5)|0;i=o+(i<<5)|0;H[g>>2]=H[g>>2]-((H[i+16>>2]+H[g+16>>2]|0)+2>>2);H[g+4>>2]=H[g+4>>2]-((H[i+20>>2]+H[g+20>>2]|0)+2>>2);H[g+8>>2]=H[g+8>>2]-((H[i+24>>2]+H[g+24>>2]|0)+2>>2);H[g+12>>2]=H[g+12>>2]-((H[i+28>>2]+H[g+28>>2]|0)+2>>2);i=b;b=b+1|0;if((n|0)!=(b|0)){continue}break}g=n}if((g|0)>=(E|0)){break va}while(1){b=o+(g<<5)|0;i=(g|0)<(ka|0);Ca:{if((g|0)<=0){h=H[Ra>>2];if((g|0)>=0){H[b>>2]=H[b>>2]-((h+H[(i?b+16|0:Ka)>>2]|0)+2>>2);H[b+4>>2]=H[b+4>>2]-((H[o+20>>2]+H[(i?b+20|0:Ga)>>2]|0)+2>>2);H[b+8>>2]=H[b+8>>2]-((H[o+24>>2]+H[(i?b+24|0:ya)>>2]|0)+2>>2);H[b+12>>2]=H[b+12>>2]-((H[o+28>>2]+H[(i?b+28|0:va)>>2]|0)+2>>2);break Ca}H[b>>2]=H[b>>2]-((h<<1)+2>>2);H[b+4>>2]=H[b+4>>2]-((H[o+20>>2]<<1)+2>>2);H[b+8>>2]=H[b+8>>2]-((H[o+24>>2]<<1)+2>>2);H[b+12>>2]=H[b+12>>2]-((H[o+28>>2]<<1)+2>>2);break Ca}h=o+((i?g:ka)<<5)|0;t=H[h-16>>2];if(!i){H[b>>2]=H[b>>2]-((t+H[Ka>>2]|0)+2>>2);H[b+4>>2]=H[b+4>>2]-((H[h-12>>2]+H[Ga>>2]|0)+2>>2);H[b+8>>2]=H[b+8>>2]-((H[h-8>>2]+H[ya>>2]|0)+2>>2);H[b+12>>2]=H[b+12>>2]-((H[h-4>>2]+H[va>>2]|0)+2>>2);break Ca}H[b>>2]=H[b>>2]-((t+H[b+16>>2]|0)+2>>2);H[b+4>>2]=H[b+4>>2]-((H[h-12>>2]+H[b+20>>2]|0)+2>>2);H[b+8>>2]=H[b+8>>2]-((H[h-8>>2]+H[b+24>>2]|0)+2>>2);H[b+12>>2]=H[b+12>>2]-((H[h-4>>2]+H[b+28>>2]|0)+2>>2)}g=g+1|0;if((E|0)!=(g|0)){continue}break}}if((j|0)>=(V|0)){break ua}g=j;if((xa|0)>(g|0)){while(1){b=o+(g<<5)|0;H[b+16>>2]=H[b+16>>2]+(H[b+32>>2]+H[b>>2]>>1);H[b+20>>2]=H[b+20>>2]+(H[b+36>>2]+H[b+4>>2]>>1);H[b+24>>2]=H[b+24>>2]+(H[b+40>>2]+H[b+8>>2]>>1);H[b+28>>2]=H[b+28>>2]+(H[b+44>>2]+H[b+12>>2]>>1);g=g+1|0;if(($a|0)>(g|0)){continue}break}}if((g|0)>=(V|0)){break ua}while(1){b=o+(g<<5)|0;Da:{Ea:{Fa:{if((g|0)<0){i=H[o>>2];if((g|0)!=-1){break Fa}if((u|0)<=0){H[b+16>>2]=H[b+16>>2]+(i+H[ba>>2]>>1);H[b+20>>2]=H[b+20>>2]+(H[ea>>2]+H[o+4>>2]>>1);H[b+24>>2]=H[b+24>>2]+(H[z>>2]+H[o+8>>2]>>1);t=H[o+12>>2];i=H[S>>2];break Ea}break Fa}i=g+1|0;Ga:{if((i|0)<(u|0)){g=o+(i<<5)|0;H[b+16>>2]=H[b+16>>2]+(H[g>>2]+H[b>>2]>>1);H[b+20>>2]=H[b+20>>2]+(H[g+4>>2]+H[b+4>>2]>>1);H[b+24>>2]=H[b+24>>2]+(H[g+8>>2]+H[b+8>>2]>>1);H[b+28>>2]=H[b+28>>2]+(H[g+12>>2]+H[b+12>>2]>>1);break Ga}if((g|0)>=(u|0)){H[b+16>>2]=H[b+16>>2]+H[ba>>2];H[b+20>>2]=H[b+20>>2]+H[ea>>2];H[b+24>>2]=H[b+24>>2]+H[z>>2];H[b+28>>2]=H[b+28>>2]+H[S>>2];break Ga}H[b+16>>2]=H[b+16>>2]+(H[ba>>2]+H[b>>2]>>1);H[b+20>>2]=H[b+20>>2]+(H[ea>>2]+H[b+4>>2]>>1);H[b+24>>2]=H[b+24>>2]+(H[z>>2]+H[b+8>>2]>>1);H[b+28>>2]=H[b+28>>2]+(H[S>>2]+H[b+12>>2]>>1)}g=i;break Da}H[b+16>>2]=i+H[b+16>>2];H[b+20>>2]=H[b+20>>2]+H[o+4>>2];H[b+24>>2]=H[b+24>>2]+H[o+8>>2];t=H[o+12>>2];i=t}H[b+28>>2]=H[b+28>>2]+(i+t>>1);g=g+1|0}if((V|0)!=(g|0)){continue}break}}if(Za(oa,m,mb,l,Ua,qb,1,4)){continue}break}break}break ba}Ca(o);g=1}b=H[ma-16>>2];i=H[gb>>2];h=H[hb>>2];q=H[ma-8>>2];Oa(oa,b-i|0,H[ma-12>>2]-h|0,q-i|0,H[ma-4>>2]-h|0,H[C+52>>2],1,q-b|0);Va(oa);break X}Va(oa);Ca(o);g=0;break X}Va(oa);g=0;break X}g=0;Sa(m);Ca(j)}na=_+32|0;if(g){break W}break b}h=0;J=na+-64|0;na=J;Ha:{Ia:{if(H[D+64>>2]){r=H[C+28>>2];i=r+N(H[C+24>>2],152)|0;k=H[i-152>>2];o=1;x=H[D+44>>2];M=H[x+4>>2];if((j|0)==1){break Ha}t=j-1|0;b=r;Ja:{if((j|0)!=2){q=t&1;n=t&-2;g=0;while(1){j=H[b+160>>2]-H[b+152>>2]|0;h=h>>>0>j>>>0?h:j;j=H[b+164>>2]-H[b+156>>2]|0;h=h>>>0>j>>>0?h:j;j=H[b+312>>2]-H[b+304>>2]|0;h=h>>>0>j>>>0?h:j;j=H[b+316>>2]-H[b+308>>2]|0;h=h>>>0>j>>>0?h:j;b=b+304|0;g=g+2|0;if((n|0)!=(g|0)){continue}break}if(!q){break Ja}}g=H[b+160>>2]-H[b+152>>2]|0;g=g>>>0>>0?h:g;b=H[b+164>>2]-H[b+156>>2]|0;h=b>>>0>>0?g:b}o=0;if(h>>>0>134217727){break Ha}b=H[i-144>>2];g=H[r+4>>2];i=H[r+12>>2];n=H[r>>2];j=H[r+8>>2];$=h<<5;E=Ia($);H[J+32>>2]=E;if(!E){break Ha}q=i-g|0;i=j-n|0;g=M>>>1|0;ea=g>>>0<=2?2:g;u=b-k|0;aa=u<<5;ga=N(u,28);_=N(u,24);da=N(u,20);ka=u<<4;W=N(u,12);Z=u<<3;H[J>>2]=E;ba=E+32|0;b=H[C+36>>2];while(1){l=q;H[J+8>>2]=l;h=i;H[J+40>>2]=h;Y=H[r+156>>2];w=H[r+164>>2];n=H[r+160>>2];g=H[r+152>>2];H[J+56>>2]=0;H[J+52>>2]=h;H[J+48>>2]=0;R=(g|0)%2|0;H[J+44>>2]=R;i=n-g|0;j=i-h|0;H[J+60>>2]=j;H[J+36>>2]=j;S=(M|0)<2;q=w-Y|0;Ka:{if(!(!S&q>>>0>15)){o=0;k=b;if(q>>>0<8){break Ka}m=i&-2;Q=i&1;j=0;R=(n|0)==(g+1|0);while(1){g=J+32|0;zb(g,k,u,8);Ta(g);if(i){h=0;o=0;La:{if(!R){while(1){g=(h<<2)+k|0;n=E+(h<<5)|0;L[g>>2]=L[n>>2];z=u<<2;L[z+g>>2]=L[n+4>>2];L[g+Z>>2]=L[n+8>>2];L[g+W>>2]=L[n+12>>2];n=h|1;g=(n<<2)+k|0;n=E+(n<<5)|0;L[g>>2]=L[n>>2];L[g+z>>2]=L[n+4>>2];L[g+Z>>2]=L[n+8>>2];L[g+W>>2]=L[n+12>>2];h=h+2|0;o=o+2|0;if((m|0)!=(o|0)){continue}break}if(!Q){break La}}g=(h<<2)+k|0;h=E+(h<<5)|0;L[g>>2]=L[h>>2];L[g+(u<<2)>>2]=L[h+4>>2];L[g+Z>>2]=L[h+8>>2];L[g+W>>2]=L[h+12>>2]}h=0;while(1){g=(h<<2)+k|0;n=E+(h<<5)|0;L[g+ka>>2]=L[n+16>>2];L[g+da>>2]=L[n+20>>2];L[g+_>>2]=L[n+24>>2];L[g+ga>>2]=L[n+28>>2];h=h+1|0;if((i|0)!=(h|0)){continue}break}}k=k+aa|0;g=j+15|0;o=j+8|0;j=o;if(g>>>0>>0){continue}break}break Ka}g=q>>>3|0;m=g>>>0>>0?g:M;Q=(q>>>0)/(m>>>0)&-8;o=q&-8;g=0;k=b;while(1){n=Fa(48);if(!n){break Ia}z=Ia($);H[n>>2]=z;if(!z){o=0;Sa(x);Ca(n);Ca(E);break Ha}H[n+40>>2]=k;H[n+36>>2]=u;H[n+32>>2]=i;H[n+28>>2]=j;H[n+24>>2]=0;H[n+20>>2]=h;H[n+16>>2]=0;H[n+12>>2]=R;H[n+8>>2]=h;H[n+4>>2]=j;V=o-N(g,Q)|0;g=g+1|0;z=(m|0)==(g|0)?V:Q;H[n+44>>2]=z;eb(x,12,n);k=(N(u,z)<<2)+k|0;if((g|0)!=(m|0)){continue}break}Sa(x)}Ma:{if(o>>>0>=q>>>0){break Ma}h=J+32|0;g=q-o|0;zb(h,k,u,g);Ta(h);if(!i){break Ma}R=g&-4;Q=g&3;m=0;z=Y+(o-w|0)>>>0>4294967292;while(1){j=(m<<2)+k|0;o=E+(m<<5)|0;h=0;n=0;g=0;Na:{if(!z){while(1){L[j+(N(h,u)<<2)>>2]=L[o+(h<<2)>>2];g=h|1;L[j+(N(g,u)<<2)>>2]=L[o+(g<<2)>>2];g=h|2;L[j+(N(g,u)<<2)>>2]=L[o+(g<<2)>>2];g=h|3;L[j+(N(g,u)<<2)>>2]=L[o+(g<<2)>>2];h=h+4|0;n=n+4|0;if((R|0)!=(n|0)){continue}break}g=h;if(!Q){break Na}}h=0;while(1){L[j+(N(g,u)<<2)>>2]=L[o+(g<<2)>>2];g=g+1|0;h=h+1|0;if((Q|0)!=(h|0)){continue}break}}m=m+1|0;if((m|0)!=(i|0)){continue}break}}n=q-l|0;H[J+4>>2]=n;g=H[r+156>>2];H[J+28>>2]=n;H[J+24>>2]=0;H[J+20>>2]=l;H[J+16>>2]=0;Q=(g|0)%2|0;H[J+12>>2]=Q;Oa:{if(!(!S&i>>>0>15)){g=b;if(i>>>0<8){break Oa}oa=q&-2;la=q&1;ia=n&-2;ha=n&1;V=l&-2;ma=l&1;h=Q<<5;R=ba-h|0;S=h+E|0;fa=N(l,u)<<2;pa=w-1|0;sa=(pa|0)==(l+Y|0);j=i;while(1){h=0;k=0;Pa:{Qa:{switch(l|0){default:while(1){m=(N(h,u)<<2)+g|0;z=H[m+28>>2];o=S+(h<<6)|0;H[o+24>>2]=H[m+24>>2];H[o+28>>2]=z;z=H[m+20>>2];H[o+16>>2]=H[m+16>>2];H[o+20>>2]=z;z=H[m+12>>2];H[o+8>>2]=H[m+8>>2];H[o+12>>2]=z;z=H[m+4>>2];H[o>>2]=H[m>>2];H[o+4>>2]=z;o=h|1;m=S+(o<<6)|0;o=(N(o,u)<<2)+g|0;z=H[o+28>>2];H[m+24>>2]=H[o+24>>2];H[m+28>>2]=z;z=H[o+20>>2];H[m+16>>2]=H[o+16>>2];H[m+20>>2]=z;z=H[o+12>>2];H[m+8>>2]=H[o+8>>2];H[m+12>>2]=z;z=H[o+4>>2];H[m>>2]=H[o>>2];H[m+4>>2]=z;h=h+2|0;k=k+2|0;if((V|0)!=(k|0)){continue}break};if(!ma){break Pa}break;case 0:break Pa;case 1:break Qa}}k=S+(h<<6)|0;h=(N(h,u)<<2)+g|0;m=H[h+28>>2];H[k+24>>2]=H[h+24>>2];H[k+28>>2]=m;m=H[h+20>>2];H[k+16>>2]=H[h+16>>2];H[k+20>>2]=m;m=H[h+12>>2];H[k+8>>2]=H[h+8>>2];H[k+12>>2]=m;m=H[h+4>>2];H[k>>2]=H[h>>2];H[k+4>>2]=m}Ra:{if((l|0)==(q|0)){break Ra}z=g+fa|0;h=0;m=0;if(!sa){while(1){k=z+(N(h,u)<<2)|0;ja=H[k+28>>2];o=R+(h<<6)|0;H[o+24>>2]=H[k+24>>2];H[o+28>>2]=ja;ja=H[k+20>>2];H[o+16>>2]=H[k+16>>2];H[o+20>>2]=ja;ja=H[k+12>>2];H[o+8>>2]=H[k+8>>2];H[o+12>>2]=ja;ja=H[k+4>>2];H[o>>2]=H[k>>2];H[o+4>>2]=ja;o=h|1;k=R+(o<<6)|0;o=z+(N(o,u)<<2)|0;ja=H[o+28>>2];H[k+24>>2]=H[o+24>>2];H[k+28>>2]=ja;ja=H[o+20>>2];H[k+16>>2]=H[o+16>>2];H[k+20>>2]=ja;ja=H[o+12>>2];H[k+8>>2]=H[o+8>>2];H[k+12>>2]=ja;ja=H[o+4>>2];H[k>>2]=H[o>>2];H[k+4>>2]=ja;h=h+2|0;m=m+2|0;if((ia|0)!=(m|0)){continue}break}if(!ha){break Ra}}k=R+(h<<6)|0;h=z+(N(h,u)<<2)|0;m=H[h+28>>2];H[k+24>>2]=H[h+24>>2];H[k+28>>2]=m;m=H[h+20>>2];H[k+16>>2]=H[h+16>>2];H[k+20>>2]=m;m=H[h+12>>2];H[k+8>>2]=H[h+8>>2];H[k+12>>2]=m;m=H[h+4>>2];H[k>>2]=H[h>>2];H[k+4>>2]=m}Ta(J);Sa:{if(!q){break Sa}h=0;k=0;if((Y|0)!=(pa|0)){while(1){m=E+(h<<5)|0;z=H[m+28>>2];o=(N(h,u)<<2)+g|0;H[o+24>>2]=H[m+24>>2];H[o+28>>2]=z;z=H[m+20>>2];H[o+16>>2]=H[m+16>>2];H[o+20>>2]=z;z=H[m+12>>2];H[o+8>>2]=H[m+8>>2];H[o+12>>2]=z;z=H[m+4>>2];H[o>>2]=H[m>>2];H[o+4>>2]=z;o=h|1;m=(N(o,u)<<2)+g|0;o=E+(o<<5)|0;z=H[o+28>>2];H[m+24>>2]=H[o+24>>2];H[m+28>>2]=z;z=H[o+20>>2];H[m+16>>2]=H[o+16>>2];H[m+20>>2]=z;z=H[o+12>>2];H[m+8>>2]=H[o+8>>2];H[m+12>>2]=z;z=H[o+4>>2];H[m>>2]=H[o>>2];H[m+4>>2]=z;h=h+2|0;k=k+2|0;if((oa|0)!=(k|0)){continue}break}if(!la){break Sa}}k=(N(h,u)<<2)+g|0;h=E+(h<<5)|0;m=H[h+28>>2];H[k+24>>2]=H[h+24>>2];H[k+28>>2]=m;m=H[h+20>>2];H[k+16>>2]=H[h+16>>2];H[k+20>>2]=m;m=H[h+12>>2];H[k+8>>2]=H[h+8>>2];H[k+12>>2]=m;m=H[h+4>>2];H[k>>2]=H[h>>2];H[k+4>>2]=m}g=g+32|0;j=j-8|0;if(j>>>0>7){continue}break}break Oa}g=i>>>3|0;k=g>>>0>>0?g:ea;m=(i>>>0)/(k>>>0)&-8;o=i&-8;j=0;g=b;while(1){h=Fa(48);if(!h){break Ia}R=Ia($);H[h>>2]=R;if(!R){o=0;Sa(x);Ca(h);Ca(E);break Ha}H[h+40>>2]=g;H[h+36>>2]=u;H[h+32>>2]=q;H[h+28>>2]=n;H[h+24>>2]=0;H[h+20>>2]=l;H[h+16>>2]=0;H[h+12>>2]=Q;H[h+8>>2]=l;H[h+4>>2]=n;V=o-N(j,m)|0;j=j+1|0;R=(k|0)==(j|0)?V:m;H[h+44>>2]=R;eb(x,13,h);g=(R<<2)+g|0;if((k|0)!=(j|0)){continue}break}Sa(x)}j=i&7;Ta:{if(!j){break Ta}Q=Q<<5;Ua:{if(!l){break Ua}m=E+Q|0;k=j<<2;h=0;if((l|0)!=1){R=l&1;S=l&-2;o=0;while(1){z=!k;if(!z){B(m+(h<<6)|0,(N(h,u)<<2)+g|0,k)}if(!z){z=h|1;B(m+(z<<6)|0,(N(u,z)<<2)+g|0,k)}h=h+2|0;o=o+2|0;if((S|0)!=(o|0)){continue}break}if(!R){break Ua}}if(!k){break Ua}B(m+(h<<6)|0,(N(h,u)<<2)+g|0,k)}Va:{if((l|0)==(q|0)){break Va}m=ba-Q|0;Q=(N(l,u)<<2)+g|0;k=j<<2;h=0;if((Y|0)!=(w+(l^-1)|0)){l=n&1;n=n&-2;o=0;while(1){R=!k;if(!R){B(m+(h<<6)|0,Q+(N(h,u)<<2)|0,k)}if(!R){R=h|1;B(m+(R<<6)|0,Q+(N(u,R)<<2)|0,k)}h=h+2|0;o=o+2|0;if((n|0)!=(o|0)){continue}break}if(!l){break Va}}if(!k){break Va}B(m+(h<<6)|0,Q+(N(h,u)<<2)|0,k)}Ta(J);if(!q){break Ta}k=j<<2;h=0;if((w|0)!=(Y+1|0)){n=q&1;j=q&-2;o=0;while(1){m=!k;if(!m){B((N(h,u)<<2)+g|0,E+(h<<5)|0,k)}if(!m){m=h|1;B((N(m,u)<<2)+g|0,E+(m<<5)|0,k)}h=h+2|0;o=o+2|0;if((j|0)!=(o|0)){continue}break}if(!n){break Ta}}if(!k){break Ta}B((N(h,u)<<2)+g|0,E+(h<<5)|0,k)}r=r+152|0;t=t-1|0;if(t){continue}break}o=1;Ca(E);break Ha}o=1;q=H[C+28>>2];Q=q+N(j,152)|0;da=Q-152|0;if(H[da>>2]==H[Q-144>>2]){break Ha}ka=Q-148|0;if(H[ka>>2]==H[Q-140>>2]){break Ha}g=H[q+4>>2];i=H[q+12>>2];k=H[q>>2];n=H[q+8>>2];t=H[C+68>>2];E=H[C+64>>2];x=H[C+60>>2];Y=H[C+56>>2];Z=_b(C,j);if(!Z){o=0;break Ha}if((j|0)==1){b=H[Q-16>>2];g=H[da>>2];i=H[ka>>2];h=H[Q-8>>2];Oa(Z,b-g|0,H[Q-12>>2]-i|0,h-g|0,H[Q-4>>2]-i|0,H[C+52>>2],1,h-b|0);Va(Z);break Ha}b=q;Wa:{if((j|0)!=2){h=j-1|0;m=h&1;l=h&-2;h=0;o=0;while(1){r=H[b+160>>2]-H[b+152>>2]|0;h=h>>>0>r>>>0?h:r;r=H[b+164>>2]-H[b+156>>2]|0;h=h>>>0>r>>>0?h:r;r=H[b+312>>2]-H[b+304>>2]|0;h=h>>>0>r>>>0?h:r;r=H[b+316>>2]-H[b+308>>2]|0;h=h>>>0>r>>>0?h:r;b=b+304|0;o=o+2|0;if((l|0)!=(o|0)){continue}break}if(!m){break Wa}}m=H[b+160>>2]-H[b+152>>2]|0;h=h>>>0>m>>>0?h:m;b=H[b+164>>2]-H[b+156>>2]|0;h=b>>>0>>0?h:b}Xa:{if(h>>>0>=134217728){break Xa}R=Ia(h<<5);H[J+32>>2]=R;if(!R){break Xa}H[J>>2]=R;Ya:{if(j){r=i-g|0;b=n-k|0;oa=R+32|0;W=j;la=H[C+20>>2];z=1;ea=0;while(1){H[J+8>>2]=r;H[J+40>>2]=b;i=H[q+164>>2];h=H[q+160>>2];g=H[q+156>>2];k=H[q+152>>2];$=(k|0)%2|0;H[J+44>>2]=$;ia=(g|0)%2|0;H[J+12>>2]=ia;M=h-k|0;ba=M-b|0;H[J+36>>2]=ba;k=i-g|0;ha=k-r|0;H[J+4>>2]=ha;i=Y;o=i;h=x;g=h;j=E;m=j;n=t;u=n;Za:{if(!ea&(z|0)==(la|0)){break Za}w=la-z|0;g=0;o=0;if(i){i=w&31;if((w&63)>>>0>=32){l=-1<>>32-i}i=Y+(h^-1)|0;h=l^-1;n=i>>>0>>0?h+1|0:h;h=w&31;if((w&63)>>>0>=32){o=n>>>h|0}else{o=((1<>>h}}if(x){g=w&31;if((w&63)>>>0>=32){l=-1<>>32-g}g=x+(i^-1)|0;i=l^-1;h=g>>>0>>0?i+1|0:i;i=w&31;if((w&63)>>>0>=32){g=h>>>i|0}else{g=((1<>>i}}n=0;j=0;if(E){i=w&31;if((w&63)>>>0>=32){l=-1<>>32-i}i=E+(h^-1)|0;h=l^-1;j=i>>>0>>0?h+1|0:h;h=w&31;if((w&63)>>>0>=32){j=j>>>h|0}else{j=((1<>>h}}if(t){i=w&31;if((w&63)>>>0>=32){l=-1<>>32-i}i=t+(h^-1)|0;h=l^-1;n=i>>>0>>0?h+1|0:h;h=w&31;if((w&63)>>>0>=32){n=n>>>h|0}else{n=((1<>>h}}m=0;i=0;S=1<>>0>>0){i=w&31;if((w&63)>>>0>=32){l=-1<>>32-i}h=h^-1;i=h+(Y-S|0)|0;l=l^-1;l=h>>>0>i>>>0?l+1|0:l;h=w&31;if((w&63)>>>0>=32){i=l>>>h|0}else{i=((1<>>h}}if(E>>>0>S>>>0){h=w&31;if((w&63)>>>0>=32){l=-1<>>32-h}m=m^-1;h=m+(E-S|0)|0;l=l^-1;l=h>>>0>>0?l+1|0:l;m=w&31;if((w&63)>>>0>=32){m=l>>>m|0}else{m=((1<>>m}}u=0;h=0;if(x>>>0>S>>>0){h=w&31;if((w&63)>>>0>=32){l=-1<>>32-h}aa=aa^-1;h=aa+(x-S|0)|0;l=l^-1;aa=h>>>0>>0?l+1|0:l;l=w&31;if((w&63)>>>0>=32){h=aa>>>l|0}else{h=((1<>>l}}if(t>>>0<=S>>>0){break Za}l=w&31;if((w&63)>>>0>=32){l=-1<>>32-l}aa=u^-1;u=aa+(t-S|0)|0;l=l^-1;S=u>>>0>>0?l+1|0:l;l=w&31;if((w&63)>>>0>=32){u=S>>>l|0}else{u=((1<>>l}}l=H[q+180>>2];w=m-l|0;m=m>>>0>=w>>>0?w:0;w=m+4|0;m=m>>>0>w>>>0?-1:w;ga=m>>>0>>0?m:ba;m=H[q+216>>2];w=j-m|0;j=j>>>0>=w>>>0?w:0;w=j+4|0;j=j>>>0>w>>>0?-1:w;_=b>>>0>j>>>0?j:b;j=($?ga:_)<<1;w=($?_:ga)<<1|1;j=j>>>0>w>>>0?j:w;S=j>>>0>>0;l=i-l|0;i=i>>>0>=l>>>0?l:0;l=i-4|0;ba=i>>>0>=l>>>0?l:0;i=o-m|0;i=i>>>0<=o>>>0?i:0;m=i-4|0;aa=i>>>0>=m>>>0?m:0;V=($?ba:aa)<<1;ma=($?aa:ba)<<1|1;fa=V>>>0>>0;m=H[q+184>>2];i=g-m|0;g=g>>>0>=i>>>0?i:0;i=g-4|0;i=g>>>0>=i>>>0?i:0;o=i;g=H[q+220>>2];l=h-g|0;h=h>>>0>=l>>>0?l:0;l=h-4|0;h=h>>>0>=l>>>0?l:0;l=h;m=n-m|0;n=m>>>0<=n>>>0?m:0;m=n+4|0;n=m>>>0>>0?-1:m;n=n>>>0>>0?n:r;m=n;g=u-g|0;g=g>>>0<=u>>>0?g:0;u=g+4|0;g=g>>>0>u>>>0?-1:u;w=g>>>0>>0?g:ha;u=w;if(ia){l=i;u=m;o=h;m=w}S=S?j:M;j=fa?V:ma;H[J+60>>2]=ga;H[J+56>>2]=ba;H[J+52>>2]=_;H[J+48>>2]=aa;_a:{if(k>>>0<8){b=7;g=0;break _a}g=$<<5;ha=(oa-g|0)+(ba<<6)|0;V=(g+R|0)+(aa<<6)|0;ga=b+ga|0;ma=b+ba|0;fa=r+w|0;pa=h+r|0;sa=R+(j<<5)|0;g=0;while(1){b=g|7;$a:{if(!(g>>>0>>0&b>>>0>=i>>>0|g>>>0>>0&b>>>0>=pa>>>0)){g=g+8|0;break $a}b=k-g|0;ja=b>>>0>=8?8:b;b=0;while(1){$=b+g|0;ba=$+1|0;wa=b<<2;Oa(Z,aa,$,_,ba,wa+V|0,16,0);Oa(Z,ma,$,ga,ba,ha+wa|0,16,0);b=b+1|0;if((ja|0)!=(b|0)){continue}break}Ta(J+32|0);b=g;g=g+8|0;if(!Za(Z,j,b,S,g,sa,8,1)){break Ya}}b=g|7;if(k>>>0>b>>>0){continue}break}}if(!(!(g>>>0>>0&b>>>0>=i>>>0)&(r+w>>>0<=g>>>0|h+r>>>0>b>>>0)|g>>>0>=k>>>0)){b=J+32|0;$=0;ga=k-g|0;if(ga){while(1){ba=g+$|0;aa=ba+1|0;_=H[b+16>>2];ha=$<<2;Oa(Z,_,ba,H[b+20>>2],aa,ha+((H[b>>2]+(H[b+12>>2]<<5)|0)+(_<<6)|0)|0,16,0);_=H[b+24>>2];V=H[b+8>>2];Oa(Z,_+V|0,ba,V+H[b+28>>2]|0,aa,(ha+((H[b>>2]-(H[b+12>>2]<<5)|0)+(_<<6)|0)|0)+32|0,16,0);$=$+1|0;if((ga|0)!=($|0)){continue}break}}Ta(b);if(!Za(Z,j,g,S,k,R+(j<<5)|0,8,1)){break Ya}}H[J+28>>2]=w;H[J+24>>2]=h;H[J+20>>2]=n;H[J+16>>2]=i;if(j>>>0>>0){b=m<<1;g=u<<1|1;b=b>>>0>g>>>0?b:g;g=b>>>0>>0?b:k;b=ia<<5;m=(oa-b|0)+(h<<6)|0;u=(b+R|0)+(i<<6)|0;w=r+w|0;h=h+r|0;b=o<<1;l=l<<1|1;l=b>>>0>>0?b:l;r=R+(l<<5)|0;while(1){b=S-j|0;b=(b>>>0>=8?8:b)+j|0;Oa(Z,j,i,b,n,u,1,16);Oa(Z,j,h,b,w,m,1,16);Ta(J);if(!Za(Z,j,l,b,g,r,1,8)){break Ya}j=j+8|0;if(S>>>0>j>>>0){continue}break}}q=q+152|0;b=M;r=k;z=z+1|0;ea=z?ea:ea+1|0;if(ea|(z|0)!=(W|0)){continue}break}}o=1;b=H[Q-16>>2];g=H[da>>2];i=H[ka>>2];h=H[Q-8>>2];Oa(Z,b-g|0,H[Q-12>>2]-i|0,h-g|0,H[Q-4>>2]-i|0,H[C+52>>2],1,h-b|0);Va(Z);Ca(R);break Ha}Va(Z);Ca(R);o=0;break Ha}Va(Z);o=0;break Ha}o=0;Sa(x);Ca(E)}na=J- -64|0;if(o){break W}break b}s=s+1080|0;A=A+52|0;C=C+76|0;p=p+1|0;if(p>>>0>2]){continue}break}s=H[D+32>>2];v=H[H[D+20>>2]>>2]}k=H[s+16>>2];ab:{if(H[D+68>>2]|!k){break ab}A=H[v+20>>2];g=H[A+28>>2];bb:{cb:{i=H[D+64>>2];if(i){p=H[v+16>>2];if(p>>>0<3){break bb}b=H[A+24>>2];if(!((b|0)==H[A+100>>2]&(b|0)==H[A+176>>2])){Ba(f,1,10089,0);break b}h=H[H[D+24>>2]+24>>2];q=H[h+36>>2];db:{if((q|0)!=H[h+88>>2]|(q|0)!=H[h+140>>2]){break db}h=N(b,152);b=h+g|0;b=N(H[b-140>>2]-H[b-148>>2]|0,H[b-144>>2]-H[b-152>>2]|0);g=h+H[A+104>>2]|0;if((b|0)!=(N(H[g-140>>2]-H[g-148>>2]|0,H[g-144>>2]-H[g-152>>2]|0)|0)){break db}g=h+H[A+180>>2]|0;if((N(H[g-140>>2]-H[g-148>>2]|0,H[g-144>>2]-H[g-152>>2]|0)|0)==(b|0)){break cb}}Ba(f,1,10089,0);break b}p=H[v+16>>2];if(p>>>0<3){break bb}h=H[H[D+24>>2]+24>>2];b=H[h+36>>2];eb:{if((b|0)!=H[h+88>>2]){break eb}h=H[h+140>>2];if((h|0)!=(b|0)){break eb}q=N(b,152);b=g+q|0;b=N(H[b+148>>2]-H[b+140>>2]|0,H[b+144>>2]-H[b+136>>2]|0);g=q+H[A+104>>2]|0;if((b|0)!=(N(H[g+148>>2]-H[g+140>>2]|0,H[g+144>>2]-H[g+136>>2]|0)|0)){break eb}g=H[A+180>>2]+N(h,152)|0;if((N(H[g+148>>2]-H[g+140>>2]|0,H[g+144>>2]-H[g+136>>2]|0)|0)==(b|0)){break cb}}Ba(f,1,10089,0);break b}if((k|0)==2){if(!H[s+5608>>2]){break ab}i=Fa(p<<2);if(!i){break b}n=H[v+16>>2];fb:{if(!n){break fb}gb:{hb:{if(H[D+64>>2]){p=n&3;g=0;if(n>>>0>=4){break hb}C=0;break gb}p=n&3;g=0;ib:{if(n>>>0<4){C=0;break ib}k=n&-4;C=0;h=0;while(1){q=i+(C<<2)|0;H[q>>2]=H[A+52>>2];H[q+4>>2]=H[A+128>>2];H[q+8>>2]=H[A+204>>2];H[q+12>>2]=H[A+280>>2];C=C+4|0;A=A+304|0;h=h+4|0;if((k|0)!=(h|0)){continue}break}if(!p){break fb}}while(1){H[i+(C<<2)>>2]=H[A+52>>2];C=C+1|0;A=A+76|0;g=g+1|0;if((p|0)!=(g|0)){continue}break}break fb}k=n&-4;C=0;h=0;while(1){q=i+(C<<2)|0;H[q>>2]=H[A+36>>2];H[q+4>>2]=H[A+112>>2];H[q+8>>2]=H[A+188>>2];H[q+12>>2]=H[A+264>>2];C=C+4|0;A=A+304|0;h=h+4|0;if((k|0)!=(h|0)){continue}break}if(!p){break fb}}while(1){H[i+(C<<2)>>2]=H[A+36>>2];C=C+1|0;A=A+76|0;g=g+1|0;if((p|0)!=(g|0)){continue}break}}h=H[s+5608>>2];g=i;k=0;s=Fa(n<<3);i=0;jb:{if(!s){break jb}if(!(!b|!n)){i=b;u=s+(n<<2)|0;r=n&-4;m=n&3;o=n-1|0;while(1){v=0;p=0;b=0;t=o>>>0<3;kb:{if(!t){while(1){b=v<<2;L[b+s>>2]=L[H[b+g>>2]>>2];q=b|4;L[q+s>>2]=L[H[g+q>>2]>>2];q=b|8;L[q+s>>2]=L[H[g+q>>2]>>2];b=b|12;L[b+s>>2]=L[H[b+g>>2]>>2];v=v+4|0;p=p+4|0;if((r|0)!=(p|0)){continue}break}b=v;if(!m){break kb}}v=0;while(1){q=b<<2;L[q+s>>2]=L[H[g+q>>2]>>2];b=b+1|0;v=v+1|0;if((m|0)!=(v|0)){continue}break}}q=0;b=h;while(1){E=q<<2;j=E+u|0;H[j>>2]=0;ca=O(0);v=0;p=0;lb:{if(!t){while(1){l=s+(v<<2)|0;ca=O(O(L[b>>2]*L[l>>2])+ca);L[j>>2]=ca;ca=O(O(L[b+4>>2]*L[l+4>>2])+ca);L[j>>2]=ca;ca=O(O(L[b+8>>2]*L[l+8>>2])+ca);L[j>>2]=ca;ca=O(O(L[b+12>>2]*L[l+12>>2])+ca);L[j>>2]=ca;v=v+4|0;b=b+16|0;p=p+4|0;if((r|0)!=(p|0)){continue}break}p=v;if(!m){break lb}}v=0;while(1){ca=O(O(L[b>>2]*L[s+(p<<2)>>2])+ca);L[j>>2]=ca;p=p+1|0;b=b+4|0;v=v+1|0;if((m|0)!=(v|0)){continue}break}}v=g+E|0;p=H[v>>2];H[v>>2]=p+4;L[p>>2]=ca;q=q+1|0;if((n|0)!=(q|0)){continue}break}k=k+1|0;if((i|0)!=(k|0)){continue}break}}Ca(s);i=1}b=i;Ca(g);if(b){break ab}break b}if(H[H[s+5584>>2]+20>>2]==1){if(i){cc(H[A+36>>2],H[A+112>>2],H[A+188>>2],b);break ab}cc(H[A+52>>2],H[A+128>>2],H[A+204>>2],b);break ab}if(i){bc(H[A+36>>2],H[A+112>>2],H[A+188>>2],b);break ab}bc(H[A+52>>2],H[A+128>>2],H[A+204>>2],b);break ab}H[ra>>2]=p;Ba(f,1,10150,ra)}m=H[H[D+20>>2]>>2];if(!H[m+16>>2]){X=1;break b}l=H[D+68>>2];k=H[m+20>>2];b=H[H[D+32>>2]+5584>>2];q=H[H[D+24>>2]+24>>2];i=0;while(1){mb:{if(H[l+(i<<2)>>2]?0:l){break mb}h=H[k+28>>2];g=h+N(H[q+36>>2],152)|0;nb:{if(!H[D+64>>2]){h=H[g+148>>2]-H[g+140>>2]|0;v=H[g+144>>2]-H[g+136>>2]|0;r=0;p=52;break nb}h=h+N(H[k+24>>2],152)|0;v=H[g+8>>2]-H[g>>2]|0;r=H[h-144>>2]-(v+H[h-152>>2]|0)|0;h=H[g+12>>2]-H[g+4>>2]|0;p=36}g=H[q+24>>2];ob:{if(H[q+32>>2]){g=1<>2];if(H[b+20>>2]==1){n=v&-2;j=v&1;A=0;r=r<<2;while(1){p=0;pb:{if((v|0)!=1){while(1){g=H[b+1076>>2]+H[X>>2]|0;H[X>>2]=(g|0)<(s|0)?s:(g|0)<(C|0)?g:C;g=H[b+1076>>2]+H[X+4>>2]|0;H[X+4>>2]=(g|0)<(s|0)?s:(g|0)<(C|0)?g:C;X=X+8|0;p=p+2|0;if((n|0)!=(p|0)){continue}break}if(!j){break pb}}g=H[b+1076>>2]+H[X>>2]|0;H[X>>2]=(g|0)<(s|0)?s:(g|0)<(C|0)?g:C;X=X+4|0}X=r+X|0;A=A+1|0;if((A|0)!=(h|0)){continue}break}break mb}u=s>>31;g=0;while(1){p=0;while(1){ca=L[X>>2];j=C;qb:{if(ca>O(2147483648)){break qb}j=s;if(ca>2];j=n;za=ca;ca=O(T(ca));Pa=O(za-ca);if(!(PaO(.5)){break rb}Pa=ca;ca=O(ca*O(.5));za=O(ca-O(T(ca)))==O(0)?Pa:za}ca=za}t=n>>31;if(O(P(ca))>31)|0;j=j+n|0;n=n>>>0>j>>>0?o+1|0:o;j=j>>>0>>0&(n|0)<=(u|0)|(n|0)<(u|0)?s:j>>>0>>0&(n|0)<=0|(n|0)<0?j:C}H[X>>2]=j;X=X+4|0;p=p+1|0;if((v|0)!=(p|0)){continue}break}X=(r<<2)+X|0;g=g+1|0;if((h|0)!=(g|0)){continue}break}}k=k+76|0;b=b+1080|0;q=q+52|0;X=1;i=i+1|0;if(i>>>0>2]){continue}break}break b}X=0;Ba(f,1,3372,0)}na=ra+16|0;if(!X){jb(ua);H[a+8>>2]=H[a+8>>2]|32768;Ba(f,1,11451,0);break a}sb:{if(!c){break sb}b=0;q=H[a+232>>2];g=Rb(q,1);if(!((g|0)==-1|d>>>0>>0)){tb:{b=1;d=H[q+24>>2];if(!H[d+16>>2]){break tb}h=H[d+24>>2];k=H[H[H[q+20>>2]>>2]+20>>2];while(1){b=H[h+24>>2];i=b&7;v=b>>>3|0;d=H[k+28>>2];b=d+N(H[h+36>>2],152)|0;ub:{if(H[q+64>>2]){g=d+N(H[k+24>>2],152)|0;d=H[b+8>>2]-H[b>>2]|0;p=H[g-144>>2]-(d+H[g-152>>2]|0)|0;g=H[b+12>>2]-H[b+4>>2]|0;b=36;break ub}g=H[b+148>>2]-H[b+140>>2]|0;d=H[b+144>>2]-H[b+136>>2]|0;p=0;b=52}b=H[b+k>>2];vb:{wb:{xb:{yb:{i=v+((i|0)!=0)|0;switch(((i|0)==3?4:i)-1|0){case 0:break xb;case 1:break wb;case 3:break yb;default:break vb}}if(!g){break vb}d=d<<2;if((g|0)!=1){i=g&1;v=g&-2;g=0;while(1){s=!d;if(!s){B(c,b,d)}n=p<<2;b=n+(b+d|0)|0;c=c+d|0;if(!s){B(c,b,d)}c=c+d|0;b=n+(b+d|0)|0;g=g+2|0;if((v|0)!=(g|0)){continue}break}if(!i){break vb}}if(d){B(c,b,d)}c=c+d|0;break vb}i=!g|!d;if(H[h+32>>2]){if(i){break vb}s=d&-8;v=d&7;i=0;n=d-1>>>0<7;while(1){d=0;zb:{if(!n){while(1){F[c|0]=H[b>>2];F[c+1|0]=H[b+4>>2];F[c+2|0]=H[b+8>>2];F[c+3|0]=H[b+12>>2];F[c+4|0]=H[b+16>>2];F[c+5|0]=H[b+20>>2];F[c+6|0]=H[b+24>>2];F[c+7|0]=H[b+28>>2];c=c+8|0;b=b+32|0;d=d+8|0;if((s|0)!=(d|0)){continue}break}if(!v){break zb}}d=0;while(1){F[c|0]=H[b>>2];c=c+1|0;b=b+4|0;d=d+1|0;if((v|0)!=(d|0)){continue}break}}b=(p<<2)+b|0;i=i+1|0;if((i|0)!=(g|0)){continue}break}break vb}if(i){break vb}s=d&-8;v=d&7;i=0;n=d-1>>>0<7;p=p<<2;while(1){d=0;Ab:{if(!n){while(1){F[c|0]=H[b>>2];F[c+1|0]=H[b+4>>2];F[c+2|0]=H[b+8>>2];F[c+3|0]=H[b+12>>2];F[c+4|0]=H[b+16>>2];F[c+5|0]=H[b+20>>2];F[c+6|0]=H[b+24>>2];F[c+7|0]=H[b+28>>2];c=c+8|0;b=b+32|0;d=d+8|0;if((s|0)!=(d|0)){continue}break}if(!v){break Ab}}d=0;while(1){F[c|0]=H[b>>2];c=c+1|0;b=b+4|0;d=d+1|0;if((v|0)!=(d|0)){continue}break}}b=b+p|0;i=i+1|0;if((i|0)!=(g|0)){continue}break}break vb}i=!g|!d;if(H[h+32>>2]){if(i){break vb}s=d&-8;v=d&7;i=0;n=d-1>>>0<7;while(1){d=0;Bb:{if(!n){while(1){G[c>>1]=H[b>>2];G[c+2>>1]=H[b+4>>2];G[c+4>>1]=H[b+8>>2];G[c+6>>1]=H[b+12>>2];G[c+8>>1]=H[b+16>>2];G[c+10>>1]=H[b+20>>2];G[c+12>>1]=H[b+24>>2];G[c+14>>1]=H[b+28>>2];c=c+16|0;b=b+32|0;d=d+8|0;if((s|0)!=(d|0)){continue}break}if(!v){break Bb}}d=0;while(1){G[c>>1]=H[b>>2];c=c+2|0;b=b+4|0;d=d+1|0;if((v|0)!=(d|0)){continue}break}}b=(p<<2)+b|0;i=i+1|0;if((i|0)!=(g|0)){continue}break}break vb}if(i){break vb}s=d&-8;v=d&7;i=0;n=d-1>>>0<7;while(1){d=0;Cb:{if(!n){while(1){G[c>>1]=H[b>>2];G[c+2>>1]=H[b+4>>2];G[c+4>>1]=H[b+8>>2];G[c+6>>1]=H[b+12>>2];G[c+8>>1]=H[b+16>>2];G[c+10>>1]=H[b+20>>2];G[c+12>>1]=H[b+24>>2];G[c+14>>1]=H[b+28>>2];c=c+16|0;b=b+32|0;d=d+8|0;if((s|0)!=(d|0)){continue}break}if(!v){break Cb}}d=0;while(1){G[c>>1]=H[b>>2];c=c+2|0;b=b+4|0;d=d+1|0;if((v|0)!=(d|0)){continue}break}}b=(p<<2)+b|0;i=i+1|0;if((i|0)!=(g|0)){continue}break}}k=k+76|0;h=h+52|0;b=1;pb=pb+1|0;if(pb>>>0>2]+16>>2]){continue}break}}}if(!b){break a}b=H[ua+5596>>2];if(!b){break sb}Ca(b);H[ua+5596>>2]=0;H[ua+5600>>2]=0}F[a+92|0]=I[a+92|0]&254;H[a+8>>2]=H[a+8>>2]&-129;fb=1;c=Qa(e);b=H[a+8>>2];if(!(c|qa)&(b|0)==64|(b|0)==256){break a}if((Ja(e,Aa+10|0,2,f)|0)!=2){Ba(f,H[a+208>>2]?1:2,2472,0);fb=!H[a+208>>2];break a}Da(Aa+10|0,Aa+12|0,2);b=H[Aa+12>>2];if((b|0)==65424){break a}if((b|0)==65497){H[a+8>>2]=256;H[a+228>>2]=0;break a}if(!(Qa(e)|qa)){H[a+8>>2]=64;Ba(f,2,8419,0);break a}fb=0;Ba(f,1,8306,0)}na=Aa+16|0;return fb|0}function cb(a,b,c,d,e,f,g,h,i,j,k){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;i=i|0;j=j|0;k=k|0;var l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,v=0,x=0,z=0,A=0,C=0,D=0,E=0,J=0,M=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=O(0),Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,oa=0,pa=0,sa=0,ta=0,ua=0,va=O(0);t=na-80|0;na=t;H[t+40>>2]=65424;v=N(H[a+132>>2],H[a+128>>2]);a:{b:{c:{l=H[a+8>>2];d:{if((l|0)!=8){j=0;if((l|0)!=256){break a}H[t+40>>2]=65497;break d}if(F[a+92|0]&1){break d}Q=v&-2;V=v&1;D=t+77|0;R=t+76|0;U=t+72|0;l=65424;e:{f:{while(1){g:{h:{i:{j:{k:{l:{m:{n:{p=H[a+84>>2];if(!p){break n}n=H[a+80>>2];if(p>>>0<=n>>>0){break n}l=H[a+88>>2]+(n<<3)|0;p=H[l>>2];l=H[l+4>>2];H[a+80>>2]=n+1;if(!bb(j,p,l,k)){Ba(k,1,5440,0);j=0;break a}if((Ja(j,H[a+16>>2],2,k)|0)!=2){Ba(k,1,2472,0);j=0;break a}Da(H[a+16>>2],t+40|0,2);if(H[t+40>>2]==65424){break m}Ba(k,1,4073,0);j=0;break a}if((l|0)==65427){break l}}while(1){if(!(Qa(j)|qa)){H[a+8>>2]=64;break l}if((Ja(j,H[a+16>>2],2,k)|0)!=2){Ba(k,1,2472,0);j=0;break a}Da(H[a+16>>2],t+36|0,2);if(K[t+36>>2]<=1){Ba(k,1,6048,0);j=0;break a}o:{if(H[t+40>>2]!=32896){break o}if(Qa(j)|qa){break o}H[a+8>>2]=64;break l}m=H[a+8>>2];p:{if(!(m&16)){l=H[t+36>>2];break p}l=H[t+36>>2];n=H[a+24>>2];if(!n){break p}p=l+2|0;if(p>>>0>n>>>0){Ba(k,1,8370,0);j=0;break a}H[a+24>>2]=n-p}p=l-2|0;H[t+36>>2]=p;l=24912;A=H[t+40>>2];while(1){n=l;o=H[l>>2];if(o){l=l+12|0;if((o|0)!=(A|0)){continue}}break}if(!(m&H[n+4>>2])){Ba(k,1,5397,0);j=0;break a}q:{if(K[a+20>>2]>=p>>>0){l=H[a+16>>2];break q}l=Qa(j);m=qa;if((m|0)<0){l=1}else{l=l>>>0

>>0&(m|0)<=0}if(l){Ba(k,1,5797,0);j=0;break a}l=Ha(H[a+16>>2],H[t+36>>2]);if(!l){Ca(H[a+16>>2]);H[a+16>>2]=0;H[a+20>>2]=0;Ba(k,1,4973,0);j=0;break a}H[a+16>>2]=l;p=H[t+36>>2];H[a+20>>2]=p}l=Ja(j,l,p,k);if((l|0)!=H[t+36>>2]){Ba(k,1,2472,0);j=0;break a}n=H[n+8>>2];if(!n){Ba(k,1,11725,0);j=0;break a}if(!(ra[n|0](a,H[a+16>>2],l,k)|0)){H[t+32>>2]=H[t+40>>2];Ba(k,1,13959,t+32|0);j=0;break a}q=H[j+56>>2];m=H[t+36>>2];A=H[a+224>>2];p=H[A+40>>2];x=H[a+228>>2];r=N(x,40);l=p+r|0;z=H[l+20>>2];E=z+1|0;n=H[l+28>>2];r:{if(E>>>0<=n>>>0){l=H[l+24>>2];break r}X=O(O(n>>>0)+O(100));if(X=O(0)){n=~~X>>>0}else{n=0}H[l+28>>2]=n;l=Ha(H[l+24>>2],N(n,24));p=H[A+40>>2];n=r+p|0;if(!l){break k}H[n+24>>2]=l;z=H[n+20>>2];E=z+1|0}l=N(z,24)+l|0;H[l+16>>2]=m+4;n=(q-m|0)-4|0;H[l+8>>2]=n;H[l+12>>2]=n>>31;G[l>>1]=o;l=p+r|0;H[l+20>>2]=E;s:{if((o|0)!=65424){break s}p=H[l+16>>2];t:{if(!p){break t}o=H[l+12>>2];if(o>>>0>=K[l+4>>2]){break t}l=p+N(o,24)|0;H[l>>2]=n;H[l+4>>2]=0}l=(H[j+56>>2]-H[t+36>>2]|0)-4|0;n=H[a+48>>2];p=H[a+52>>2];if((p|0)>0){m=1}else{m=l>>>0<=n>>>0&(p|0)>=0}if(m){break s}H[a+48>>2]=l;H[a+52>>2]=0}if(I[a+92|0]&4){if((rb(j,H[a+24>>2],k)|0)!=H[a+24>>2]|qa){Ba(k,1,2472,0);j=0;break a}H[t+40>>2]=65427;break l}if((Ja(j,H[a+16>>2],2,k)|0)!=2){Ba(k,1,2472,0);j=0;break a}Da(H[a+16>>2],t+40|0,2);if(H[t+40>>2]!=65427){continue}break}}if(!(!(Qa(j)|qa)&H[a+8>>2]==64)){l=I[a+92|0];if(!(l&4)){l=N(H[a+228>>2],5644);n=H[a+180>>2];u:{v:{if(H[a+56>>2]){q=Qa(j);break v}q=H[a+24>>2];if(q>>>0<2){break u}}q=q-2|0;H[a+24>>2]=q}n=l+n|0;if(!q){break j}l=Qa(j);p=qa;if((p|0)<0){l=1}else{l=l>>>0>>0&(p|0)<=0}if(l){if(H[a+208>>2]){Ba(k,1,5842,0);j=0;break a}Ba(k,2,5842,0)}l=H[a+24>>2];if(l>>>0>=4294967294){Ba(k,1,1480,0);j=0;break a}p=H[n+5596>>2];w:{if(p){o=H[n+5600>>2];if(o>>>0>-3-l>>>0){Ba(k,1,1211,0);j=0;break a}l=Ha(p,(l+o|0)+2|0);if(l){H[n+5596>>2]=l;break j}Ca(H[n+5596>>2]);H[n+5596>>2]=0;break w}l=Fa(l+2|0);H[n+5596>>2]=l;if(l){break j}}Ba(k,1,6176,0);j=0;break a}H[a+8>>2]=8;F[a+92|0]=l&250;break i}l=H[t+40>>2];break g}Ca(H[n+24>>2]);a=H[A+40>>2]+N(x,40)|0;H[a+28>>2]=0;H[a+20>>2]=0;H[a+24>>2]=0;Ba(k,1,3863,0);j=0;break a}m=H[j+56>>2];p=m-2|0;A=H[j+60>>2];z=A-(m>>>0<2)|0;r=H[a+224>>2];J=H[r+40>>2];M=H[a+228>>2];x=N(M,40);l=J+x|0;o=H[l+16>>2]+N(H[l+12>>2],24)|0;H[o+8>>2]=p;H[o+12>>2]=z;z=m;m=H[a+24>>2];z=z+m|0;H[o+16>>2]=z;H[o+20>>2]=m>>>0>z>>>0?A+1|0:A;m=H[a+24>>2];z=H[l+20>>2];E=z+1|0;o=H[l+28>>2];x:{if(E>>>0<=o>>>0){l=H[l+24>>2];break x}X=O(O(o>>>0)+O(100));if(X=O(0)){o=~~X>>>0}else{o=0}H[l+28>>2]=o;l=Ha(H[l+24>>2],N(o,24));J=H[r+40>>2];o=x+J|0;if(!l){break f}H[o+24>>2]=l;z=H[o+20>>2];E=z+1|0}l=N(z,24)+l|0;H[l+16>>2]=m+2;H[l+8>>2]=p;H[l+12>>2]=p>>31;G[l>>1]=65427;H[(x+J|0)+20>>2]=E;y:{if(q){q=Ja(j,H[n+5596>>2]+H[n+5600>>2]|0,H[a+24>>2],k);l=8;if((q|0)==H[a+24>>2]){break y}l=64;if((q|0)!=-1){break y}Ba(k,1,2472,0);j=0;break a}q=0;l=H[a+24>>2]?64:8}H[a+8>>2]=l;H[n+5600>>2]=H[n+5600>>2]+q;z:{if(F[a+92|0]&1){break z}l=H[a+44>>2];if(H[a+76>>2]|((l|0)<0|(l|0)!=H[a+228>>2])){break z}if(!Ab(j)){break z}n=H[a+228>>2];p=H[a+180>>2]+N(n,5644)|0;l=H[p+5592>>2];n=H[H[a+224>>2]+40>>2]+N(n,40)|0;if((l|0)!=H[n+4>>2]){break z}p=H[p+5588>>2]+1|0;if(l>>>0<=p>>>0){break z}A:{l=H[n+16>>2]+N(p,24)|0;n=H[l>>2];l=H[l+4>>2];if((n|0)==H[j+56>>2]&(l|0)==H[j+60>>2]){break A}if(bb(j,n,l,k)){break A}Ba(k,1,5440,0);j=0;break a}if((Ja(j,H[a+16>>2],2,k)|0)!=2){Ba(k,1,2472,0);j=0;break a}Da(H[a+16>>2],t+40|0,2);if(H[t+40>>2]==65424){break h}Ba(k,1,4073,0);j=0;break a}l=I[a+92|0];if((l&9)!=1){break i}F[a+92|0]=l|8;p=H[a+228>>2];if(H[(H[a+180>>2]+N(p,5644)|0)+5592>>2]==1){break i}if(!Ab(j)){break i}l=H[j+60>>2];o=l;m=H[j+56>>2];if((l&m)==-1){break i}B:{while(1){l=1;n=t+70|0;if((Ja(j,n,2,k)|0)!=2){break B}Da(n,t- -64|0,2);if(H[t+64>>2]!=65424){break B}q=2472;if((Ja(j,n,2,k)|0)!=2){break c}Da(n,t+60|0,2);if(H[t+60>>2]!=10){q=6048;break c}H[t+60>>2]=8;n=Ja(j,t+70|0,8,k);if((n|0)!=H[t+60>>2]){break c}if((n|0)!=8){q=4047;break c}Da(t+70|0,t+56|0,2);Da(U,t+52|0,4);Da(R,t+48|0,1);Da(D,t+44|0,1);if((p|0)!=H[t+56>>2]){n=H[t+52>>2];if(n>>>0<14){break B}n=n-12|0;H[t+52>>2]=n;n=rb(j,n,k);if(!qa&H[t+52>>2]==(n|0)){continue}break B}break}l=H[t+48>>2]!=H[t+44>>2]}if(!nc(j,m,o,k)){break b}if(l){break i}F[a+92|0]=I[a+92|0]&238|16;C:{if(!v){break C}p=H[a+180>>2];q=0;l=0;if((v|0)!=1){while(1){n=p+N(q,5644)|0;o=H[n+5592>>2];if(o){H[n+5592>>2]=o+1}o=H[n+11236>>2];if(o){H[n+11236>>2]=o+1}q=q+2|0;l=l+2|0;if((Q|0)!=(l|0)){continue}break}if(!V){break C}}l=p+N(q,5644)|0;n=H[l+5592>>2];if(!n){break C}H[l+5592>>2]=n+1}Ba(k,2,9035,0)}if(F[a+92|0]&1){break h}if((Ja(j,H[a+16>>2],2,k)|0)!=2){if(!(!v|(v|0)!=(H[a+228>>2]+1|0))){j=H[a+180>>2];l=0;while(1){n=j+N(l,5644)|0;if(!(H[n+5588>>2]|H[n+5592>>2])){break e}l=l+1|0;if((v|0)!=(l|0)){continue}break}}Ba(k,1,2472,0);j=0;break a}Da(H[a+16>>2],t+40|0,2)}l=H[t+40>>2];if(F[a+92|0]&1){break g}if((l|0)!=65497){continue}}break}if(H[a+8>>2]==256|(l|0)!=65497){break d}H[a+8>>2]=256;H[a+228>>2]=0;break d}Ca(H[o+24>>2]);a=H[r+40>>2]+N(M,40)|0;H[a+28>>2]=0;H[a+20>>2]=0;H[a+24>>2]=0;Ba(k,1,3863,0);j=0;break a}H[t+16>>2]=l;Ba(k,4,11004,t+16|0);H[a+228>>2]=l;H[t+40>>2]=65497;H[a+8>>2]=256}l=H[a+228>>2];j=H[a+180>>2];D:{E:{if(F[a+92|0]&1){break E}F:{G:{if(l>>>0>=v>>>0){break G}q=j+N(l,5644)|0;while(1){if(H[q+5596>>2]){break G}l=l+1|0;H[a+228>>2]=l;q=q+5644|0;if((l|0)!=(v|0)){continue}break}break F}if((l|0)!=(v|0)){break E}}H[i>>2]=0;break D}H:{I:{n=j+N(l,5644)|0;if(H[n+5172>>2]){a=6837}else{if(!(I[n+5640|0]&2)){break H}p=H[n+5160>>2];J:{if(!p){q=0;break J}o=p&3;m=H[n+5164>>2];j=0;q=0;l=0;if(p>>>0>=4){A=p&-4;p=0;while(1){v=m+(l<<3)|0;q=H[v+28>>2]+(H[v+20>>2]+(H[v+12>>2]+(H[v+4>>2]+q|0)|0)|0)|0;l=l+4|0;p=p+4|0;if((A|0)!=(p|0)){continue}break}if(!o){break J}}while(1){q=H[(m+(l<<3)|0)+4>>2]+q|0;l=l+1|0;j=j+1|0;if((o|0)!=(j|0)){continue}break}}j=Fa(q);H[n+5172>>2]=j;if(j){break I}a=4009}Ba(k,1,a,0);Ba(k,1,8059,0);j=0;break a}H[n+5180>>2]=q;q=H[n+5164>>2];j=H[n+5160>>2];if(j){p=0;l=0;while(1){v=l<<3;o=v+q|0;m=H[o>>2];if(m){j=H[o+4>>2];if(j){B(H[n+5172>>2]+p|0,m,j)}j=v+H[n+5164>>2]|0;o=H[j+4>>2];Ca(H[j>>2]);q=H[n+5164>>2];j=v+q|0;H[j>>2]=0;H[j+4>>2]=0;p=o+p|0;j=H[n+5160>>2]}l=l+1|0;if(l>>>0>>0){continue}break}}H[n+5160>>2]=0;Ca(q);H[n+5164>>2]=0;H[n+5168>>2]=H[n+5172>>2];H[n+5176>>2]=H[n+5180>>2]}l=H[a+232>>2];Y=H[l+28>>2];n=H[a+228>>2];E=H[(H[Y+76>>2]+N(n,5644)|0)+5584>>2];j=H[l+24>>2];Z=H[j+24>>2];v=H[Y+24>>2];p=(n>>>0)/(v>>>0)|0;V=H[H[l+20>>2]>>2];o=H[Y+12>>2];l=H[Y+4>>2]+N(o,n-N(p,v)|0)|0;n=H[j>>2];n=l>>>0>n>>>0?l:n;H[V>>2]=n;v=l+o|0;l=l>>>0>v>>>0?-1:v;v=H[j+8>>2];l=l>>>0>>0?l:v;H[V+8>>2]=l;K:{L:{if(!((l|0)>(n|0)&(n|0)>=0)){Ba(k,1,6682,0);break L}q=H[V+20>>2];l=p;p=H[Y+16>>2];l=H[Y+8>>2]+N(l,p)|0;n=H[j+4>>2];n=l>>>0>n>>>0?l:n;H[V+4>>2]=n;p=l+p|0;l=l>>>0>p>>>0?-1:p;j=H[j+12>>2];j=j>>>0>l>>>0?l:j;H[V+12>>2]=j;if(!((j|0)>(n|0)&(n|0)>=0)){Ba(k,1,6644,0);break L}M:{if(H[E+4>>2]){if(H[V+16>>2]){break M}j=1;break K}Ba(k,1,5358,0);break L}N:{O:{while(1){H[Z+36>>2]=0;j=H[Z>>2];n=j>>31;o=j-1|0;l=H[V>>2];p=o+l|0;m=n-!j|0;v=m+(l>>31)|0;ta=q,ua=ue(p,l>>>0>p>>>0?v+1|0:v,j,n),H[ta>>2]=ua;l=H[Z+4>>2];p=l>>31;v=l-1|0;A=H[V+4>>2];r=v+A|0;x=p-!l|0;z=x+(A>>31)|0;ta=q,ua=ue(r,r>>>0>>0?z+1|0:z,l,p),H[ta+4>>2]=ua;z=o;o=H[V+8>>2];A=z+o|0;m=(o>>31)+m|0;ta=q,ua=ue(A,o>>>0>A>>>0?m+1|0:m,j,n),H[ta+8>>2]=ua;j=H[V+12>>2];H[q+16>>2]=fa;n=x+(j>>31)|0;j=j+v|0;n=j>>>0>>0?n+1|0:n;ta=q,ua=ue(j,n,l,p),H[ta+12>>2]=ua;j=H[E+4>>2];H[q+20>>2]=j;l=H[Y+80>>2];H[q+24>>2]=j>>>0>>0?1:j-l|0;Ca(H[q+52>>2]);H[q+68>>2]=0;H[q+60>>2]=0;H[q+64>>2]=0;H[q+52>>2]=0;H[q+56>>2]=0;j=N(j,152);l=H[q+28>>2];P:{if(!l){l=Fa(j);H[q+28>>2]=l;if(!l){break L}H[q+32>>2]=j;if(!j){break P}y(l,0,j);break P}if(j>>>0<=K[q+32>>2]){break P}l=Ha(l,j);if(!l){Ba(k,1,3090,0);Ca(H[q+28>>2]);H[q+28>>2]=0;H[q+32>>2]=0;break L}H[q+28>>2]=l;n=H[q+32>>2];p=j-n|0;if(p){y(l+n|0,0,p)}H[q+32>>2]=j}j=H[q+20>>2];if(j){ha=E+944|0;ia=E+812|0;da=E+28|0;z=H[q+28>>2];_=0;while(1){n=j-1|0;l=n&31;if((n&63)>>>0>=32){l=-1<>>32-l}p=p^-1;m=H[q>>2];o=p+m|0;v=l^-1;l=v+(m>>31)|0;m=m>>>0>o>>>0?l+1|0:l;l=n&31;if((n&63)>>>0>=32){r=m>>l}else{r=((1<>>l}H[z>>2]=r;o=H[q+4>>2];l=o+p|0;m=(o>>31)+v|0;m=l>>>0>>0?m+1|0:m;o=n&31;if((n&63)>>>0>=32){x=m>>o}else{x=((1<>>o}H[z+4>>2]=x;o=H[q+8>>2];l=o+p|0;m=(o>>31)+v|0;m=l>>>0>>0?m+1|0:m;o=n&31;if((n&63)>>>0>=32){m=m>>o}else{m=((1<>>o}H[z+8>>2]=m;o=H[q+12>>2];l=o+p|0;A=(o>>31)+v|0;A=l>>>0>>0?A+1|0:A;o=n&31;if((n&63)>>>0>=32){A=A>>o}else{A=((1<>>o}H[z+12>>2]=A;Q=m>>31;D=_<<2;J=H[D+ia>>2];l=J&31;if((J&63)>>>0>=32){l=1<>>32-l}R=o;o=R+m|0;U=o-1|0;l=l+Q|0;o=(o>>>0>>0?l+1|0:l)-!o|0;l=J&31;if((J&63)>>>0>=32){l=o>>l}else{l=((1<>>l}R=l<>31;Q=H[D+ha>>2];l=Q&31;if((Q&63)>>>0>=32){l=-1<>>32-l}D=o^-1;o=D+A|0;l=(l^-1)+U|0;D=o>>>0>>0?l+1|0:l;l=Q&31;if((Q&63)>>>0>=32){l=D>>l}else{l=((1<>>l}l=l<>Q:0;H[z+20>>2]=o;aa=r&-1<>J:0;H[z+16>>2]=l;re(l,0,o);if(!(!l|!qa)){break O}ba=N(l,o);if(ba>>>0>=107374183){break O}D=N(ba,40);if(_){Q=Q-1|0;J=J-1|0;l=$>>31;m=l;o=l+1|0;l=$+1|0;$=((l?m:o)&1)<<31|l>>>1;l=aa>>31;m=l;o=l+1|0;l=aa+1|0;aa=((l?m:o)&1)<<31|l>>>1;l=3}else{l=1}H[z+24>>2]=l;x=z+28|0;o=j;l=j&31;if((j&63)>>>0>=32){l=1<>>32-l}ga=j;A=l;j=H[E+12>>2];R=j>>>0>>0?j:Q;j=R&31;if((R&63)>>>0>=32){l=-1<>>32-j}ja=m^-1;ka=l^-1;j=H[E+8>>2];U=j>>>0>>0?j:J;j=U&31;if((U&63)>>>0>=32){l=-1<>>32-j}la=m^-1;ma=l^-1;ea=0;while(1){Q:{if(!_){l=H[q+4>>2];j=l+p|0;m=(l>>31)+v|0;m=j>>>0>>0?m+1|0:m;l=n&31;if((n&63)>>>0>=32){S=m>>l}else{S=((1<>>l}l=H[q>>2];j=l+p|0;m=(l>>31)+v|0;m=j>>>0>>0?m+1|0:m;l=n&31;if((n&63)>>>0>=32){C=m>>l}else{C=((1<>>l}j=0;m=p;M=m;l=v;T=l;r=n;break Q}j=ea+1|0;r=j>>>1|0;m=n&31;if((n&63)>>>0>=32){l=r<>>32-m;m=r<>2];m=M+r|0;l=(l^-1)+A|0;T=s>>>0>M>>>0?l+1|0:l;l=T+(r>>31)|0;r=m>>>0>>0?l+1|0:l;l=o&31;if((o&63)>>>0>=32){S=r>>l}else{S=((1<>>l}r=j&1;m=n&31;if((n&63)>>>0>=32){l=r<>>32-m;m=r<>2];r=m+s|0;l=(l^-1)+A|0;l=m>>>0>>0?l+1|0:l;C=l+(s>>31)|0;C=r>>>0>>0?C+1|0:C;s=o&31;if((o&63)>>>0>=32){C=C>>s}else{C=((1<>>s}r=o}s=r;P=H[q+8>>2];W=P>>31;r=H[q+12>>2];H[x+4>>2]=S;H[x>>2]=C;H[x+16>>2]=j;S=(r>>31)+T|0;C=r;r=r+M|0;S=C>>>0>r>>>0?S+1|0:S;M=s&31;if((s&63)>>>0>=32){r=S>>M}else{r=((1<>>M}H[x+12>>2]=r;l=l+W|0;C=l+1|0;r=l;l=m+P|0;r=l>>>0

>>0?C:r;m=s&31;if((s&63)>>>0>=32){l=r>>m}else{l=((1<>>m}H[x+8>>2]=l;ca=1;l=H[da>>2];j=(H[Z+24>>2]+(!H[E+20>>2]|!j?0:(j|0)==3?2:1)|0)-l|0;R:{if((j|0)>=1024){ca=898846567431158e293;if(j>>>0<2047){j=j-1023|0;break R}ca=Infinity;j=(j>>>0>=3069?3069:j)-2046|0;break R}if((j|0)>-1023){break R}ca=2004168360008973e-307;if(j>>>0>4294965304){j=j+969|0;break R}ca=0;j=(j>>>0<=4294964336?-2960:j)+1938|0}oa=+H[da+4>>2]*.00048828125+1;u(0,0);u(1,j+1023<<20);ta=x,va=O(oa*(ca*+w())),L[ta+32>>2]=va;H[x+28>>2]=(l+H[E+804>>2]|0)-1;j=H[x+20>>2];S:{T:{if(!(j|!ba)){j=Fa(D);H[x+20>>2]=j;if(!j){Ba(k,1,2854,0);break L}if(D){y(j,0,D)}H[x+24>>2]=D;break T}if(D>>>0>K[x+24>>2]){j=Ha(j,D);if(!j){Ba(k,1,2854,0);Ca(H[x+20>>2]);H[x+20>>2]=0;H[x+24>>2]=0;break L}H[x+20>>2]=j;l=H[x+24>>2];m=D-l|0;if(m){y(j+l|0,0,m)}H[x+24>>2]=D}if(!ba){break S}}j=H[x+20>>2];M=0;while(1){m=H[z+16>>2];l=(M>>>0)/(m>>>0)|0;m=M-N(l,m)|0;r=(m<>2];S=(r|0)>(s|0)?r:s;H[j>>2]=S;r=(l<>2];C=(r|0)>(s|0)?r:s;H[j+4>>2]=C;m=(m+1<>2];m=(m|0)<(r|0)?m:r;H[j+8>>2]=m;l=(l+1<>2];r=(l|0)<(r|0)?l:r;H[j+12>>2]=r;l=(m>>31)+ma|0;T=l+1|0;s=l;l=m+la|0;s=m>>>0>l>>>0?T:s;S=S>>U;m=U&31;if((U&63)>>>0>=32){l=s>>m}else{l=((1<>>m}s=l-S<>U;H[j+16>>2]=s;l=(r>>31)+ka|0;T=l+1|0;m=l;l=r+ja|0;r=l>>>0>>0?T:m;C=C>>R;m=R&31;if((R&63)>>>0>=32){l=r>>m}else{l=((1<>>m}l=l-C<>R;H[j+20>>2]=l;r=N(l,s);re(r,0,68);if(qa){Ba(k,1,2935,0);break L}l=N(r,68);m=H[j+24>>2];U:{V:{if(!(m|!r)){m=Fa(l);H[j+24>>2]=m;if(!m){break L}if(!l){break V}y(m,0,l);break V}if(l>>>0<=K[j+28>>2]){break U}m=Ha(m,l);if(!m){Ca(H[j+24>>2]);H[j+24>>2]=0;H[j+28>>2]=0;Ba(k,1,2549,0);break L}H[j+24>>2]=m;s=H[j+28>>2];T=l-s|0;if(!T){break V}y(m+s|0,0,T)}H[j+28>>2]=l}l=H[j+20>>2];m=H[j+16>>2];s=H[j+32>>2];W:{if(!s){l=gc(m,l,k);break W}l=ec(s,m,l,k)}H[j+32>>2]=l;l=H[j+20>>2];m=H[j+16>>2];s=H[j+36>>2];X:{if(!s){l=gc(m,l,k);break X}l=ec(s,m,l,k)}H[j+36>>2]=l;if(r){l=0;while(1){W=H[j+16>>2];T=(l>>>0)/(W>>>0)|0;m=H[j+24>>2]+N(l,68)|0;P=H[m>>2];Y:{if(P){pa=H[m+56>>2];sa=H[m+4>>2];s=H[m+48>>2];Ca(H[m+60>>2]);H[m+48>>2]=0;H[m+52>>2]=0;H[m+64>>2]=0;H[m+56>>2]=0;H[m+60>>2]=0;H[m+40>>2]=0;H[m+44>>2]=0;H[m+32>>2]=0;H[m+36>>2]=0;H[m+24>>2]=0;H[m+28>>2]=0;H[m+16>>2]=0;H[m+20>>2]=0;H[m+8>>2]=0;H[m+12>>2]=0;H[m>>2]=P;H[m+48>>2]=s;Z:{if(!s){break Z}s=N(s,24);if(!s){break Z}y(P,0,s)}H[m+56>>2]=pa;H[m+4>>2]=sa;break Y}s=Ea(10,24);H[m>>2]=s;if(!s){break L}H[m+48>>2]=10}s=(l-N(T,W)|0)+S|0;P=s<>2];H[m+8>>2]=(P|0)>(W|0)?P:W;T=C+T|0;P=T<>2];H[m+12>>2]=(P|0)>(W|0)?P:W;s=s+1<>2];H[m+16>>2]=(s|0)<(P|0)?s:P;P=m;m=T+1<>2];H[P+20>>2]=(m|0)<(s|0)?m:s;l=l+1|0;if((r|0)!=(l|0)){continue}break}}j=j+40|0;M=M+1|0;if((M|0)!=(ba|0)){continue}break}}da=da+8|0;x=x+36|0;ea=ea+1|0;if(ea>>>0>2]){continue}break}z=z+152|0;j=n;_=_+1|0;if(_>>>0>2]){continue}break}}Z=Z+52|0;q=q+76|0;E=E+1080|0;fa=fa+1|0;if(fa>>>0>2]){continue}break}j=1;break K}Ba(k,1,2982,0);break L}Ba(k,1,2373,0)}j=0}if(!j){Ba(k,1,3668,0);j=0;break a}j=H[a+228>>2];H[t+4>>2]=N(H[a+128>>2],H[a+132>>2]);H[t>>2]=j+1;Ba(k,4,11825,t);H[b>>2]=H[a+228>>2];H[i>>2]=1;if(c){b=Rb(H[a+232>>2],0);H[c>>2]=b;j=0;if((b|0)==-1){break a}}b=H[H[H[a+232>>2]+20>>2]>>2];H[d>>2]=H[b>>2];H[e>>2]=H[b+4>>2];H[f>>2]=H[b+8>>2];H[g>>2]=H[b+12>>2];H[h>>2]=H[b+16>>2];H[a+8>>2]=H[a+8>>2]|128}j=1;break a}Ba(k,1,q,0)}Ba(k,1,3702,0);j=0}a=j;na=t+80|0;return a|0}function Vb(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,L=0,M=0,O=0,P=0,Q=0,R=0,S=0,T=0;a:{b:{c:{d:{e:{f:{g:{h:{i:{j:{switch(H[a+84>>2]){case 0:k:{c=H[a+52>>2];b=H[a+196>>2];if(c>>>0>>0){q=H[a+64>>2];if(q>>>0>>0){break k}}Ba(H[a+236>>2],1,8491,0);break b}if(!H[a+44>>2]){k=H[a+36>>2];b=0;break i}H[a+44>>2]=0;i=H[a+68>>2];b=1;break i;case 1:l:{c=H[a+52>>2];b=H[a+196>>2];if(c>>>0>>0){q=H[a+64>>2];if(q>>>0>>0){break l}}Ba(H[a+236>>2],1,8536,0);break b}if(!H[a+44>>2]){e=H[a+36>>2];b=0;break e}H[a+44>>2]=0;i=H[a+48>>2];b=1;break e;case 2:m:{A=H[a+52>>2];x=H[a+196>>2];if(A>>>0>>0){r=H[a+64>>2];if(r>>>0>>0){break m}}Ba(H[a+236>>2],1,8671,0);break b}if(!H[a+44>>2]){y=H[a+40>>2];break f}H[a+228>>2]=0;H[a+232>>2]=0;H[a+44>>2]=0;j=H[a+200>>2];while(1){O=j+(u<<4)|0;l=H[O+8>>2];if(l){q=H[O+12>>2];b=0;while(1){g=l+(b^-1)|0;d=q+(b<<4)|0;s=g+H[d>>2]|0;n:{if(s>>>0>31){break n}c=H[O>>2];if(c>>>0>-1>>>s>>>0){break n}c=c<>>0>k>>>0?k:c:c;H[a+228>>2]=k}g=g+H[d+4>>2]|0;o:{if(g>>>0>31){break o}c=H[O+4>>2];if(c>>>0>-1>>>g>>>0){break o}c=c<>>0>i>>>0?i:c:c;H[a+232>>2]=i}b=b+1|0;if((l|0)!=(b|0)){continue}break}}u=u+1|0;if((x|0)!=(u|0)){continue}break};if(!k|!i){break d}if(!I[a|0]){H[a+108>>2]=H[a+208>>2];H[a+100>>2]=H[a+204>>2];H[a+112>>2]=H[a+216>>2];H[a+104>>2]=H[a+212>>2]}o=H[a+48>>2];b=1;break f;case 3:p:{A=H[a+52>>2];l=H[a+196>>2];if(A>>>0>>0){P=H[a+64>>2];if(P>>>0>>0){break p}}Ba(H[a+236>>2],1,8626,0);break b}if(!H[a+44>>2]){B=H[a+200>>2];e=H[a+28>>2];y=B+(e<<4)|0;E=H[a+40>>2];break g}H[a+228>>2]=0;H[a+232>>2]=0;H[a+44>>2]=0;B=H[a+200>>2];while(1){x=(p<<4)+B|0;s=H[x+8>>2];if(s){q=H[x+12>>2];b=0;while(1){g=s+(b^-1)|0;d=q+(b<<4)|0;j=g+H[d>>2]|0;q:{if(j>>>0>31){break q}c=H[x>>2];if(c>>>0>-1>>>j>>>0){break q}c=c<>>0>k>>>0?k:c:c;H[a+228>>2]=k}g=g+H[d+4>>2]|0;r:{if(g>>>0>31){break r}c=H[x+4>>2];if(c>>>0>-1>>>g>>>0){break r}c=c<>>0>i>>>0?i:c:c;H[a+232>>2]=i}b=b+1|0;if((s|0)!=(b|0)){continue}break}}p=p+1|0;if((l|0)!=(p|0)){continue}break};if(!k|!i){break d}s:{if(I[a|0]){p=H[a+108>>2];break s}p=H[a+208>>2];H[a+108>>2]=p;H[a+100>>2]=H[a+204>>2];H[a+112>>2]=H[a+216>>2];H[a+104>>2]=H[a+212>>2]}b=1;break g;case 4:break j;default:break d}}t:{p=H[a+52>>2];b=H[a+196>>2];if(p>>>0>>0){r=H[a+64>>2];if(r>>>0>>0){break t}}Ba(H[a+236>>2],1,8581,0);break d}if(!H[a+44>>2]){p=H[a+28>>2];o=H[a+200>>2]+(p<<4)|0;u=H[a+40>>2];b=0;break h}H[a+28>>2]=p;H[a+44>>2]=0;b=1;break h}u:while(1){v:{w:{if(!b){k=k+1|0;break w}H[a+40>>2]=i;if(K[a+56>>2]<=i>>>0){break b}e=H[a+48>>2];b=0;break v}b=1}x:while(1){y:{z:{A:{B:{if(!b){H[a+32>>2]=e;if(K[a+60>>2]<=e>>>0){break B}H[a+28>>2]=c;b=c;o=0;break y}H[a+36>>2]=k;if(K[a+76>>2]<=k>>>0){b=H[a+28>>2];o=1;break y}b=((N(H[a+16>>2],H[a+32>>2])+N(H[a+12>>2],H[a+40>>2])|0)+N(H[a+20>>2],H[a+28>>2])|0)+N(H[a+24>>2],k)|0;if(b>>>0>=K[a+8>>2]){break c}b=H[a+4>>2]+(b<<1)|0;if(J[b>>1]){break A}break a}i=H[a+40>>2]+1|0;break z}b=0;continue u}b=1;continue u}while(1){C:{D:{E:{if(!o){if(b>>>0>=q>>>0){break E}g=H[a+32>>2];d=H[a+200>>2]+(b<<4)|0;if(g>>>0>=K[d+8>>2]){break C}if(!I[a|0]){b=H[d+12>>2]+(g<<4)|0;H[a+76>>2]=N(H[b+12>>2],H[b+8>>2])}k=H[a+72>>2];b=1;continue x}b=b+1|0;H[a+28>>2]=b;break D}e=H[a+32>>2]+1|0;b=0;continue x}o=0;continue}o=1;continue}}}}F:while(1){G:{H:{if(!b){u=u+1|0;H[a+40>>2]=u;break H}if(p>>>0>=r>>>0){break b}H[a+228>>2]=0;H[a+232>>2]=0;o=H[a+200>>2]+(p<<4)|0;s=H[o+8>>2];if(!s){break b}q=H[o+12>>2];k=0;e=0;b=0;while(1){g=s+(b^-1)|0;d=q+(b<<4)|0;j=g+H[d>>2]|0;I:{if(j>>>0>31){break I}c=H[o>>2];if(c>>>0>-1>>>j>>>0){break I}c=c<>>0>e>>>0?e:c:c;H[a+228>>2]=e}g=g+H[d+4>>2]|0;J:{if(g>>>0>31){break J}c=H[o+4>>2];if(c>>>0>-1>>>g>>>0){break J}c=c<>>0>k>>>0?k:c:c;H[a+232>>2]=k}b=b+1|0;if((s|0)!=(b|0)){continue}break}if(!e|!k){break d}K:{if(I[a|0]){k=H[a+108>>2];break K}k=H[a+208>>2];H[a+108>>2]=k;H[a+100>>2]=H[a+204>>2];H[a+112>>2]=H[a+216>>2];H[a+104>>2]=H[a+212>>2]}b=0;break G}b=1}L:while(1){M:{N:{O:{P:{if(!b){H[a+224>>2]=k;if(K[a+112>>2]<=k>>>0){break P}B=H[a+100>>2];b=0;break M}if(K[a+56>>2]<=u>>>0){i=H[a+32>>2];b=1;break M}b=((N(H[a+16>>2],H[a+32>>2])+N(H[a+12>>2],u)|0)+N(H[a+20>>2],p)|0)+N(H[a+24>>2],H[a+36>>2])|0;if(b>>>0>=K[a+8>>2]){break c}b=H[a+4>>2]+(b<<1)|0;if(J[b>>1]){break O}break a}p=p+1|0;H[a+28>>2]=p;break N}b=0;continue F}b=1;continue F}while(1){Q:{R:{S:{T:{if(!b){H[a+220>>2]=B;if(K[a+104>>2]<=B>>>0){break S}i=H[a+48>>2];break T}i=i+1|0}H[a+32>>2]=i;b=H[a+60>>2];d=H[o+8>>2];if((b>>>0>>0?b:d)>>>0>i>>>0){g=H[o>>2];c=g;n=d+(i^-1)|0;m=n;d=m&31;if((m&63)>>>0>=32){b=c<>>32-d;v=g<>>0>=32){b=b>>>d|0}else{b=((1<>>d}if((q|0)!=(b|0)){break Q}b=m&31;if((m&63)>>>0>=32){b=-1>>>b|0}else{b=(1<>>b}c=H[o+4>>2];if((b&c)!=(c|0)){break Q}d=m&31;if((m&63)>>>0>=32){b=c<>>32-d;w=c<>2];j=F+d|0;O=ve(j,d>>>0>j>>>0?h+1|0:h,w,b);b=h;L=H[a+208>>2];d=F+L|0;b=L>>>0>d>>>0?b+1|0:b;s=ve(d,b,w,C);A=v-1|0;j=H[a+212>>2];l=A+j|0;d=f-!v|0;b=d;x=ve(l,l>>>0>>0?b+1|0:b,v,f);D=H[a+204>>2];j=A+D|0;b=D>>>0>j>>>0?b+1|0:b;j=ve(j,b,v,f);z=H[o+12>>2]+(i<<4)|0;M=H[z>>2];t=M+n|0;b=t&31;if((t&63)>>>0>=32){b=-1>>>b|0}else{b=(1<>>b}if((g|0)!=(b&g)){break Q}h=c;P=H[z+4>>2];n=P+n|0;e=n&31;if((n&63)>>>0>=32){b=c<>>32-e;e=c<>>0>=32){c=b>>>l|0}else{c=((1<>>l}if((h|0)!=(c|0)){break Q}l=H[a+224>>2];e=!!(we(l,e,b)|qa);b=n&31;if((n&63)>>>0>=32){h=-1<>>32-b;b=-1<>>0>=32){h=n<>>32-e|b<>2];if((t&63)>>>0>=32){b=g<>>32-n;e=g<>>0>=32){h=-1<>>32-b;b=-1<>>0>=32){h=j<>>32-t|b<>2];if(!n|(!H[z+12>>2]|(j|0)==(x|0))){break Q}if((s|0)==(O|0)){break Q}u=H[a+68>>2];H[a+40>>2]=u;b=d;c=c+A|0;b=c>>>0>>0?b+1|0:b;g=(ve(c,b,v,f)>>>M)-(j>>>M)|0;b=q;c=l+F|0;b=c>>>0>>0?b+1|0:b;S=a,T=N(n,(ve(c,b,w,C)>>>P)-(s>>>P)|0)+g|0,H[S+36>>2]=T;b=1;continue L}c=H[a+220>>2];b=H[a+228>>2];B=c+b-(c>>>0)%(b>>>0)|0;break R}c=H[a+224>>2];b=H[a+232>>2];k=c+b-(c>>>0)%(b>>>0)|0;b=0;continue L}b=0;continue}b=1;continue}}}}U:while(1){V:{W:{if(!b){E=E+1|0;H[a+40>>2]=E;break W}H[a+224>>2]=p;if(K[a+112>>2]<=p>>>0){break b}v=H[a+100>>2];b=0;break V}b=1}X:while(1){Y:{Z:{_:{$:{if(!b){H[a+220>>2]=v;if(K[a+104>>2]<=v>>>0){break $}H[a+28>>2]=A;e=A;b=0;break Y}if(K[a+56>>2]<=E>>>0){u=H[a+32>>2];b=1;break Y}b=((N(H[a+16>>2],H[a+32>>2])+N(H[a+12>>2],E)|0)+N(H[a+20>>2],e)|0)+N(H[a+24>>2],H[a+36>>2])|0;if(b>>>0>=K[a+8>>2]){break c}b=H[a+4>>2]+(b<<1)|0;if(J[b>>1]){break _}break a}c=H[a+224>>2];b=H[a+232>>2];p=c+b-(c>>>0)%(b>>>0)|0;break Z}b=0;continue U}b=1;continue U}while(1){aa:{ba:{ca:{da:{if(!b){if(e>>>0>=P>>>0){break ca}u=H[a+48>>2];H[a+32>>2]=u;y=(e<<4)+B|0;break da}u=u+1|0;H[a+32>>2]=u}b=H[a+60>>2];d=H[y+8>>2];if((b>>>0>>0?b:d)>>>0>u>>>0){g=H[y>>2];c=g;f=d+(u^-1)|0;i=f;d=f&31;if((f&63)>>>0>=32){b=c<>>32-d;k=g<>>0>=32){b=b>>>d|0}else{b=((1<>>d}if((q|0)!=(b|0)){break aa}b=i&31;if((i&63)>>>0>=32){b=-1>>>b|0}else{b=(1<>>b}c=H[y+4>>2];if((b&c)!=(c|0)){break aa}d=i&31;if((i&63)>>>0>=32){b=c<>>32-d;o=c<>2];j=F+d|0;O=ve(j,d>>>0>j>>>0?h+1|0:h,o,b);b=h;w=H[a+208>>2];d=w+F|0;b=w>>>0>d>>>0?b+1|0:b;s=ve(d,b,o,n);C=k-1|0;j=H[a+212>>2];l=C+j|0;d=t-!k|0;b=d;x=ve(l,l>>>0>>0?b+1|0:b,k,t);L=H[a+204>>2];j=C+L|0;b=L>>>0>j>>>0?b+1|0:b;j=ve(j,b,k,t);D=H[y+12>>2]+(u<<4)|0;z=H[D>>2];m=z+f|0;b=m&31;if((m&63)>>>0>=32){b=-1>>>b|0}else{b=(1<>>b}if((g|0)!=(b&g)){break aa}h=c;M=H[D+4>>2];f=M+f|0;r=f&31;if((f&63)>>>0>=32){b=c<>>32-r;r=c<>>0>=32){c=b>>>l|0}else{c=((1<>>l}if((h|0)!=(c|0)){break aa}l=H[a+224>>2];r=!!(we(l,r,b)|qa);b=f&31;if((f&63)>>>0>=32){h=-1<>>32-b;b=-1<>>0>=32){h=f<>>32-r|b<>2];if((m&63)>>>0>=32){b=g<>>32-f;f=g<>>0>=32){h=-1<>>32-b;b=-1<>>0>=32){h=f<>>32-m|b<>2];if(!f|(!H[D+12>>2]|(j|0)==(x|0))){break aa}if((s|0)==(O|0)){break aa}E=H[a+68>>2];H[a+40>>2]=E;b=d;c=c+C|0;b=c>>>0>>0?b+1|0:b;g=(ve(c,b,k,t)>>>z)-(j>>>z)|0;b=q;c=l+F|0;b=c>>>0>>0?b+1|0:b;S=a,T=N(f,(ve(c,b,o,n)>>>M)-(s>>>M)|0)+g|0,H[S+36>>2]=T;b=1;continue X}e=e+1|0;H[a+28>>2]=e;break ba}c=H[a+220>>2];b=H[a+228>>2];v=c+b-(c>>>0)%(b>>>0)|0;b=0;continue X}b=0;continue}b=1;continue}}}}ea:while(1){fa:{ga:{if(!b){y=y+1|0;H[a+40>>2]=y;break ga}H[a+32>>2]=o;if(K[a+60>>2]<=o>>>0){break b}E=H[a+108>>2];b=0;break fa}b=1}ha:while(1){ia:{ja:{ka:{la:{if(!b){H[a+224>>2]=E;if(K[a+112>>2]<=E>>>0){break la}B=H[a+100>>2];b=0;break ia}if(K[a+56>>2]<=y>>>0){p=H[a+28>>2];b=1;break ia}b=((N(H[a+16>>2],H[a+32>>2])+N(H[a+12>>2],y)|0)+N(H[a+20>>2],H[a+28>>2])|0)+N(H[a+24>>2],H[a+36>>2])|0;if(b>>>0>=K[a+8>>2]){break c}b=H[a+4>>2]+(b<<1)|0;if(J[b>>1]){break ka}break a}o=H[a+32>>2]+1|0;break ja}b=0;continue ea}b=1;continue ea}while(1){ma:{na:{oa:{pa:{if(!b){H[a+220>>2]=B;if(K[a+104>>2]<=B>>>0){break oa}H[a+28>>2]=A;p=A;break pa}p=p+1|0;H[a+28>>2]=p}if(p>>>0>>0){m=H[a+32>>2];e=H[a+200>>2]+(p<<4)|0;b=H[e+8>>2];if(m>>>0>=b>>>0){break ma}g=H[e>>2];c=g;f=b+(m^-1)|0;i=f;d=f&31;if((f&63)>>>0>=32){b=c<>>32-d;v=g<>>0>=32){b=b>>>d|0}else{b=((1<>>d}if((q|0)!=(b|0)){break ma}b=i&31;if((i&63)>>>0>=32){b=-1>>>b|0}else{b=(1<>>b}c=H[e+4>>2];if((b&c)!=(c|0)){break ma}d=i&31;if((i&63)>>>0>=32){b=c<>>32-d;w=c<>2];j=F+d|0;O=ve(j,d>>>0>j>>>0?h+1|0:h,w,b);b=h;L=H[a+208>>2];d=F+L|0;b=L>>>0>d>>>0?b+1|0:b;s=ve(d,b,w,n);C=v-1|0;j=H[a+212>>2];l=C+j|0;d=t-!v|0;b=d;x=ve(l,l>>>0>>0?b+1|0:b,v,t);D=H[a+204>>2];j=C+D|0;b=D>>>0>j>>>0?b+1|0:b;j=ve(j,b,v,t);z=H[e+12>>2]+(m<<4)|0;M=H[z>>2];m=M+f|0;b=m&31;if((m&63)>>>0>=32){b=-1>>>b|0}else{b=(1<>>b}if((g|0)!=(b&g)){break ma}h=c;P=H[z+4>>2];f=P+f|0;e=f&31;if((f&63)>>>0>=32){b=c<>>32-e;e=c<>>0>=32){c=b>>>l|0}else{c=((1<>>l}if((h|0)!=(c|0)){break ma}l=H[a+224>>2];e=!!(we(l,e,b)|qa);b=f&31;if((f&63)>>>0>=32){h=-1<>>32-b;b=-1<>>0>=32){h=f<>>32-e|b<>2];if((m&63)>>>0>=32){b=g<>>32-f;f=g<>>0>=32){h=-1<>>32-b;b=-1<>>0>=32){h=f<>>32-m|b<>2];if(!f|(!H[z+12>>2]|(j|0)==(x|0))){break ma}if((s|0)==(O|0)){break ma}y=H[a+68>>2];H[a+40>>2]=y;b=d;c=c+C|0;b=c>>>0>>0?b+1|0:b;g=(ve(c,b,v,t)>>>M)-(j>>>M)|0;b=q;c=l+F|0;b=c>>>0>>0?b+1|0:b;S=a,T=N(f,(ve(c,b,w,n)>>>P)-(s>>>P)|0)+g|0,H[S+36>>2]=T;b=1;continue ha}c=H[a+220>>2];b=H[a+228>>2];B=c+b-(c>>>0)%(b>>>0)|0;break na}c=H[a+224>>2];b=H[a+232>>2];E=c+b-(c>>>0)%(b>>>0)|0;b=0;continue ha}b=0;continue}b=1;continue}}}}qa:while(1){ra:{sa:{if(!b){e=e+1|0;break sa}H[a+32>>2]=i;if(K[a+60>>2]<=i>>>0){break b}k=H[a+68>>2];b=0;break ra}b=1}ta:while(1){ua:{va:{wa:{xa:{if(!b){H[a+40>>2]=k;if(K[a+56>>2]<=k>>>0){break xa}H[a+28>>2]=c;b=c;o=0;break ua}H[a+36>>2]=e;if(K[a+76>>2]<=e>>>0){b=H[a+28>>2];o=1;break ua}b=((N(H[a+16>>2],H[a+32>>2])+N(H[a+12>>2],H[a+40>>2])|0)+N(H[a+20>>2],H[a+28>>2])|0)+N(H[a+24>>2],e)|0;if(b>>>0>=K[a+8>>2]){break c}b=H[a+4>>2]+(b<<1)|0;if(J[b>>1]){break wa}break a}i=H[a+32>>2]+1|0;break va}b=0;continue qa}b=1;continue qa}while(1){ya:{za:{Aa:{if(!o){if(b>>>0>=q>>>0){break Aa}g=H[a+32>>2];d=H[a+200>>2]+(b<<4)|0;if(g>>>0>=K[d+8>>2]){break ya}if(!I[a|0]){b=H[d+12>>2]+(g<<4)|0;H[a+76>>2]=N(H[b+12>>2],H[b+8>>2])}e=H[a+72>>2];b=1;continue ta}b=b+1|0;H[a+28>>2]=b;break za}k=H[a+40>>2]+1|0;b=0;continue ta}o=0;continue}o=1;continue}}}}return 0}Ba(H[a+236>>2],1,1343,0)}return 0}G[b>>1]=1;return 1}function nd(a,b,c,d,e,f){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=O(0),t=0,u=0,v=0,w=O(0),x=0,z=0,A=0,C=O(0),D=O(0),E=O(0),G=0,J=0,K=0,L=0,M=0,Q=O(0),R=0,S=0,T=0;n=na-8320|0;na=n;H[n+64>>2]=0;j=2;g=H[a>>2];a:{b:{if((g|0)==176622093){break b}if((g|0)!=1375686655){if(!((g|0)!=201326592|H[a+4>>2]!=538988650)&H[a+8>>2]==176622093){break b}Y(1101);j=1;break a}j=0}g=Ea(1,96);k=0;c:{if(!g){break c}H[g+76>>2]=1;d:{e:{f:{switch(j|0){case 0:H[g+88>>2]=68;H[g+84>>2]=69;H[g+80>>2]=70;H[g+16>>2]=71;H[g+4>>2]=72;H[g+28>>2]=73;H[g+24>>2]=74;H[g+20>>2]=75;H[g>>2]=76;H[g+92>>2]=77;H[g+44>>2]=78;H[g+40>>2]=79;H[g+36>>2]=80;H[g+32>>2]=81;H[g+12>>2]=82;H[g+8>>2]=83;i=Jb();H[g+48>>2]=i;if(i){break e}break d;case 2:break f;default:break d}}H[g+88>>2]=84;H[g+84>>2]=85;H[g+80>>2]=86;H[g+16>>2]=87;H[g+4>>2]=88;H[g+92>>2]=89;H[g+44>>2]=90;H[g+40>>2]=91;H[g+36>>2]=92;H[g+32>>2]=93;H[g+28>>2]=94;H[g+24>>2]=95;H[g+20>>2]=96;H[g+12>>2]=97;H[g+8>>2]=98;H[g>>2]=99;i=Ea(1,136);g:{if(i){k=Jb();H[i>>2]=k;h:{if(!k){break h}F[i+124|0]=0;H[i+116>>2]=0;H[i+120>>2]=0;H[i+108>>2]=0;H[i+112>>2]=0;k=qb();H[i+4>>2]=k;if(!k){break h}k=qb();H[i+8>>2]=k;if(!k){break h}break g}Fc(i)}i=0}H[g+48>>2]=i;if(!i){break d}}H[g+72>>2]=1;H[g+64>>2]=1;H[g+60>>2]=0;H[g+52>>2]=0;H[g+56>>2]=0;H[g+68>>2]=1;k=g;break c}Ca(g);k=0}g=k;if(g){H[g+60>>2]=0;H[g+72>>2]=100}if(g){H[g+56>>2]=0;H[g+68>>2]=101}if(g){H[g+52>>2]=0;H[g+64>>2]=102}i=n+68|0;if(i){y(i,0,8248);H[i+8248>>2]=0;H[i+8200>>2]=-1;H[i+8204>>2]=-1}if(d){H[n+8316>>2]=H[n+8316>>2]|1}H[n+60>>2]=b;H[n+56>>2]=a;H[n+52>>2]=a;j=1;b=0;i=n+52|0;i:{if(!i){break i}a=Ea(1,72);if(a){j:{H[a+64>>2]=1048576;k=Fa(1048576);H[a+32>>2]=k;if(!k){Ca(a);a=0;break j}H[a+36>>2]=k;H[a+28>>2]=2;H[a+24>>2]=3;H[a+20>>2]=4;H[a+16>>2]=5;H[a+44>>2]=6;H[a+40>>2]=8;H[a+68>>2]=H[a+68>>2]|2}}else{a=0}if(!a){break i}if(a){H[a+4>>2]=0;H[a>>2]=i}if(a){H[a+8>>2]=H[i+8>>2];H[a+12>>2]=0}if(!(!a|!(I[a+68|0]&2))){H[a+16>>2]=64}if(a){H[a+24>>2]=66}if(a){H[a+28>>2]=67}b=a}a=n+68|0;if(!g|!a){a=0}else{k:{if(!H[g+76>>2]){Ba(g+52|0,1,9902,0);a=0;break k}ra[H[g+24>>2]](H[g+48>>2],a);a=1}}if(!a){Y(1153);fb(b);hb(g);break a}if(!b|!g){a=0}else{l:{if(!H[g+76>>2]){Ba(g+52|0,1,9983,0);a=0;break l}a=ra[H[g>>2]](b,H[g+48>>2],n- -64|0,g+52|0)|0}}if(!a){Y(1181);fb(b);hb(g);Ua(H[n+64>>2]);break a}m:{if(!f){break m}if(g){a=ra[H[g+40>>2]](H[g+48>>2],f,g+52|0)|0}else{a=0}if(a){break m}Y(1116);fb(b);hb(g);Ua(H[n+64>>2]);break a}a=H[n+64>>2];n:{if(!H[g+76>>2]|(!g|!b)){a=0}else{a=ra[H[g+4>>2]](H[g+48>>2],b,a,g+52|0)|0}if(a){if(!H[g+76>>2]|(!g|!b)){a=0}else{a=ra[H[g+16>>2]](H[g+48>>2],b,g+52|0)|0}if(a){break n}}Y(1316);hb(g);fb(b);Ua(H[n+64>>2]);break a}fb(b);hb(g);l=H[n+64>>2];a=H[l+28>>2];if(a){Ca(a);l=H[n+64>>2];H[l+28>>2]=0;H[l+32>>2]=0}v=H[l+16>>2];o:{p:{if(!c){if(!(!e|(v|0)!=4)){p=1;v=4;break o}q:{b=H[l+20>>2];if(!((b|0)==3|(v|0)!=3)){a=H[l+24>>2];if(H[a>>2]!=H[a+4>>2]|H[a+52>>2]==1){break q}H[l+20>>2]=3;break p}if(v>>>0>2){break q}H[l+20>>2]=2;break o}r:{switch(b-3|0){case 2:s:{t:{if(v>>>0<4){break t}b=H[l+24>>2];a=H[b>>2];if((a|0)!=H[b+52>>2]|(a|0)!=H[b+104>>2]|(a|0)!=H[b+156>>2]){break t}a=H[b+4>>2];if((a|0)!=H[b+56>>2]|(a|0)!=H[b+108>>2]){break t}if((a|0)==H[b+160>>2]){break s}}H[n+20>>2]=1053;H[n+16>>2]=1373;Ga(26072,8179,n+16|0);break o}f=N(H[b+12>>2],H[b+8>>2]);C=O(O(1)/O((-1<>2]^-1)>>>0));D=O(O(1)/O((-1<>2]^-1)>>>0));w=O(O(1)/O((-1<>2]^-1)>>>0));Q=O(O(1)/O((-1<>2]^-1)>>>0));a=0;while(1){if((a|0)!=(f|0)){c=a<<2;g=c+H[b+148>>2]|0;i=H[g>>2];k=c+H[b+96>>2]|0;j=H[k>>2];m=c+H[b+44>>2]|0;s=O(O(1)-O(C*O(H[c+H[b+200>>2]>>2])));E=O(O(O(O(1)-O(Q*O(H[m>>2])))*O(255))*s);if(O(P(E))>2]=c;E=O(O(O(O(1)-O(w*O(j|0)))*O(255))*s);if(O(P(E))>2]=c;s=O(O(O(O(1)-O(D*O(i|0)))*O(255))*s);if(O(P(s))>2]=c;a=a+1|0;continue}break};Ca(H[b+200>>2]);a=H[l+24>>2];H[a+128>>2]=8;H[a+76>>2]=8;H[a+24>>2]=8;H[a+200>>2]=0;H[l+20>>2]=1;a=H[l+16>>2]-1|0;H[l+16>>2]=a;h=3;while(1){if(a>>>0<=h>>>0){break o}a=H[l+24>>2]+N(h,52)|0;B(a,a+52|0,52);h=h+1|0;a=H[l+16>>2];continue};case 0:break p;case 1:break r;default:break o}}b=H[l+24>>2];a=H[b>>2];u:{v:{if((a|0)!=H[b+52>>2]|(a|0)!=H[b+104>>2]){break v}a=H[b+4>>2];if((a|0)!=H[b+56>>2]){break v}if((a|0)==H[b+108>>2]){break u}}H[n+36>>2]=1115;H[n+32>>2]=1373;Ga(26072,8221,n+32|0);break o}a=H[b+24>>2];c=-1<>2]?0:a;i=H[b+84>>2]?0:a;k=N(H[b+12>>2],H[b+8>>2]);a=0;while(1){if((a|0)!=(k|0)){f=a<<2;j=f+H[b+44>>2]|0;m=f+H[b+148>>2]|0;s=O(H[m>>2]-g|0);h=f+H[b+96>>2]|0;C=O(H[h>>2]-i|0);D=O(H[j>>2]);w=O(O(O(s*O(1.4019900560379028))+O(O(C*O(-3680000008898787e-20))+D))+O(.5));if(O(P(w))>2]=(c|0)<(f|0)?c:(f|0)>0?f:0;w=O(O(O(s*O(-.7141128182411194))+O(O(D*O(1.0003000497817993))+O(C*O(-.34412500262260437))))+O(.5));if(O(P(w))>2]=(c|0)<(f|0)?c:(f|0)>0?f:0;s=O(O(O(s*O(-7999999979801942e-21))+O(O(D*O(.9998229742050171))+O(C*O(1.7720400094985962))))+O(.5));if(O(P(s))>2]=(c|0)<(f|0)?c:(f|0)>0?f:0;a=a+1|0;continue}break}H[l+20>>2]=1;break o}v=c>>>0>v>>>0?v:c;p=1;break o}w:{x:{c=H[l+24>>2];if(H[c>>2]!=1){break x}y:{switch(H[c+52>>2]-1|0){case 1:if(H[c+104>>2]!=2){break x}if(!(H[c+4>>2]!=1|H[c+56>>2]!=2|H[c+108>>2]!=2)){b=H[c+24>>2];h=H[c+148>>2];a=H[c+96>>2];j=H[c+44>>2];G=H[c+60>>2];q=H[c+8>>2];f=H[c+12>>2];c=N(q,f)<<2;g=Ia(c);i=Ia(c);k=Ia(c);if(!(!g|!i|!k)){m=-1<>2]&1;L=f-b|0;K=H[l>>2]&1;x=q-K|0;if(!b){c=k;f=i;b=g;break w}c=k;f=i;b=g;while(1){if((p|0)==(q|0)){break w}Ka(o,m,H[j>>2],0,0,b,f,c);p=p+1|0;c=c+4|0;f=f+4|0;b=b+4|0;j=j+4|0;continue}}Ca(g);Ca(i);Ca(k);break o}if(H[c+4>>2]!=1|H[c+56>>2]!=1|H[c+108>>2]!=1){break x}a=H[c+24>>2];f=H[c+148>>2];b=H[c+96>>2];h=H[c+44>>2];t=H[c+60>>2];g=H[c+8>>2];x=H[c+12>>2];c=N(g,x)<<2;i=Ia(c);k=Ia(c);m=Ia(c);if(!(!i|!k|!m)){o=-1<>2]&1;a=g-z|0;G=a&1;u=a>>>1|0;J=a&-2;a=m;j=k;c=i;while(1){if((q|0)!=(x|0)){if(z){Ka(r,o,H[h>>2],0,0,c,j,a);j=j+4|0;c=c+4|0;h=h+4|0;a=a+4|0}g=0;while(1){if(g>>>0>>0){Ka(r,o,H[h>>2],H[b>>2],H[f>>2],c,j,a);Ka(r,o,H[h+4>>2],H[b>>2],H[f>>2],c+4|0,j+4|0,a+4|0);g=g+2|0;f=f+4|0;b=b+4|0;a=a+8|0;j=j+8|0;c=c+8|0;h=h+8|0;continue}break}z:{if(!G){break z}g=H[h>>2];A:{if((t|0)==(u|0)){Ka(r,o,g,0,0,c,j,a);break A}Ka(r,o,g,H[b>>2],H[f>>2],c,j,a)}a=a+4|0;j=j+4|0;c=c+4|0;h=h+4|0;if(t>>>0<=u>>>0){break z}f=f+4|0;b=b+4|0}q=q+1|0;continue}break}Ca(H[H[l+24>>2]+44>>2]);a=H[l+24>>2];H[a+44>>2]=i;Ca(H[a+96>>2]);a=H[l+24>>2];H[a+96>>2]=k;Ca(H[a+148>>2]);a=H[l+24>>2];H[a+148>>2]=m;b=H[a+8>>2];H[a+112>>2]=b;H[a+60>>2]=b;b=H[a+12>>2];H[a+116>>2]=b;H[a+64>>2]=b;b=H[a>>2];H[a+104>>2]=b;H[a+52>>2]=b;b=H[a+4>>2];H[a+108>>2]=b;H[a+56>>2]=b;H[l+20>>2]=1;break o}Ca(i);Ca(k);Ca(m);break o;case 0:break y;default:break x}}if(H[c+104>>2]!=1|H[c+4>>2]!=1|(H[c+56>>2]!=1|H[c+108>>2]!=1)){break x}b=H[c+24>>2];h=H[c+148>>2];a=H[c+96>>2];j=H[c+44>>2];o=N(H[c+12>>2],H[c+8>>2]);c=o<<2;i=Ia(c);k=Ia(c);m=Ia(c);if(!(!i|!k|!m)){q=-1<>2],H[a>>2],H[h>>2],g,b,f);c=c+1|0;f=f+4|0;b=b+4|0;g=g+4|0;h=h+4|0;a=a+4|0;j=j+4|0;continue}break}Ca(H[H[l+24>>2]+44>>2]);a=H[l+24>>2];H[a+44>>2]=i;Ca(H[a+96>>2]);a=H[l+24>>2];H[a+96>>2]=k;Ca(H[a+148>>2]);H[H[l+24>>2]+148>>2]=m;H[l+20>>2]=1;break o}Ca(i);Ca(k);Ca(m);break o}H[n+4>>2]=463;H[n>>2]=1373;Ga(26072,8264,n);break o}J=x>>>1|0;z=x&-2;R=L&-2;u=q<<2;while(1){if(M>>>0>>0){p=c+u|0;r=f+u|0;q=b+u|0;t=j+u|0;if(K){Ka(o,m,H[j>>2],0,0,b,f,c);Ka(o,m,H[t>>2],H[a>>2],H[h>>2],q,r,p);p=p+4|0;r=r+4|0;q=q+4|0;t=t+4|0;c=c+4|0;f=f+4|0;j=j+4|0;b=b+4|0}A=0;while(1){if(z>>>0>A>>>0){Ka(o,m,H[j>>2],H[a>>2],H[h>>2],b,f,c);Ka(o,m,H[j+4>>2],H[a>>2],H[h>>2],b+4|0,f+4|0,c+4|0);Ka(o,m,H[t>>2],H[a>>2],H[h>>2],q,r,p);Ka(o,m,H[t+4>>2],H[a>>2],H[h>>2],q+4|0,r+4|0,p+4|0);A=A+2|0;h=h+4|0;a=a+4|0;p=p+8|0;r=r+8|0;q=q+8|0;t=t+8|0;c=c+8|0;f=f+8|0;b=b+8|0;j=j+8|0;continue}break}B:{if((x|0)==(z|0)){break B}A=H[j>>2];C:{if((G|0)==(J|0)){Ka(o,m,A,0,0,b,f,c);Ka(o,m,H[t>>2],0,0,q,r,p);break C}Ka(o,m,A,H[a>>2],H[h>>2],b,f,c);Ka(o,m,H[t>>2],H[a>>2],H[h>>2],q,r,p)}c=c+4|0;f=f+4|0;b=b+4|0;j=j+4|0;if(G>>>0<=J>>>0){break B}h=h+4|0;a=a+4|0}M=M+2|0;c=c+u|0;f=f+u|0;b=b+u|0;j=j+u|0;continue}break}D:{if(!(L&1)){break D}if(K){Ka(o,m,H[j>>2],0,0,b,f,c);c=c+4|0;f=f+4|0;j=j+4|0;b=b+4|0}p=0;while(1){if(p>>>0>>0){Ka(o,m,H[j>>2],H[a>>2],H[h>>2],b,f,c);Ka(o,m,H[j+4>>2],H[a>>2],H[h>>2],b+4|0,f+4|0,c+4|0);p=p+2|0;h=h+4|0;a=a+4|0;c=c+8|0;f=f+8|0;b=b+8|0;j=j+8|0;continue}break}if((x|0)==(z|0)){break D}j=H[j>>2];if((G|0)==(J|0)){Ka(o,m,j,0,0,b,f,c);break D}Ka(o,m,j,H[a>>2],H[h>>2],b,f,c)}Ca(H[H[l+24>>2]+44>>2]);a=H[l+24>>2];H[a+44>>2]=g;Ca(H[a+96>>2]);a=H[l+24>>2];H[a+96>>2]=i;Ca(H[a+148>>2]);a=H[l+24>>2];H[a+148>>2]=k;b=H[a+8>>2];H[a+112>>2]=b;H[a+60>>2]=b;b=H[a+12>>2];H[a+116>>2]=b;H[a+64>>2]=b;b=H[a>>2];H[a+104>>2]=b;H[a+52>>2]=b;b=H[a+4>>2];H[a+108>>2]=b;H[a+56>>2]=b;H[l+20>>2]=1;p=0}c=H[n+64>>2];E:{if(d){break E}b=0;while(1){if((b|0)==(v|0)){break E}a=H[c+24>>2]+N(b,52)|0;d=H[a+24>>2];if((d|0)!=8){F:{if(d>>>0<=7){f=N(H[a+12>>2],H[a+8>>2]);g=H[a+44>>2];if(H[a+32>>2]){i=1<>2];k=d>>31<<7|d>>>25;S=m,T=ue(d<<7,k,i,0),H[S>>2]=T;h=h+1|0;continue}}d=-1<>2],0,255),qa,d,0);H[i>>2]=k;h=h+1|0;continue}}d=d-8|0;f=N(H[a+12>>2],H[a+8>>2]);g=H[a+44>>2];h=0;if(H[a+32>>2]){while(1){if((f|0)==(h|0)){break F}i=g+(h<<2)|0;H[i>>2]=H[i>>2]>>d;h=h+1|0;continue}}while(1){if((f|0)==(h|0)){break F}i=g+(h<<2)|0;H[i>>2]=H[i>>2]>>>d;h=h+1|0;continue}}H[a+24>>2]=8}b=b+1|0;continue}}a=H[c+24>>2];b=N(H[a+12>>2],H[a+8>>2]);G:{if(!p){if(H[c+20>>2]==2){if(H[c+16>>2]==1){ma(H[a+44>>2],b|0);break G}if(!e){break G}da(H[a+44>>2],H[a+96>>2],b|0);break G}ca(H[a+44>>2],H[a+96>>2],H[a+148>>2],b|0);break G}H:{switch(v-1|0){case 0:ba(H[a+44>>2],b|0);break G;case 2:aa(H[a+44>>2],H[a+96>>2],H[a+148>>2],b|0);break G;case 3:break H;default:break G}}$(H[a+44>>2],H[a+96>>2],H[a+148>>2],H[a+200>>2],b|0)}Ua(H[n+64>>2]);j=0}na=n+8320|0;return j|0}function ac(a,b,c,d,e,f,g,h,i){var j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,z=0,A=0,C=0,D=0,E=0,F=0,G=0,I=0,J=0,L=0,M=0,O=0,P=0;j=H[a>>2];a:{if(j>>>0>>0|b>>>0>=d>>>0|b>>>0>=j>>>0){break a}j=H[a+4>>2];if(j>>>0>>0|c>>>0>=e>>>0|c>>>0>=j>>>0){break a}E=(c>>>0)/K[a+12>>2]|0;s=H[a+8>>2];G=(b>>>0)/(s>>>0)|0;M=(N(s,G)-b|0)+s|0;I=(g|0)!=8;z=c;while(1){k=H[a+12>>2];j=k;j=(c|0)==(z|0)?j-((c>>>0)%(j>>>0)|0)|0:j;t=e-z|0;r=j>>>0>>0?j:t;A=r&-4;u=r&3;O=r&-8;J=r&7;x=r-1|0;P=(g|0)==2&(r|0)==1;L=N(k-j|0,s);C=(N(z-c|0,h)<<2)+f|0;F=G;t=b;while(1){j=(b|0)==(t|0)?M:s;k=d-t|0;q=j>>>0>>0?j:k;k=s-j|0;l=F<<2;j=H[l+(H[a+24>>2]+(N(H[a+16>>2],E)<<2)|0)>>2];b:{c:{d:{e:{f:{g:{if(i){h:{i:{j:{k:{if(j){l=((L<<2)+j|0)+(k<<2)|0;j=t-b|0;if((g|0)==1){break h}m=(N(g,j)<<2)+C|0;if((q|0)==1){break i}if(P){break j}if(q>>>0<=7|I){break k}if(!r){break b}o=q&-4;k=0;while(1){j=0;while(1){H[(j<<5)+m>>2]=H[(j<<2)+l>>2];n=j|1;H[(n<<5)+m>>2]=H[(n<<2)+l>>2];n=j|2;H[(n<<5)+m>>2]=H[(n<<2)+l>>2];n=j|3;H[(n<<5)+m>>2]=H[(n<<2)+l>>2];j=j+4|0;if(o>>>0>j>>>0){continue}break}if(j>>>0>>0){while(1){H[(j<<5)+m>>2]=H[(j<<2)+l>>2];j=j+1|0;if((q|0)!=(j|0)){continue}break}}l=(s<<2)+l|0;m=(h<<2)+m|0;k=k+1|0;if((r|0)!=(k|0)){continue}break}break b}if((g|0)!=1){if(!r){break b}p=q&-4;n=q&3;l=(N(t-b|0,g)<<2)+C|0;o=0;D=q-1>>>0<3;while(1){l:{if(!q){break l}m=0;j=0;k=0;if(!D){while(1){H[(N(g,j)<<2)+l>>2]=0;H[(N(j|1,g)<<2)+l>>2]=0;H[(N(j|2,g)<<2)+l>>2]=0;H[(N(j|3,g)<<2)+l>>2]=0;j=j+4|0;k=k+4|0;if((p|0)!=(k|0)){continue}break}if(!n){break l}}while(1){H[(N(g,j)<<2)+l>>2]=0;j=j+1|0;m=m+1|0;if((n|0)!=(m|0)){continue}break}}l=(h<<2)+l|0;o=o+1|0;if((r|0)!=(o|0)){continue}break}break b}if(!r){break b}l=q<<2;k=(t-b<<2)+C|0;o=0;if(x>>>0>=7){break g}break f}if(!r){break b}D=q&-4;p=q&3;n=0;v=q-1>>>0<3;break c}j=0;k=q&-4;if(k){while(1){H[(j<<3)+m>>2]=H[(j<<2)+l>>2];o=j|1;H[(o<<3)+m>>2]=H[(o<<2)+l>>2];o=j|2;H[(o<<3)+m>>2]=H[(o<<2)+l>>2];o=j|3;H[(o<<3)+m>>2]=H[(o<<2)+l>>2];j=j+4|0;if(k>>>0>j>>>0){continue}break}}if(j>>>0>=q>>>0){break b}o=0;k=j;n=q-j&3;if(n){while(1){H[(k<<3)+m>>2]=H[(k<<2)+l>>2];k=k+1|0;o=o+1|0;if((n|0)!=(o|0)){continue}break}}if(j-q>>>0>4294967292){break b}while(1){H[(k<<3)+m>>2]=H[(k<<2)+l>>2];j=k+1|0;H[(j<<3)+m>>2]=H[(j<<2)+l>>2];j=k+2|0;H[(j<<3)+m>>2]=H[(j<<2)+l>>2];j=k+3|0;H[(j<<3)+m>>2]=H[(j<<2)+l>>2];k=k+4|0;if((q|0)!=(k|0)){continue}break}break b}if(!r){break b}k=0;if(x>>>0>=3){while(1){H[m>>2]=H[l>>2];j=h<<2;m=j+m|0;p=l;l=s<<2;o=p+l|0;H[m>>2]=H[o>>2];m=j+m|0;o=l+o|0;H[m>>2]=H[o>>2];m=j+m|0;o=l+o|0;H[m>>2]=H[o>>2];l=l+o|0;m=j+m|0;k=k+4|0;if((A|0)!=(k|0)){continue}break}if(!u){break b}}j=0;while(1){H[m>>2]=H[l>>2];l=(s<<2)+l|0;m=(h<<2)+m|0;j=j+1|0;if((u|0)!=(j|0)){continue}break}break b}j=(j<<2)+C|0;if((q|0)!=4){if(!r){break b}m=q<<2;o=0;if(x>>>0>=3){break e}break d}if(!r){break b}o=0;if(x>>>0>=3){while(1){k=H[l+12>>2];H[j+8>>2]=H[l+8>>2];H[j+12>>2]=k;k=H[l+4>>2];H[j>>2]=H[l>>2];H[j+4>>2]=k;k=l;l=s<<2;k=k+l|0;n=H[k+12>>2];m=h<<2;j=m+j|0;H[j+8>>2]=H[k+8>>2];H[j+12>>2]=n;n=H[k+4>>2];H[j>>2]=H[k>>2];H[j+4>>2]=n;k=l+k|0;n=H[k+12>>2];j=j+m|0;H[j+8>>2]=H[k+8>>2];H[j+12>>2]=n;n=H[k+4>>2];H[j>>2]=H[k>>2];H[j+4>>2]=n;k=l+k|0;n=H[k+12>>2];j=j+m|0;H[j+8>>2]=H[k+8>>2];H[j+12>>2]=n;n=H[k+4>>2];H[j>>2]=H[k>>2];H[j+4>>2]=n;l=l+k|0;j=j+m|0;o=o+4|0;if((A|0)!=(o|0)){continue}break}if(!u){break b}}m=0;while(1){k=H[l+12>>2];H[j+8>>2]=H[l+8>>2];H[j+12>>2]=k;k=H[l+4>>2];H[j>>2]=H[l>>2];H[j+4>>2]=k;l=(s<<2)+l|0;j=(h<<2)+j|0;m=m+1|0;if((u|0)!=(m|0)){continue}break}break b}if(!j){j=Ea(1,N(H[a+8>>2],H[a+12>>2])<<2);if(!j){return 0}H[l+(H[a+24>>2]+(N(H[a+16>>2],E)<<2)|0)>>2]=j}l=((L<<2)+j|0)+(k<<2)|0;j=t-b|0;m:{n:{o:{p:{q:{r:{if((g|0)!=1){m=(N(g,j)<<2)+C|0;if((q|0)==1){break r}if(q>>>0<=7|I){break q}if(!r){break b}o=q&-4;k=0;while(1){j=0;while(1){H[(j<<2)+l>>2]=H[(j<<5)+m>>2];n=j|1;H[(n<<2)+l>>2]=H[(n<<5)+m>>2];n=j|2;H[(n<<2)+l>>2]=H[(n<<5)+m>>2];n=j|3;H[(n<<2)+l>>2]=H[(n<<5)+m>>2];j=j+4|0;if(o>>>0>j>>>0){continue}break}if(j>>>0>>0){while(1){H[(j<<2)+l>>2]=H[(j<<5)+m>>2];j=j+1|0;if((q|0)!=(j|0)){continue}break}}l=(s<<2)+l|0;m=(h<<2)+m|0;k=k+1|0;if((r|0)!=(k|0)){continue}break}break b}j=(j<<2)+C|0;if((q|0)==4){break p}if(!r){break b}m=q<<2;o=0;if(x>>>0>=3){break o}break n}if(!r){break b}o=0;if(x>>>0>=3){while(1){H[l>>2]=H[m>>2];j=s<<2;l=j+l|0;k=h<<2;m=k+m|0;H[l>>2]=H[m>>2];l=j+l|0;m=k+m|0;H[l>>2]=H[m>>2];l=j+l|0;m=k+m|0;H[l>>2]=H[m>>2];l=j+l|0;m=k+m|0;o=o+4|0;if((A|0)!=(o|0)){continue}break}if(!u){break b}}j=0;while(1){H[l>>2]=H[m>>2];l=(s<<2)+l|0;m=(h<<2)+m|0;j=j+1|0;if((u|0)!=(j|0)){continue}break}break b}if(!r){break b}D=q&-4;p=q&3;n=0;break m}if(!r){break b}o=0;if(x>>>0>=3){while(1){k=H[j+12>>2];H[l+8>>2]=H[j+8>>2];H[l+12>>2]=k;k=H[j+4>>2];H[l>>2]=H[j>>2];H[l+4>>2]=k;m=h<<2;j=m+j|0;n=H[j+12>>2];k=l;l=s<<2;k=k+l|0;H[k+8>>2]=H[j+8>>2];H[k+12>>2]=n;n=H[j+4>>2];H[k>>2]=H[j>>2];H[k+4>>2]=n;j=j+m|0;n=H[j+12>>2];k=l+k|0;H[k+8>>2]=H[j+8>>2];H[k+12>>2]=n;n=H[j+4>>2];H[k>>2]=H[j>>2];H[k+4>>2]=n;j=j+m|0;n=H[j+12>>2];k=l+k|0;H[k+8>>2]=H[j+8>>2];H[k+12>>2]=n;n=H[j+4>>2];H[k>>2]=H[j>>2];H[k+4>>2]=n;j=j+m|0;l=l+k|0;o=o+4|0;if((A|0)!=(o|0)){continue}break}if(!u){break b}}m=0;while(1){k=H[j+12>>2];H[l+8>>2]=H[j+8>>2];H[l+12>>2]=k;k=H[j+4>>2];H[l>>2]=H[j>>2];H[l+4>>2]=k;j=(h<<2)+j|0;l=(s<<2)+l|0;m=m+1|0;if((u|0)!=(m|0)){continue}break}break b}while(1){k=!m;if(!k){B(l,j,m)}p=j;j=h<<2;n=p+j|0;p=l;l=s<<2;p=p+l|0;if(!k){B(p,n,m)}n=j+n|0;p=l+p|0;if(!k){B(p,n,m)}n=j+n|0;p=l+p|0;if(!k){B(p,n,m)}j=j+n|0;l=l+p|0;o=o+4|0;if((A|0)!=(o|0)){continue}break}if(!u){break b}}k=0;while(1){if(m){B(l,j,m)}j=(h<<2)+j|0;l=(s<<2)+l|0;k=k+1|0;if((u|0)!=(k|0)){continue}break}break b}while(1){s:{if(!q){break s}k=0;j=0;o=0;if(q>>>0>=4){while(1){H[(j<<2)+l>>2]=H[(N(g,j)<<2)+m>>2];v=j|1;H[(v<<2)+l>>2]=H[(N(g,v)<<2)+m>>2];v=j|2;H[(v<<2)+l>>2]=H[(N(g,v)<<2)+m>>2];v=j|3;H[(v<<2)+l>>2]=H[(N(g,v)<<2)+m>>2];j=j+4|0;o=o+4|0;if((D|0)!=(o|0)){continue}break}if(!p){break s}}while(1){H[(j<<2)+l>>2]=H[(N(g,j)<<2)+m>>2];j=j+1|0;k=k+1|0;if((p|0)!=(k|0)){continue}break}}l=(s<<2)+l|0;m=(h<<2)+m|0;n=n+1|0;if((r|0)!=(n|0)){continue}break}break b}while(1){j=!l;if(!j){y(k,0,l)}p=k;k=h<<2;m=p+k|0;if(!j){y(m,0,l)}m=k+m|0;if(!j){y(m,0,l)}m=k+m|0;if(!j){y(m,0,l)}m=k+m|0;if(!j){y(m,0,l)}m=k+m|0;if(!j){y(m,0,l)}m=k+m|0;if(!j){y(m,0,l)}m=k+m|0;if(!j){y(m,0,l)}k=k+m|0;o=o+8|0;if((O|0)!=(o|0)){continue}break}if(!J){break b}}j=0;while(1){if(l){y(k,0,l)}k=(h<<2)+k|0;j=j+1|0;if((J|0)!=(j|0)){continue}break}break b}while(1){k=!m;if(!k){B(j,l,m)}p=l;l=s<<2;n=p+l|0;p=j;j=h<<2;p=p+j|0;if(!k){B(p,n,m)}n=l+n|0;p=j+p|0;if(!k){B(p,n,m)}n=l+n|0;p=j+p|0;if(!k){B(p,n,m)}l=l+n|0;j=j+p|0;o=o+4|0;if((A|0)!=(o|0)){continue}break}if(!u){break b}}k=0;while(1){if(m){B(j,l,m)}l=(s<<2)+l|0;j=(h<<2)+j|0;k=k+1|0;if((u|0)!=(k|0)){continue}break}break b}while(1){t:{if(!q){break t}k=0;j=0;o=0;if(!v){while(1){H[(N(g,j)<<2)+m>>2]=H[(j<<2)+l>>2];w=j|1;H[(N(w,g)<<2)+m>>2]=H[(w<<2)+l>>2];w=j|2;H[(N(w,g)<<2)+m>>2]=H[(w<<2)+l>>2];w=j|3;H[(N(w,g)<<2)+m>>2]=H[(w<<2)+l>>2];j=j+4|0;o=o+4|0;if((D|0)!=(o|0)){continue}break}if(!p){break t}}while(1){H[(N(g,j)<<2)+m>>2]=H[(j<<2)+l>>2];j=j+1|0;k=k+1|0;if((p|0)!=(k|0)){continue}break}}l=(s<<2)+l|0;m=(h<<2)+m|0;n=n+1|0;if((r|0)!=(n|0)){continue}break}}F=F+1|0;t=q+t|0;if(t>>>0>>0){continue}break}E=E+1|0;z=r+z|0;if(z>>>0>>0){continue}break}}return 1}function Od(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0;if(!c){return 0}if(!Gb(H[a>>2],b,c,d)){Ba(d,1,6210,0);return 0}n=c;m=d;g=na-240|0;na=g;r=1;o=a;a:{if(H[H[a>>2]+60>>2]|H[a+128>>2]){break a}b:{c=H[a+116>>2];c:{if(!c){a=H[a+120>>2];break c}b=H[n+16>>2];a=H[o+120>>2];if(!(!a|!H[a+12>>2])){b=I[a+18|0]}d:{d=J[c+4>>1];if(d){c=H[c>>2];while(1){f=c+N(e,6)|0;h=J[f>>1];if(h>>>0>=b>>>0){H[g+180>>2]=b;H[g+176>>2]=h;Ba(m,1,13715,g+176|0);r=0;break a}e:{f=J[f+4>>1];if((f+1&65535)>>>0<2){break e}f=f-1|0;if(f>>>0>>0){break e}H[g+164>>2]=b;H[g+160>>2]=f;Ba(m,1,13715,g+160|0);r=0;break a}e=e+1|0;if((d|0)!=(e|0)){continue}break}break d}if(b){break b}break c}while(1){b=b-1|0;e=0;while(1){if(J[c+N(e,6)>>1]!=(b|0)){e=e+1|0;if((d|0)!=(e|0)){continue}break b}break}if(b){continue}break}}f:{if(!a){break f}c=H[a+12>>2];if(!c){break f}g:{b=I[a+18|0];h:{if(b){e=0;h=1;while(1){a=H[n+16>>2];d=J[c+(e<<2)>>1];if(a>>>0<=d>>>0){H[g+148>>2]=a;H[g+144>>2]=d;Ba(m,1,13715,g+144|0);h=0}e=e+1|0;if((b|0)!=(e|0)){continue}break}a=Ea(b,4);if(!a){break h}e=0;while(1){d=c+(e<<2)|0;f=I[d+2|0];i:{if(f>>>0>=2){H[g+68>>2]=f;H[g+64>>2]=e;Ba(m,1,12094,g- -64|0);h=0;break i}d=I[d+3|0];if(d>>>0>=b>>>0){H[g+128>>2]=d;Ba(m,1,12038,g+128|0);h=0;break i}j=(f|0)!=1;i=(d<<2)+a|0;if(!(j|!H[i>>2])){H[g+80>>2]=d;Ba(m,1,11527,g+80|0);h=0;break i}if(!(f|!d)){H[g+100>>2]=d;H[g+96>>2]=e;Ba(m,1,11901,g+96|0);h=0;break i}if(!(j|(d|0)==(e|0))){H[g+120>>2]=d;H[g+116>>2]=e;H[g+112>>2]=e;Ba(m,1,11937,g+112|0);h=0;break i}H[i>>2]=1}e=e+1|0;if((b|0)!=(e|0)){continue}break}h=!h;e=0;while(1){j:{d=e<<2;if(I[(c+d|0)+2|0]?H[d+a>>2]:1){e=e+1|0;if((b|0)!=(e|0)){continue}if(h&1){break j}if(H[n+16>>2]!=1){break g}e=0;while(1){if(H[(e<<2)+a>>2]){e=e+1|0;if((b|0)!=(e|0)){continue}break g}break}d=0;Ba(m,2,9253,0);f=b&3;e=0;if(b>>>0>=4){h=b&252;b=0;while(1){j=c+(e<<2)|0;F[j+3|0]=e;F[j+2|0]=1;j=e|1;i=c+(j<<2)|0;F[i+3|0]=j;F[i+2|0]=1;j=e|2;i=c+(j<<2)|0;F[i+3|0]=j;F[i+2|0]=1;j=e|3;i=c+(j<<2)|0;F[i+3|0]=j;F[i+2|0]=1;e=e+4|0;b=b+4|0;if((h|0)!=(b|0)){continue}break}if(!f){break g}}while(1){b=c+(e<<2)|0;F[b+3|0]=e;F[b+2|0]=1;e=e+1|0;d=d+1|0;if((f|0)!=(d|0)){continue}break}break g}H[g+48>>2]=e;h=1;Ba(m,1,11101,g+48|0);e=e+1|0;if((b|0)!=(e|0)){continue}}break}Ca(a);r=0;break a}a=Ea(b,4);if(a){break g}}r=0;Ba(m,1,12285,0);break a}Ca(a)}a=H[o+120>>2];k:{if(!a){break k}l=H[a+12>>2];if(!l){Ca(H[a+4>>2]);Ca(H[H[o+120>>2]+8>>2]);Ca(H[H[o+120>>2]>>2]);a=H[o+120>>2];b=H[a+12>>2];if(b){Ca(b);a=H[o+120>>2]}Ca(a);H[o+120>>2]=0;break k}j=H[n+24>>2];l:{c=I[a+18|0];m:{if(c){u=H[a>>2];h=H[a+4>>2];i=H[a+8>>2];e=0;n:{while(1){if(H[(j+N(J[l+(e<<2)>>1],52)|0)+44>>2]){e=e+1|0;if((c|0)!=(e|0)){continue}break n}break}H[g+32>>2]=e;Ba(m,1,13877,g+32|0);r=0;break a}f=Fa(N(c,52));if(!f){break m}d=0;while(1){a=l+(d<<2)|0;e=J[a>>1];b=N(I[a+2|0]?I[a+3|0]:d,52)+f|0;a=j+N(e,52)|0;H[b+48>>2]=H[a+48>>2];e=H[a+44>>2];H[b+40>>2]=H[a+40>>2];H[b+44>>2]=e;e=H[a+36>>2];H[b+32>>2]=H[a+32>>2];H[b+36>>2]=e;e=H[a+28>>2];H[b+24>>2]=H[a+24>>2];H[b+28>>2]=e;e=H[a+20>>2];H[b+16>>2]=H[a+16>>2];H[b+20>>2]=e;e=H[a+12>>2];H[b+8>>2]=H[a+8>>2];H[b+12>>2]=e;e=H[a+4>>2];H[b>>2]=H[a>>2];H[b+4>>2]=e;b=N(d,52)+f|0;a=Ia(N(H[a+8>>2],H[a+12>>2])<<2);H[b+44>>2]=a;if(!a){if(d){a=d&65535;while(1){Ca(H[(N(a,52)+f|0)-8>>2]);a=a-1|0;if(a){continue}break}}Ca(f);r=0;Ba(m,1,13825,0);break a}H[b+24>>2]=I[d+i|0];H[b+32>>2]=I[d+h|0];d=d+1|0;if((c|0)!=(d|0)){continue}break}h=J[H[o+120>>2]+16>>1]-1|0;while(1){a=N(q,52)+f|0;d=N(H[a+12>>2],H[a+8>>2]);b=l+(q<<2)|0;e=H[(j+N(J[b>>1],52)|0)+44>>2];o:{if(!I[b+2|0]){if(!d){break o}i=H[a+44>>2];p=d&3;a=0;b=0;if(d>>>0>=4){t=d&-4;k=0;while(1){d=b<<2;H[d+i>>2]=H[d+e>>2];s=d|4;H[s+i>>2]=H[e+s>>2];s=d|8;H[s+i>>2]=H[e+s>>2];d=d|12;H[d+i>>2]=H[d+e>>2];b=b+4|0;k=k+4|0;if((t|0)!=(k|0)){continue}break}if(!p){break o}}while(1){d=b<<2;H[d+i>>2]=H[d+e>>2];b=b+1|0;a=a+1|0;if((p|0)!=(a|0)){continue}break}break o}if(!d){break o}b=I[b+3|0];a=(b<<2)+u|0;i=H[(N(b,52)+f|0)+44>>2];b=0;if((d|0)!=1){t=d&1;s=d&-2;d=0;while(1){k=b<<2;p=H[k+e>>2];H[i+k>>2]=H[a+(N(c,(p|0)>=0?(h|0)>(p|0)?p:h:0)<<2)>>2];k=k|4;p=H[k+e>>2];H[i+k>>2]=H[a+(N(c,(p|0)>=0?(h|0)>(p|0)?p:h:0)<<2)>>2];b=b+2|0;d=d+2|0;if((s|0)!=(d|0)){continue}break}if(!t){break o}}d=b<<2;b=H[d+e>>2];H[d+i>>2]=H[a+(N(c,(b|0)>=0?(b|0)<(h|0)?b:h:0)<<2)>>2]}q=q+1|0;if((c|0)!=(q|0)){continue}break}break l}f=Fa(N(c,52));if(f){break l}}r=0;Ba(m,1,13825,0);break a}a=H[n+16>>2];if(a){e=0;while(1){b=H[(j+N(e,52)|0)+44>>2];if(b){Ca(b)}e=e+1|0;if((a|0)!=(e|0)){continue}break}}Ca(j);H[n+16>>2]=c;H[n+24>>2]=f}e=H[o+116>>2];if(!e){break a}h=H[e>>2];i=J[e+4>>1];if(i){e=0;p=i-2&65535;d=1;while(1){a=H[n+16>>2];q=N(e,6)+h|0;b=J[q>>1];p:{if(a>>>0<=b>>>0){H[g+20>>2]=a;H[g+16>>2]=b;Ba(m,2,7334,g+16|0);break p}c=J[q+4>>1];if((c+1&65535)>>>0<=1){G[(H[n+24>>2]+N(b,52)|0)+48>>1]=J[q+2>>1];break p}c=c-1|0;j=c&65535;if(j>>>0>=a>>>0){H[g+4>>2]=a;H[g>>2]=j;Ba(m,2,7293,g);break p}q:{if(J[q+2>>1]|(b|0)==(j|0)){break q}f=H[n+24>>2];a=f+N(b,52)|0;H[g+232>>2]=H[a+48>>2];l=H[a+44>>2];H[g+224>>2]=H[a+40>>2];H[g+228>>2]=l;l=H[a+36>>2];H[g+216>>2]=H[a+32>>2];H[g+220>>2]=l;l=H[a+28>>2];H[g+208>>2]=H[a+24>>2];H[g+212>>2]=l;l=H[a+20>>2];H[g+200>>2]=H[a+16>>2];H[g+204>>2]=l;l=H[a+12>>2];H[g+192>>2]=H[a+8>>2];H[g+196>>2]=l;l=H[a+4>>2];H[g+184>>2]=H[a>>2];H[g+188>>2]=l;l=N(j,52);f=l+f|0;H[a+48>>2]=H[f+48>>2];k=H[f+44>>2];H[a+40>>2]=H[f+40>>2];H[a+44>>2]=k;k=H[f+36>>2];H[a+32>>2]=H[f+32>>2];H[a+36>>2]=k;k=H[f+28>>2];H[a+24>>2]=H[f+24>>2];H[a+28>>2]=k;k=H[f+20>>2];H[a+16>>2]=H[f+16>>2];H[a+20>>2]=k;k=H[f+12>>2];H[a+8>>2]=H[f+8>>2];H[a+12>>2]=k;k=H[f+4>>2];H[a>>2]=H[f>>2];H[a+4>>2]=k;a=l+H[n+24>>2]|0;H[a+48>>2]=H[g+232>>2];f=H[g+228>>2];H[a+40>>2]=H[g+224>>2];H[a+44>>2]=f;f=H[g+220>>2];H[a+32>>2]=H[g+216>>2];H[a+36>>2]=f;f=H[g+212>>2];H[a+24>>2]=H[g+208>>2];H[a+28>>2]=f;f=H[g+204>>2];H[a+16>>2]=H[g+200>>2];H[a+20>>2]=f;f=H[g+196>>2];H[a+8>>2]=H[g+192>>2];H[a+12>>2]=f;f=H[g+188>>2];H[a>>2]=H[g+184>>2];H[a+4>>2]=f;if(i>>>0<=e+1>>>0){break q}f=d;if(!(e-i&1)){a=c;f=N(d,6)+h|0;l=J[f>>1];r:{if((l|0)!=(b|0)){a=b;if((l|0)!=(j|0)){break r}}G[f>>1]=a}f=d+1|0}if((p|0)==(e&65535)){break q}while(1){a=c;l=N(f,6)+h|0;k=J[l>>1];s:{if((k|0)!=(b|0)){a=b;if((k|0)!=(j|0)){break s}}G[l>>1]=a}a=c;k=J[l+6>>1];t:{if((k|0)!=(b|0)){a=b;if((k|0)!=(j|0)){break t}}G[l+6>>1]=a}f=f+2|0;if((i|0)!=(f&65535)){continue}break}}G[(H[n+24>>2]+N(b,52)|0)+48>>1]=J[q+2>>1]}d=d+1|0;e=e+1|0;if((i|0)!=(e|0)){continue}break}e=H[o+116>>2];h=H[e>>2]}if(h){Ca(h);e=H[o+116>>2]}Ca(e);H[o+116>>2]=0;break a}r=0;Ba(m,1,9499,0)}na=g+240|0;return r|0}function Tc(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=O(0),p=0;k=na-48|0;na=k;H[a+8>>2]=1;a:{b:{d=k+40|0;c:{if((Ja(b,d,2,c)|0)!=2){break c}Da(d,k+44|0,2);if(H[k+44>>2]!=65359){break c}H[a+8>>2]=2;d=H[b+56>>2];e=d-2|0;d=H[b+60>>2]-(d>>>0<2)|0;g=H[a+224>>2];H[g>>2]=e;H[g+4>>2]=d;H[k+16>>2]=e;H[k+20>>2]=d;Ba(c,4,12769,k+16|0);f=H[a+224>>2];j=H[f>>2];e=H[f+24>>2];d=e+1|0;g=H[f+32>>2];if(d>>>0<=g>>>0){g=H[f+28>>2];break b}o=O(O(g>>>0)+O(100));if(o=O(0)){d=~~o>>>0}else{d=0}H[f+32>>2]=d;g=Ha(H[f+28>>2],N(d,24));if(g){H[f+28>>2]=g;e=H[f+24>>2];d=e+1|0;break b}Ca(H[f+28>>2]);H[f+32>>2]=0;H[f+24>>2]=0;H[f+28>>2]=0;Ba(c,1,3899,0)}Ba(c,1,15656,0);a=0;break a}e=N(e,24)+g|0;H[e+16>>2]=2;H[e+8>>2]=j;H[e+12>>2]=j>>31;G[e>>1]=65359;H[f+24>>2]=d;if((Ja(b,H[a+16>>2],2,c)|0)!=2){Ba(c,1,2472,0);a=0;break a}Da(H[a+16>>2],k+40|0,2);d:{e:{g=H[k+40>>2];if((g|0)!=65424){while(1){e=24912;if(g>>>0<=65279){H[k>>2]=g;Ba(c,1,2268,k);a=0;break a}while(1){d=e;f=H[d>>2];if(f){e=d+12|0;if((f|0)!=(g|0)){continue}}break}f:{g:{if(f){break g}h=2;Ba(c,2,3847,0);e=2472;h:{i:{if((Ja(b,H[a+16>>2],2,c)|0)!=2){break i}while(1){Da(H[a+16>>2],k+44|0,2);f=24912;g=H[k+44>>2];if(g>>>0>=65280){while(1){d=f;i=H[d>>2];if(i){f=d+12|0;if((g|0)!=(i|0)){continue}}break}if(!(H[d+4>>2]&H[a+8>>2])){e=5397;break i}if(i){if((i|0)==65424){H[k+40>>2]=65424;break f}j=H[b+56>>2];f=H[a+224>>2];g=H[f+24>>2];e=g+1|0;d=H[f+32>>2];if(e>>>0<=d>>>0){d=H[f+28>>2];break h}o=O(O(d>>>0)+O(100));if(o=O(0)){d=~~o>>>0}else{d=0}H[f+32>>2]=d;d=Ha(H[f+28>>2],N(d,24));if(d){H[f+28>>2]=d;g=H[f+24>>2];e=g+1|0;break h}Ca(H[f+28>>2]);H[f+32>>2]=0;H[f+24>>2]=0;H[f+28>>2]=0;e=3899;break i}h=h+2|0}if((Ja(b,H[a+16>>2],2,c)|0)==2){continue}break}}Ba(c,1,e,0);Ba(c,1,9847,0);a=0;break a}d=N(g,24)+d|0;H[d+16>>2]=h;g=j-h|0;H[d+8>>2]=g;H[d+12>>2]=g>>31;G[d>>1]=0;H[f+24>>2]=e;H[k+40>>2]=i;g=24912;if((i|0)==65424){break f}while(1){d=g;f=H[d>>2];if(!f){break g}g=d+12|0;if((f|0)!=(i|0)){continue}break}}if(!(H[d+4>>2]&H[a+8>>2])){Ba(c,1,5397,0);a=0;break a}if((Ja(b,H[a+16>>2],2,c)|0)!=2){Ba(c,1,2472,0);a=0;break a}Da(H[a+16>>2],k+36|0,2);e=H[k+36>>2];if(e>>>0<=1){Ba(c,1,6074,0);a=0;break a}e=e-2|0;H[k+36>>2]=e;g=H[a+16>>2];if(K[a+20>>2]>>0){g=Ha(g,e);if(!g){Ca(H[a+16>>2]);H[a+16>>2]=0;H[a+20>>2]=0;Ba(c,1,4973,0);a=0;break a}H[a+16>>2]=g;e=H[k+36>>2];H[a+20>>2]=e}e=Ja(b,g,e,c);if((e|0)!=H[k+36>>2]){Ba(c,1,2472,0);a=0;break a}if(!(ra[H[d+8>>2]](a,H[a+16>>2],e,c)|0)){Ba(c,1,2490,0);a=0;break a}j=H[b+56>>2];i=H[k+36>>2];d=H[a+224>>2];g=H[d+24>>2];h=g+1|0;e=H[d+32>>2];j:{if(h>>>0<=e>>>0){e=H[d+28>>2];break j}o=O(O(e>>>0)+O(100));if(o=O(0)){e=~~o>>>0}else{e=0}H[d+32>>2]=e;e=Ha(H[d+28>>2],N(e,24));if(!e){break d}H[d+28>>2]=e;g=H[d+24>>2];h=g+1|0}e=N(g,24)+e|0;H[e+16>>2]=i+4;g=(j-i|0)-4|0;H[e+8>>2]=g;H[e+12>>2]=g>>31;G[e>>1]=f;H[d+24>>2]=h;if((Ja(b,H[a+16>>2],2,c)|0)!=2){Ba(c,1,2472,0);a=0;break a}m=(f|0)==65372?1:m;l=(f|0)==65362?1:l;n=(f|0)==65361?1:n;Da(H[a+16>>2],k+40|0,2);g=H[k+40>>2];if((g|0)!=65424){continue}}break}if(n){break e}}Ba(c,1,4785,0);a=0;break a}if(!l){Ba(c,1,4831,0);a=0;break a}if(!m){Ba(c,1,4877,0);a=0;break a}d=0;e=0;h=0;j=na-16|0;na=j;m=1;k:{if(!(F[a+212|0]&1)){break k}l:{f=H[a+136>>2];if(!f){break l}m:{while(1){g=H[a+140>>2]+(h<<3)|0;l=H[g>>2];if(l){i=H[g+4>>2];g=d-i|0;g=d>>>0>=g>>>0?g:0;if(d>>>0>>0){f=i-d|0;l=d+l|0;while(1){if(f>>>0<4){d=5671;break m}Da(l,j+12|0,4);d=H[j+12>>2];if((d^-1)>>>0>>0){d=5645;break m}i=f-4|0;n=i>>>0>>0;g=n?d-i|0:g;e=d+e|0;f=i-d|0;l=((n?0:d)+l|0)+4|0;if(d>>>0>>0){continue}break}f=H[a+136>>2]}d=g}h=h+1|0;if(h>>>0>>0){continue}break}if(!d){break l}m=0;Ba(c,1,3067,0);break k}m=0;Ba(c,1,d,0);break k}d=Fa(e);H[a+160>>2]=d;if(!d){m=0;Ba(c,1,4337,0);break k}H[a+148>>2]=e;h=H[a+140>>2];n:{f=H[a+136>>2];if(f){e=0;d=0;g=0;while(1){l=g<<3;n=l+h|0;i=H[n>>2];if(i){h=H[a+160>>2]+d|0;f=H[n+4>>2];o:{if(f>>>0<=e>>>0){if(f){B(h,i,f)}d=d+f|0;e=e-f|0;break o}if(e){B(h,i,e)}d=d+e|0;h=f-e|0;e=e+i|0;while(1){if(h>>>0<4){break n}Da(e,j+8|0,4);e=e+4|0;i=H[a+160>>2]+d|0;f=h-4|0;h=H[j+8>>2];if(f>>>0>>0){if(f){B(i,e,f)}d=d+f|0;e=H[j+8>>2]-f|0;break o}if(h){B(i,e,h)}h=H[j+8>>2];d=h+d|0;e=e+h|0;h=f-h|0;if(h){continue}break}e=0}Ca(H[l+H[a+140>>2]>>2]);h=H[a+140>>2];f=l+h|0;H[f>>2]=0;H[f+4>>2]=0;f=H[a+136>>2]}g=g+1|0;if(g>>>0>>0){continue}break}e=H[a+148>>2];d=H[a+160>>2]}H[a+168>>2]=e;H[a+144>>2]=d;H[a+136>>2]=0;Ca(h);H[a+140>>2]=0;break k}m=0;Ba(c,1,5671,0)}na=j+16|0;if(!m){Ba(c,1,8085,0);a=0;break a}Ba(c,4,11754,0);d=H[a+224>>2];e=H[b+56>>2];e=e-2|0;H[d+8>>2]=e;H[d+12>>2]=0;b=0;h=0;i=na-16|0;na=i;g=H[a+68>>2];p:{if(!g){H[a+76>>2]=1;break p}if(H[a+76>>2]){break p}d=H[a+72>>2];j=H[a+224>>2];e=H[j+40>>2];q:{if((g|0)!=1){m=g&1;l=g&-2;while(1){n=(b<<3)+d|0;p=J[n>>1];f=e+N(p,40)|0;H[f>>2]=p;H[f+8>>2]=H[f+8>>2]+1;n=J[n+8>>1];f=e+N(n,40)|0;H[f>>2]=n;H[f+8>>2]=H[f+8>>2]+1;b=b+2|0;h=h+2|0;if((l|0)!=(h|0)){continue}break}if(!m){break q}}f=J[(b<<3)+d>>1];b=e+N(f,40)|0;H[b>>2]=f;H[b+8>>2]=H[b+8>>2]+1}f=H[j+36>>2];r:{if(f){b=0;while(1){if(!H[(e+N(b,40)|0)+8>>2]){H[i>>2]=b;Ba(c,1,9304,i);break r}b=b+1|0;if((f|0)!=(b|0)){continue}break}}f=H[j+8>>2];b=H[j+12>>2];e=0;while(1){s:{l=e<<3;m=H[H[a+224>>2]+40>>2]+N(J[l+d>>1],40)|0;h=H[m+16>>2];if(!h){h=Ea(H[m+8>>2],24);H[m+16>>2]=h;if(!h){break s}g=H[a+68>>2];d=H[a+72>>2]}p=h;h=H[m+4>>2];j=p+N(h,24)|0;H[j>>2]=f;H[j+4>>2]=b;l=H[(d+l|0)+4>>2];f=l+f|0;H[j+16>>2]=f;b=f>>>0>>0?b+1|0:b;H[j+20>>2]=b;H[m+4>>2]=h+1;e=e+1|0;if(g>>>0>e>>>0){continue}break p}break}Ba(c,1,6882,0)}H[a+76>>2]=1;if(!H[a+68>>2]){break p}d=H[H[a+224>>2]+40>>2];b=0;while(1){c=N(J[H[a+72>>2]+(b<<3)>>1],40);d=c+d|0;H[d+8>>2]=0;Ca(H[d+16>>2]);d=H[H[a+224>>2]+40>>2];H[(c+d|0)+16>>2]=0;b=b+1|0;if(b>>>0>2]){continue}break}}na=i+16|0;H[a+8>>2]=8;a=1;break a}Ca(H[d+28>>2]);H[d+32>>2]=0;H[d+24>>2]=0;H[d+28>>2]=0;Ba(c,1,3899,0);a=0}na=k+48|0;return a|0}function ge(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0;f=na-160|0;na=f;a:{if(c>>>0<=35){c=0;Ba(d,1,6095,0);break a}j=H[a+96>>2];c=c-36|0;h=(c>>>0)/3|0;if((N(h,3)|0)!=(c|0)){c=0;Ba(d,1,6095,0);break a}c=f+156|0;Da(b,c,2);G[a+104>>1]=H[f+156>>2];Da(b+2|0,j+8|0,4);Da(b+6|0,j+12|0,4);Da(b+10|0,j,4);Da(b+14|0,j+4|0,4);Da(b+18|0,a+116|0,4);Da(b+22|0,a+120|0,4);Da(b+26|0,a+108|0,4);Da(b+30|0,a+112|0,4);Da(b+34|0,c,2);b:{c:{d:{c=H[f+156>>2];if(c>>>0<=16384){H[j+16>>2]=c;if((c|0)!=(h|0)){H[f+132>>2]=h;H[f+128>>2]=c;Ba(d,1,14980,f+128|0);c=0;break a}c=H[j+4>>2];g=H[j+12>>2];l=H[j+8>>2];e=H[j>>2];if(!(c>>>0>>0&l>>>0>e>>>0)){H[f+120>>2]=g-c;H[f+124>>2]=0-(c>>>0>g>>>0);H[f+112>>2]=l-e;H[f+116>>2]=0-(e>>>0>l>>>0);Ba(d,1,14542,f+112|0);c=0;break a}i=H[a+116>>2];k=H[a+120>>2];if(!(k?i:0)){H[f+4>>2]=k;H[f>>2]=i;Ba(d,1,15094,f);c=0;break a}e:{n=H[a+108>>2];f:{if(n>>>0>e>>>0){break f}i=i+n|0;if(e>>>0>=(i>>>0>>0?-1:i)>>>0){break f}i=H[a+112>>2];if(i>>>0>c>>>0){break f}k=i+k|0;if(c>>>0<(i>>>0>k>>>0?-1:k)>>>0){break e}}c=0;Ba(d,1,2792,0);break a}g:{if(H[a+248>>2]){break g}i=H[a+240>>2];if(!i){break g}k=H[a+244>>2];if(!k){break g}e=l-e|0;c=g-c|0;if((e|0)==(i|0)&(c|0)==(k|0)){break g}H[f+108>>2]=c;H[f+104>>2]=e;H[f+100>>2]=k;H[f+96>>2]=i;Ba(d,1,14006,f+96|0);c=0;break a}e=Ea(h,52);H[j+24>>2]=e;if(!e){break d}h:{if(!H[j+16>>2]){break h}c=f+152|0;Da(b+36|0,c,1);h=H[f+152>>2];k=h>>>7|0;H[e+32>>2]=k;n=(h&127)+1|0;H[e+24>>2]=n;l=H[a+248>>2];Da(b+37|0,c,1);H[e>>2]=H[f+152>>2];Da(b+38|0,c,1);g=H[f+152>>2];H[e+4>>2]=g;c=0;i=H[e>>2];if(i-256>>>0<4294967041){h=0;break b}h=0;if(g-256>>>0<4294967041){break b}g=H[e+24>>2];if(g>>>0>31){break c}H[e+36>>2]=0;H[e+40>>2]=H[a+184>>2];h=1;if(K[j+16>>2]<=1){break h}k=l?0:k;l=l?0:n;b=b+39|0;while(1){Da(b,f+152|0,1);i=H[f+152>>2];g=i>>>7|0;H[e+84>>2]=g;i=(i&127)+1|0;H[e+76>>2]=i;if(!(H[a+248>>2]|(I[a+212|0]&4|(i|0)==(l|0)&(g|0)==(k|0)))){H[f+84>>2]=g;H[f+80>>2]=i;H[f+76>>2]=h;H[f+72>>2]=k;H[f+68>>2]=l;H[f+64>>2]=h;Ba(d,2,14778,f- -64|0)}g=f+152|0;Da(b+1|0,g,1);H[e+52>>2]=H[f+152>>2];Da(b+2|0,g,1);g=H[f+152>>2];H[e+56>>2]=g;i=H[e+52>>2];if(i-256>>>0<4294967041|g-256>>>0<=4294967040){break b}g=H[e+76>>2];if(g>>>0>=32){break c}b=b+3|0;H[e+88>>2]=0;H[e+92>>2]=H[a+184>>2];e=e+52|0;h=h+1|0;if(h>>>0>2]){continue}break}}c=0;h=H[a+116>>2];if(!h){break a}g=H[a+120>>2];if(!g){break a}l=0-!h|0;e=l;p=H[a+108>>2];k=H[j+8>>2]-p|0;i=h-1|0;b=k+i|0;e=k>>>0>b>>>0?e+1|0:e;b=ve(b,e,h,0);H[a+128>>2]=b;n=0-!g|0;e=n;q=H[a+112>>2];o=H[j+12>>2]-q|0;m=o;k=g-1|0;o=o+k|0;e=m>>>0>o>>>0?e+1|0:e;e=ve(o,e,g,0);H[a+132>>2]=e;i:{if(!(!b|!e)){if(b>>>0<=65535/(e>>>0)>>>0){break i}}H[f+20>>2]=e;H[f+16>>2]=b;Ba(d,1,14120,f+16|0);break a}o=N(b,e);j:{if(I[a+92|0]&2){H[a+28>>2]=(H[a+28>>2]-p>>>0)/(h>>>0);H[a+32>>2]=(H[a+32>>2]-q>>>0)/(g>>>0);e=l;b=H[a+36>>2]-p|0;m=b;b=b+i|0;e=m>>>0>b>>>0?e+1|0:e;v=a,w=ve(b,e,h,0),H[v+36>>2]=w;e=n;b=H[a+40>>2]-q|0;m=b;b=b+k|0;e=m>>>0>b>>>0?e+1|0:e;v=a,w=ve(b,e,g,0),H[v+40>>2]=w;break j}H[a+40>>2]=e;H[a+36>>2]=b;H[a+28>>2]=0;H[a+32>>2]=0}b=Ea(o,5644);H[a+180>>2]=b;if(!b){Ba(d,1,3935,0);break a}b=Ea(H[j+16>>2],1080);H[H[a+12>>2]+5584>>2]=b;if(!H[H[a+12>>2]+5584>>2]){Ba(d,1,3935,0);break a}b=Ea(10,20);H[H[a+12>>2]+5616>>2]=b;b=H[a+12>>2];if(!H[b+5616>>2]){Ba(d,1,3935,0);break a}H[b+5624>>2]=10;b=Ea(10,20);H[H[a+12>>2]+5628>>2]=b;b=H[a+12>>2];if(!H[b+5628>>2]){Ba(d,1,3935,0);break a}H[b+5636>>2]=10;e=H[j+16>>2];k:{if(!e){break k}h=H[j+24>>2];b=0;if((e|0)!=1){g=e&1;l=e&-2;e=0;while(1){i=h+N(b,52)|0;if(!H[i+32>>2]){H[(H[H[a+12>>2]+5584>>2]+N(b,1080)|0)+1076>>2]=1<>2]-1}i=b|1;k=h+N(i,52)|0;if(!H[k+32>>2]){H[(H[H[a+12>>2]+5584>>2]+N(i,1080)|0)+1076>>2]=1<>2]-1}b=b+2|0;e=e+2|0;if((l|0)!=(e|0)){continue}break}if(!g){break k}}e=h+N(b,52)|0;if(H[e+32>>2]){break k}H[(H[H[a+12>>2]+5584>>2]+N(b,1080)|0)+1076>>2]=1<>2]-1}if(o){b=H[a+180>>2];e=0;while(1){h=Ea(H[j+16>>2],1080);H[b+5584>>2]=h;if(!h){Ba(d,1,3935,0);break a}b=b+5644|0;e=e+1|0;if(o>>>0>e>>>0){continue}break}}b=N(H[a+132>>2],H[a+128>>2]);H[H[a+224>>2]+36>>2]=b;b=Ea(b,40);d=H[a+224>>2];H[d+40>>2]=b;e=0;l:{if(!b){break l}e=1;if(!H[d+36>>2]){break l}d=0;while(1){m:{e=0;g=N(d,40);b=g+b|0;H[b+20>>2]=0;H[b+28>>2]=100;h=Ea(100,24);l=H[a+224>>2];b=H[l+40>>2];H[(g+b|0)+24>>2]=h;if(!h){break m}e=1;d=d+1|0;if(d>>>0>2]){continue}}break}}if(!e){break a}H[a+8>>2]=4;r=H[j+16>>2];if(r){b=H[a+112>>2];d=H[a+120>>2];c=b+N(d,H[a+132>>2]-1|0)|0;d=c+d|0;c=c>>>0>d>>>0?-1:d;d=H[j+12>>2];c=c>>>0>>0?c:d;l=c-1|0;k=0-!c|0;c=H[a+108>>2];d=H[a+116>>2];a=c+N(d,H[a+128>>2]-1|0)|0;d=a+d|0;a=a>>>0>d>>>0?-1:d;d=H[j+8>>2];a=a>>>0>>0?a:d;i=a-1|0;n=0-!a|0;a=H[j+4>>2];b=a>>>0>>0?b:a;o=b-1|0;p=0-!b|0;a=H[j>>2];b=a>>>0>>0?c:a;q=b-1|0;u=0-!b|0;a=H[j+24>>2];b=0;while(1){e=p;d=H[a+4>>2];c=d+o|0;j=ve(c,c>>>0>>0?e+1|0:e,d,0);H[a+20>>2]=j;e=u;h=H[a>>2];c=h+q|0;s=ve(c,c>>>0>>0?e+1|0:e,h,0);H[a+16>>2]=s;c=H[a+40>>2];g=c&31;if((c&63)>>>0>=32){e=-1<>>32-g}g=m^-1;e=e^-1;m=e;e=k;t=d+l|0;e=t>>>0>>0?e+1|0:e;e=ve(t,e,d,0)-j|0;d=m;j=e;e=e+g|0;d=j>>>0>e>>>0?d+1|0:d;j=e;e=c&31;if((c&63)>>>0>=32){d=d>>>e|0}else{d=((1<>>e}H[a+12>>2]=d;e=n;d=h+i|0;e=d>>>0>>0?e+1|0:e;d=ve(d,e,h,0)-s|0;e=m;d=d+g|0;e=d>>>0>>0?e+1|0:e;h=d;d=c&31;if((c&63)>>>0>=32){c=e>>>d|0}else{c=((1<>>d}H[a+8>>2]=c;a=a+52|0;b=b+1|0;if((r|0)!=(b|0)){continue}break}}c=1;break a}H[f+144>>2]=c;Ba(d,1,7932,f+144|0);c=0;break a}c=0;H[j+16>>2]=0;Ba(d,1,3935,0);break a}H[f+52>>2]=g;H[f+48>>2]=h;Ba(d,1,15402,f+48|0);break a}H[f+40>>2]=g;H[f+36>>2]=i;H[f+32>>2]=h;Ba(d,1,14340,f+32|0)}na=f+160|0;return c|0}function fd(a,b,c,d,e,f){a=a|0;b=+b;c=c|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,t=0,u=0,v=0,w=0,y=0,z=0,A=0,B=0,C=0;n=na-560|0;na=n;H[n+44>>2]=0;x(+b);i=s(1)|0;s(0)|0;a:{if((i|0)<0){r=1;y=1082;b=-b;x(+b);i=s(1)|0;s(0)|0;break a}if(e&2048){r=1;y=1085;break a}r=e&1;y=r?1088:1083;A=!r}b:{if((i&2146435072)==2146435072){d=r+3|0;Na(a,32,c,d,e&-65537);La(a,y,r);f=f&32;La(a,b!=b?f?1207:1435:f?1312:1476,3);Na(a,32,c,d,e^8192);l=(c|0)>(d|0)?c:d;break b}v=n+16|0;c:{d:{e:{b=pc(b,n+44|0);b=b+b;if(b!=0){g=H[n+44>>2];H[n+44>>2]=g-1;t=f|32;if((t|0)!=97){break e}break c}t=f|32;if((t|0)==97){break c}m=H[n+44>>2];break d}m=g-29|0;H[n+44>>2]=m;b=b*268435456}k=(d|0)<0?6:d;p=(n+48|0)+((m|0)>=0?288:0)|0;h=p;while(1){d=b<4294967295&b>=0?~~b>>>0:0;H[h>>2]=d;h=h+4|0;b=(b-+(d>>>0))*1e9;if(b!=0){continue}break}f:{if((m|0)<=0){i=m;g=h;j=p;break f}j=p;i=m;while(1){z=i>>>0>=29?29:i;g=h-4|0;g:{if(j>>>0>g>>>0){break g}d=0;i=0;while(1){w=H[g>>2];o=z&31;if((z&63)>>>0>=32){l=w<>>32-o;w=w<>>0>o>>>0?i+1|0:i;d=ve(o,l,1e9,0);i=qa;B=g,C=o+re(d,i,-1e9)|0,H[B>>2]=C;g=g-4|0;if(j>>>0<=g>>>0){continue}break}if(!l&o>>>0<1e9){break g}j=j-4|0;H[j>>2]=d}while(1){g=h;if(j>>>0>>0){h=g-4|0;if(!H[h>>2]){continue}}break}i=H[n+44>>2]-z|0;H[n+44>>2]=i;h=g;if((i|0)>0){continue}break}}if((i|0)<0){u=((k+25>>>0)/9|0)+1|0;l=(t|0)==102;while(1){d=0-i|0;d=d>>>0>=9?9:d;h:{if(g>>>0<=j>>>0){h=H[j>>2]?0:4;break h}o=1e9>>>d|0;z=-1<>2];H[h>>2]=w+(i>>>d|0);i=N(o,i&z);h=h+4|0;if(h>>>0>>0){continue}break}h=H[j>>2]?0:4;if(!i){break h}H[g>>2]=i;g=g+4|0}i=d+H[n+44>>2]|0;H[n+44>>2]=i;j=h+j|0;d=l?p:j;g=g-d>>2>(u|0)?d+(u<<2)|0:g;if((i|0)<0){continue}break}}i=0;i:{if(g>>>0<=j>>>0){break i}i=N(p-j>>2,9);h=10;d=H[j>>2];if(d>>>0<10){break i}while(1){i=i+1|0;h=N(h,10);if(d>>>0>=h>>>0){continue}break}}d=(k-((t|0)!=102?i:0)|0)-((t|0)==103&(k|0)!=0)|0;if((d|0)<(N(g-p>>2,9)-9|0)){h=(n+48|0)+((m|0)<0?-4092:-3804)|0;d=d+9216|0;m=(d|0)/9|0;l=h+(m<<2)|0;h=10;d=d+N(m,-9)|0;if((d|0)<=7){while(1){h=N(h,10);d=d+1|0;if((d|0)!=8){continue}break}}m=H[l>>2];u=(m>>>0)/(h>>>0)|0;d=N(u,h);o=l+4|0;j:{if((d|0)==(m|0)&(o|0)==(g|0)){break j}m=m-d|0;k:{if(!(u&1)){b=9007199254740992;if(!(F[l-4|0]&1)|((h|0)!=1e9|j>>>0>=l>>>0)){break k}}b=9007199254740994}q=(g|0)==(o|0)?1:1.5;o=h>>>1|0;q=m>>>0>>0?.5:(o|0)==(m|0)?q:1.5;if(!(I[y|0]!=45|A)){q=-q;b=-b}H[l>>2]=d;if(b+q==b){break j}d=d+h|0;H[l>>2]=d;if(d>>>0>=1e9){while(1){H[l>>2]=0;l=l-4|0;if(l>>>0>>0){j=j-4|0;H[j>>2]=0}d=H[l>>2]+1|0;H[l>>2]=d;if(d>>>0>999999999){continue}break}}i=N(p-j>>2,9);h=10;d=H[j>>2];if(d>>>0<10){break j}while(1){i=i+1|0;h=N(h,10);if(d>>>0>=h>>>0){continue}break}}d=l+4|0;g=d>>>0>>0?d:g}while(1){h=g;m=g>>>0<=j>>>0;if(!m){g=g-4|0;if(!H[g>>2]){continue}}break}l:{if((t|0)!=103){d=e&8;break l}d=k?k:1;g=(d|0)>(i|0)&(i|0)>-5;k=(g?i^-1:-1)+d|0;f=(g?-1:-2)+f|0;d=e&8;if(d){break l}g=-9;m:{if(m){break m}l=H[h-4>>2];if(!l){break m}d=10;g=0;if((l>>>0)%10|0){break m}while(1){m=g;g=g+1|0;d=N(d,10);if(!((l>>>0)%(d>>>0)|0)){continue}break}g=m^-1}m=N(h-p>>2,9);if((f&-33)==70){d=0;g=(g+m|0)-9|0;g=(g|0)>0?g:0;k=(g|0)>(k|0)?k:g;break l}d=0;g=((i+m|0)+g|0)-9|0;g=(g|0)>0?g:0;k=(g|0)>(k|0)?k:g}l=-1;t=d|k;if(((t?2147483645:2147483646)|0)<(k|0)){break b}m=(((t|0)!=0)+k|0)+1|0;o=f&-33;n:{if((o|0)==70){if((m^2147483647)<(i|0)){break b}g=(i|0)>0?i:0;break n}g=i>>31;g=$a((g^i)-g|0,0,v);if((v-g|0)<=1){while(1){g=g-1|0;F[g|0]=48;if((v-g|0)<2){continue}break}}u=g-2|0;F[u|0]=f;F[g-1|0]=(i|0)<0?45:43;g=v-u|0;if((g|0)>(m^2147483647)){break b}}f=g+m|0;if((f|0)>(r^2147483647)){break b}f=f+r|0;Na(a,32,c,f,e);La(a,y,r);Na(a,48,c,f,e^65536);o:{p:{q:{if((o|0)==70){d=n+16|9;i=j>>>0>p>>>0?p:j;j=i;while(1){g=$a(H[j>>2],0,d);r:{if((i|0)!=(j|0)){if(n+16>>>0>=g>>>0){break r}while(1){g=g-1|0;F[g|0]=48;if(n+16>>>0>>0){continue}break}break r}if((d|0)!=(g|0)){break r}g=g-1|0;F[g|0]=48}La(a,g,d-g|0);j=j+4|0;if(p>>>0>=j>>>0){continue}break}if(t){La(a,1684,1)}if((k|0)<=0|h>>>0<=j>>>0){break q}while(1){g=$a(H[j>>2],0,d);if(g>>>0>n+16>>>0){while(1){g=g-1|0;F[g|0]=48;if(n+16>>>0>>0){continue}break}}La(a,g,(k|0)>=9?9:k);g=k-9|0;j=j+4|0;if(h>>>0<=j>>>0){break p}i=(k|0)>9;k=g;if(i){continue}break}break p}s:{if((k|0)<0){break s}p=h>>>0>j>>>0?h:j+4|0;i=n+16|9;h=j;while(1){g=$a(H[h>>2],0,i);if((i|0)==(g|0)){g=g-1|0;F[g|0]=48}t:{if((h|0)!=(j|0)){if(n+16>>>0>=g>>>0){break t}while(1){g=g-1|0;F[g|0]=48;if(n+16>>>0>>0){continue}break}break t}La(a,g,1);g=g+1|0;if(!(d|k)){break t}La(a,1684,1)}m=g;g=i-g|0;La(a,m,(g|0)<(k|0)?g:k);k=k-g|0;h=h+4|0;if(p>>>0<=h>>>0){break s}if((k|0)>=0){continue}break}}Na(a,48,k+18|0,18,0);La(a,u,v-u|0);break o}g=k}Na(a,48,g+9|0,9,0)}Na(a,32,c,f,e^8192);l=(c|0)>(f|0)?c:f;break b}j=(f<<26>>31&9)+y|0;u:{if(d>>>0>11){break u}g=12-d|0;q=16;while(1){q=q*16;g=g-1|0;if(g){continue}break}if(I[j|0]==45){b=-(q+(-b-q));break u}b=b+q-q}h=H[n+44>>2];g=h>>31;g=$a((g^h)-g|0,0,v);if((v|0)==(g|0)){g=g-1|0;F[g|0]=48;h=H[n+44>>2]}k=f&32;i=g-2|0;F[i|0]=f+15;F[g-1|0]=(h|0)<0?45:43;g=!(e&8)&(d|0)<=0;h=n+16|0;while(1){f=h;p=P(b)<2147483647?~~b:-2147483648;F[h|0]=k|I[p+26048|0];b=(b-+(p|0))*16;h=h+1|0;if(!(g&b==0|(h-(n+16|0)|0)!=1)){F[f+1|0]=46;h=f+2|0}if(b!=0){continue}break}l=-1;g=v-i|0;if((2147483643-(g+r|0)|0)<(d|0)){break b}f=h;h=n+16|0;f=f-h|0;k=d?(f-2|0)<(d|0)?d+2|0:f:f;p=r|2;d=k+(p+g|0)|0;Na(a,32,c,d,e);La(a,j,p);Na(a,48,c,d,e^65536);La(a,h,f);Na(a,48,k-f|0,0,0);La(a,i,g);Na(a,32,c,d,e^8192);l=(c|0)>(d|0)?c:d}na=n+560|0;return l|0}function sc(a,b,c,d,e,f,g){var h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0;j=na+-64|0;na=j;H[j+60>>2]=b;z=j+41|0;A=j+39|0;t=j+40|0;a:{b:{c:{d:{e:while(1){h=0;f:while(1){k=b;if((o^2147483647)<(h|0)){break d}o=h+o|0;g:{h:{i:{j:{h=b;i=I[h|0];if(i){while(1){k:{b=i&255;l:{if(!b){b=h;break l}if((b|0)!=37){break k}i=h;while(1){if(I[i+1|0]!=37){b=i;break l}h=h+1|0;n=I[i+2|0];b=i+2|0;i=b;if((n|0)==37){continue}break}}h=h-k|0;y=o^2147483647;if((h|0)>(y|0)){break d}if(a){La(a,k,h)}if(h){continue f}H[j+60>>2]=b;h=b+1|0;q=-1;i=F[b+1|0]-48|0;if(!(I[b+2|0]!=36|i>>>0>9)){w=1;q=i;h=b+3|0}H[j+60>>2]=h;l=0;i=F[h|0];b=i-32|0;m:{if(b>>>0>31){n=h;break m}n=h;b=1<>2]=n;l=b|l;i=F[h+1|0];b=i-32|0;if(b>>>0>=32){break m}h=n;b=1<>>0>9)){p:{if(!a){H[(b<<2)+e>>2]=10;b=0;break p}b=H[(b<<3)+d>>2]}p=b;b=n+3|0;i=1;break o}if(w){break j}b=n+1|0;if(!a){H[j+60>>2]=b;w=0;p=0;break n}h=H[c>>2];H[c>>2]=h+4;p=H[h>>2];i=0}w=i;H[j+60>>2]=b;if((p|0)>=0){break n}p=0-p|0;l=l|8192;break n}p=rc(j+60|0);if((p|0)<0){break d}b=H[j+60>>2]}h=0;m=-1;u=0;q:{if(I[b|0]!=46){break q}if(I[b+1|0]==42){i=F[b+2|0]-48|0;r:{if(!(I[b+3|0]!=36|i>>>0>9)){b=b+4|0;s:{if(!a){H[(i<<2)+e>>2]=10;m=0;break s}m=H[(i<<3)+d>>2]}break r}if(w){break j}b=b+2|0;m=0;if(!a){break r}i=H[c>>2];H[c>>2]=i+4;m=H[i>>2]}H[j+60>>2]=b;u=(m|0)>=0;break q}H[j+60>>2]=b+1;m=rc(j+60|0);b=H[j+60>>2];u=1}while(1){x=h;n=28;r=b;i=F[b|0];if(i-123>>>0<4294967238){break c}b=b+1|0;h=I[(i+N(h,58)|0)+25519|0];if((h-1&255)>>>0<8){continue}break}H[j+60>>2]=b;t:{if((h|0)!=27){if(!h){break c}if((q|0)>=0){if(!a){H[(q<<2)+e>>2]=h;continue e}h=(q<<3)+d|0;i=H[h+4>>2];H[j+48>>2]=H[h>>2];H[j+52>>2]=i;break t}if(!a){break g}qc(j+48|0,h,c,g);break t}if((q|0)>=0){break c}h=0;if(!a){continue f}}if(I[a|0]&32){break b}i=l&-65537;l=l&8192?i:l;q=0;v=1072;n=t;u:{v:{w:{x:{y:{z:{A:{B:{C:{D:{E:{F:{G:{H:{I:{J:{K:{r=I[r|0];h=r<<24>>24;h=x?(r&15)==3?h&-45:h:h;switch(h-88|0){case 0:case 32:break G;case 1:case 2:case 3:case 4:case 5:case 6:case 7:case 8:case 10:case 16:case 18:case 19:case 20:case 21:case 25:case 26:case 28:case 30:case 31:break h;case 9:case 13:case 14:case 15:break u;case 11:break B;case 12:case 17:break E;case 22:break I;case 23:break F;case 24:break H;case 27:break A;case 29:break J;default:break K}}L:{switch(h-65|0){case 1:case 3:break h;case 0:case 4:case 5:case 6:break u;case 2:break z;default:break L}}if((h|0)==83){break y}break h}i=H[j+48>>2];r=H[j+52>>2];v=1072;break D}h=0;M:{switch(x|0){case 0:H[H[j+48>>2]>>2]=o;continue f;case 1:H[H[j+48>>2]>>2]=o;continue f;case 2:k=H[j+48>>2];H[k>>2]=o;H[k+4>>2]=o>>31;continue f;case 3:G[H[j+48>>2]>>1]=o;continue f;case 4:F[H[j+48>>2]]=o;continue f;case 6:H[H[j+48>>2]>>2]=o;continue f;case 7:break M;default:continue f}}k=H[j+48>>2];H[k>>2]=o;H[k+4>>2]=o>>31;continue f}m=m>>>0<=8?8:m;l=l|8;h=120}b=t;k=H[j+52>>2];r=k;i=H[j+48>>2];s=i;if(i|k){x=h&32;while(1){b=b-1|0;F[b|0]=x|I[(s&15)+26048|0];s=(k&15)<<28|s>>>4;k=k>>>4|0;if(s|k){continue}break}}k=b;if(!(l&8)|!(i|r)){break C}v=(h>>>4|0)+1072|0;q=2;break C}b=t;k=H[j+52>>2];r=k;i=H[j+48>>2];s=i;if(i|k){while(1){b=b-1|0;F[b|0]=s&7|48;s=(k&7)<<29|s>>>3;k=k>>>3|0;if(s|k){continue}break}}k=b;if(!(l&8)){break C}b=z-b|0;m=(b|0)<(m|0)?m:b;break C}i=H[j+48>>2];b=H[j+52>>2];r=b;if((b|0)<0){h=0-(b+((i|0)!=0)|0)|0;r=h;i=0-i|0;H[j+48>>2]=i;H[j+52>>2]=h;q=1;v=1072;break D}if(l&2048){q=1;v=1073;break D}q=l&1;v=q?1074:1072}k=$a(i,r,t)}if((m|0)<0&u){break d}l=u?l&-65537:l;if(!((i|r)!=0|m)){k=t;m=0;break h}b=!(i|r)+(t-k|0)|0;m=(b|0)<(m|0)?m:b;break h}h=I[j+48|0];break i}h=m>>>0>=2147483647?2147483647:m;l=h;n=(h|0)!=0;b=H[j+48>>2];k=b?b:1686;b=k;N:{O:{P:{Q:{if(!(b&3)|!h){break Q}while(1){if(!I[b|0]){break P}l=l-1|0;n=(l|0)!=0;b=b+1|0;if(!(b&3)){break Q}if(l){continue}break}}if(!n){break O}if(!(!I[b|0]|l>>>0<4)){while(1){n=H[b>>2];if(((16843008-n|n)&-2139062144)!=-2139062144){break P}b=b+4|0;l=l-4|0;if(l>>>0>3){continue}break}}if(!l){break O}}while(1){if(!I[b|0]){break N}b=b+1|0;l=l-1|0;if(l){continue}break}}b=0}b=b?b-k|0:h;n=b+k|0;if((m|0)>=0){l=i;m=b;break h}l=i;m=b;if(I[n|0]){break d}break h}h=H[j+48>>2];if(h|H[j+52>>2]){break x}h=0;break i}if(m){i=H[j+48>>2];break w}h=0;Na(a,32,p,0,l);break v}H[j+12>>2]=0;H[j+8>>2]=h;i=j+8|0;H[j+48>>2]=i;m=-1}h=0;while(1){R:{k=H[i>>2];if(!k){break R}k=oc(j+4|0,k);if((k|0)<0){break b}if(k>>>0>m-h>>>0){break R}i=i+4|0;h=h+k|0;if(m>>>0>h>>>0){continue}}break}n=61;if((h|0)<0){break c}Na(a,32,p,h,l);if(!h){h=0;break v}n=0;i=H[j+48>>2];while(1){k=H[i>>2];if(!k){break v}m=j+4|0;k=oc(m,k);n=k+n|0;if(n>>>0>h>>>0){break v}La(a,m,k);i=i+4|0;if(h>>>0>n>>>0){continue}break}}Na(a,32,p,h,l^8192);h=(h|0)<(p|0)?p:h;continue f}if((m|0)<0&u){break d}n=61;h=ra[f|0](a,M[j+48>>3],p,m,l,h)|0;if((h|0)>=0){continue f}break c}i=I[h+1|0];h=h+1|0;continue}}if(a){break a}if(!w){break g}h=1;while(1){a=H[(h<<2)+e>>2];if(a){qc((h<<3)+d|0,a,c,g);o=1;h=h+1|0;if((h|0)!=10){continue}break a}break}if(h>>>0>=10){o=1;break a}while(1){if(H[(h<<2)+e>>2]){break j}o=1;h=h+1|0;if((h|0)!=10){continue}break}break a}n=28;break c}F[j+39|0]=h;m=1;k=A;l=i}i=n-k|0;m=(i|0)<(m|0)?m:i;if((m|0)>(q^2147483647)){break d}n=61;b=m+q|0;h=(b|0)<(p|0)?p:b;if(y>>>0>>0){break c}Na(a,32,h,b,l);La(a,v,q);Na(a,48,h,b,l^65536);Na(a,48,m,i,0);La(a,k,i);Na(a,32,h,b,l^8192);b=H[j+60>>2];continue}break}break}o=0;break a}n=61}H[6597]=n}o=-1}na=j- -64|0;return o}function Pc(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;i=na-80|0;na=i;H[i+76>>2]=1;a:{b:{if(H[a+128>>2]!=1|H[a+132>>2]!=1|(H[a+108>>2]|H[a+112>>2])){break b}j=H[a+100>>2];if(H[j>>2]|H[j+4>>2]|(H[j+8>>2]!=H[a+116>>2]|H[j+12>>2]!=H[a+120>>2])){break b}if(!cb(a,i+72|0,0,i+68|0,i- -64|0,i+60|0,i+56|0,i+52|0,i+76|0,b,c)){break a}c:{d:{if(!H[i+76>>2]){break d}if(!ib(a,H[i+72>>2],0,0,b,c)){break d}b=H[a+100>>2];if(H[b+16>>2]){break c}d=1;break a}Ba(c,1,8776,0);break a}e=H[b+24>>2];while(1){b=N(g,52);Ca(H[(b+e|0)+44>>2]);c=H[a+100>>2];e=H[c+24>>2];j=b+e|0;m=H[a+232>>2];d=H[H[H[m+20>>2]>>2]+20>>2]+N(g,76)|0;H[j+44>>2]=H[d+36>>2];H[j+36>>2]=H[(b+H[H[m+24>>2]+24>>2]|0)+36>>2];H[d+36>>2]=0;d=1;g=g+1|0;if(g>>>0>2]){continue}break}break a}H[a+80>>2]=0;H[a+84>>2]=0;Ca(H[a+88>>2]);H[a+88>>2]=0;e:{if(!(H[a+28>>2]|H[a+32>>2]|H[a+36>>2]!=H[a+128>>2])){j=2;if(H[a+40>>2]==H[a+132>>2]){break e}}j=2;if(H[a+76>>2]){break e}if(!Ab(b)){break e}q=H[a+128>>2];j=N(q,H[a+132>>2]);if(j){h=H[H[a+224>>2]+40>>2];f:{g:{if((j|0)==1){j=0;break g}k=j&1;n=j&-2;j=0;while(1){d=h+N(g,40)|0;f=H[d+4>>2];if(f){l=(H[d+16>>2]+N(f,24)|0)-8|0;f=H[l>>2];o=f;p=f>>>0>j>>>0;f=H[l+4>>2];l=p&(f|0)>=(m|0)|(f|0)>(m|0);j=l?o:j;m=l?f:m}f=H[d+44>>2];if(f){f=(H[d+56>>2]+N(f,24)|0)-8|0;d=H[f>>2];o=d;l=d>>>0>j>>>0;d=H[f+4>>2];f=l&(d|0)>=(m|0)|(d|0)>(m|0);j=f?o:j;m=f?d:m}g=g+2|0;e=e+2|0;if((n|0)!=(e|0)){continue}break}if(!k){break f}}d=h+N(g,40)|0;h=H[d+4>>2];if(!h){break f}h=(H[d+16>>2]+N(h,24)|0)-8|0;d=H[h>>2];f=d;k=d>>>0>j>>>0;d=H[h+4>>2];h=k&(d|0)>=(m|0)|(d|0)>(m|0);j=h?f:j;m=h?d:m}j=j+2|0;m=j>>>0<2?m+1|0:m}else{j=2;m=0}g=0;k=H[a+32>>2];p=H[a+40>>2];h:{if(k>>>0>=p>>>0){break h}h=H[a+28>>2];f=H[a+36>>2];if(h>>>0>=f>>>0){break h}n=f-h&3;s=H[H[a+224>>2]+40>>2];t=h-f>>>0>4294967292;while(1){l=s+N(N(k,q),40)|0;d=h;e=0;if(n){while(1){g=H[(l+N(d,40)|0)+4>>2]+g|0;d=d+1|0;e=e+1|0;if((n|0)!=(e|0)){continue}break}}if(!t){while(1){e=l+N(d,40)|0;g=H[e+124>>2]+(H[e+84>>2]+(H[e+44>>2]+(H[e+4>>2]+g|0)|0)|0)|0;d=d+4|0;if((f|0)!=(d|0)){continue}break}}k=k+1|0;if((p|0)!=(k|0)){continue}break}}f=Fa(g<<3);H[a+88>>2]=f;if(!g|!f){break e}g=0;d=H[a+40>>2];n=H[a+32>>2];i:{if(d>>>0<=n>>>0){break i}e=H[a+36>>2];if(e>>>0<=K[a+28>>2]){break i}while(1){f=H[a+28>>2];if(e>>>0>f>>>0){s=H[H[a+224>>2]+40>>2]+N(N(H[a+128>>2],n),40)|0;while(1){h=s+N(f,40)|0;d=H[h+4>>2];if(d){q=d&3;p=H[h+16>>2];h=0;j:{k:{if(d>>>0<4){d=0;break k}t=d&-4;d=0;k=0;while(1){e=p+N(d,24)|0;r=H[e+4>>2];l=g<<3;o=l+H[a+88>>2]|0;H[o>>2]=H[e>>2];H[o+4>>2]=r;r=H[e+28>>2];o=l+H[a+88>>2]|0;H[o+8>>2]=H[e+24>>2];H[o+12>>2]=r;r=H[e+52>>2];o=l+H[a+88>>2]|0;H[o+16>>2]=H[e+48>>2];H[o+20>>2]=r;o=H[e+76>>2];l=l+H[a+88>>2]|0;H[l+24>>2]=H[e+72>>2];H[l+28>>2]=o;d=d+4|0;g=g+4|0;k=k+4|0;if((t|0)!=(k|0)){continue}break}if(!q){break j}}while(1){k=p+N(d,24)|0;l=H[k+4>>2];e=H[a+88>>2]+(g<<3)|0;H[e>>2]=H[k>>2];H[e+4>>2]=l;d=d+1|0;g=g+1|0;h=h+1|0;if((q|0)!=(h|0)){continue}break}}e=H[a+36>>2]}f=f+1|0;if(e>>>0>f>>>0){continue}break}d=H[a+40>>2]}n=n+1|0;if(n>>>0>>0){continue}break}f=H[a+88>>2]}H[a+84>>2]=g;e=na-208|0;na=e;H[e+8>>2]=1;H[e+12>>2]=0;h=g<<3;l:{if(!h){break l}H[e+16>>2]=8;H[e+20>>2]=8;d=2;while(1){g=(e+16|0)+(d<<2)|0;k=H[g-4>>2]+(H[g-8>>2]+8|0)|0;H[g>>2]=k;d=d+1|0;if(h>>>0>k>>>0){continue}break}d=h-8|0;m:{if((d|0)<=0){g=0;h=1;d=1;k=0;break m}g=d+f|0;h=1;d=1;while(1){n:{if((h&3)==3){Bb(f,d,e+16|0);ub(e+8|0,2);d=d+2|0;break n}n=e+16|0;k=d-1|0;o:{if(K[n+(k<<2)>>2]>=g-f>>>0){tb(f,h,H[e+12>>2],d,0,n);break o}Bb(f,d,e+16|0)}if((d|0)==1){sb(e+8|0,1);d=0;break n}sb(e+8|0,k);d=1}h=H[e+8>>2]|1;H[e+8>>2]=h;f=f+8|0;if(g>>>0>f>>>0){continue}break}g=H[e+12>>2];k=(g|0)!=0}tb(f,h,g,d,0,e+16|0);h=H[e+8>>2];if(!(k|((d|0)!=1|(h|0)!=1))){break l}while(1){p:{if((d|0)<=1){h=wc(h,g);ub(e+8|0,h);d=d+h|0;break p}g=e+8|0;sb(g,2);H[e+8>>2]=H[e+8>>2]^7;ub(g,1);n=f-8|0;k=e+16|0;h=d-2|0;tb(n-H[k+(h<<2)>>2]|0,H[e+8>>2],H[e+12>>2],d-1|0,1,k);sb(g,1);d=H[e+8>>2]|1;H[e+8>>2]=d;tb(n,d,H[e+12>>2],h,1,k);d=h}f=f-8|0;g=H[e+12>>2];h=H[e+8>>2];if(g|((d|0)!=1|(h|0)!=1)){continue}break}}na=e+208|0}d=H[a+128>>2];e=0;q:{while(1){r:{if(!(!H[H[a+180>>2]+5596>>2]|((d|0)!=1|H[a+132>>2]!=1))){H[i+72>>2]=0;H[a+228>>2]=0;H[a+8>>2]=H[a+8>>2]|128;d=0;break r}d=0;if(!cb(a,i+72|0,0,i+68|0,i- -64|0,i+60|0,i+56|0,i+52|0,i+76|0,b,c)){break a}if(!H[i+76>>2]){break q}d=H[i+72>>2]}h=d+1|0;f=ib(a,d,0,0,b,c);g=N(H[a+128>>2],H[a+132>>2]);if(!f){H[i+4>>2]=g;H[i>>2]=h;Ba(c,1,7537,i);d=0;break a}H[i+36>>2]=g;H[i+32>>2]=h;Ba(c,4,11795,i+32|0);if(!Hc(H[a+232>>2],H[H[a+100>>2]+24>>2])){d=0;break a}s:{if(!(H[a+128>>2]!=1|H[a+132>>2]!=1)){g=H[a+100>>2];f=H[a+96>>2];if(H[g>>2]!=H[f>>2]|H[g+4>>2]!=H[f+4>>2]|(H[g+8>>2]!=H[f+8>>2]|H[g+12>>2]!=H[f+12>>2])){break s}}d=H[a+180>>2]+N(d,5644)|0;g=H[d+5596>>2];if(!g){break s}Ca(g);H[d+5596>>2]=0;H[d+5600>>2]=0}H[i+16>>2]=h;Ba(c,4,16601,i+16|0);if(!(Qa(b)|qa)&H[a+8>>2]==64){break q}e=e+1|0;d=H[a+128>>2];if((e|0)==(N(d,H[a+132>>2])|0)){break q}h=H[a+84>>2];if(!h|(h|0)!=H[a+80>>2]){continue}break}nc(b,j,m,c)}d=Gc(a,c)}na=i+80|0;return d|0}function Ya(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;d=H[a+32>>2];a:{if(d){break a}b:{h=H[a+16>>2];if((h|0)>=6){b=H[a+8>>2];f=H[a+12>>2];d=h;break b}b=H[a+20>>2];c:{d:{if((b|0)>=5){c=H[a>>2];d=H[c>>2];H[a>>2]=c+4;g=b-4|0;break d}if((b|0)<=0){d=-1;break c}c=H[a>>2];e:{if((b|0)==1){e=-1;b=0;break e}d=0;f:{g:{if((b|0)==2){e=-1;i=b;break g}f=b-1|0;k=f&1;j=f&-2;e=-1;i=b;while(1){f=c;H[a>>2]=c+1;l=I[c|0];c=c+2|0;H[a>>2]=c;H[a+20>>2]=i-1;f=I[f+1|0];i=i-2|0;H[a+20>>2]=i;e=((255<>2]=f;c=I[c|0];H[a+20>>2]=i-1;e=(255<>2]=c+1;d=(255<>2]=g}b=H[a+24>>2];c=d>>>24|0;H[a+24>>2]=(c|0)==255;g=d>>>16&255;k=(g|0)==255;f=d&255;e=(f|0)==255;j=b+e|0;b=d>>>8&255;i=(b|0)==255;j=k+(j+i|0)|0;d=(h-j|0)+32|0;H[a+16>>2]=d;l=H[a+12>>2];b=c|(g|(b|f<<(e?7:8))<<(i?7:8))<<(k?7:8);f=(j-h|0)+32|0;c=f&31;if((f&63)>>>0>=32){i=b<>>32-c;g=b<>2];c=i|l;f=c;H[a+8>>2]=b;H[a+12>>2]=c;if((d|0)>=6){break b}d=0;break a}e=H[a+28>>2];i=H[(e<<2)+20752>>2];h:{if((f|0)<0){d=d-1|0;c=(-1<=11?11:e)+1|0;break h}g=b;h=63-i|0;c=h&31;if((h&63)>>>0>=32){g=f>>>c|0}else{g=((1<>>c}c=(g&(-1<>2]=d;H[a+28>>2]=e;g=b;h=i&31;if((i&63)>>>0>=32){b=b<>>32-h|f<>2]=g;H[a+12>>2]=b;i=H[a+44>>2]|c>>31;j=H[a+40>>2]&-64|c;H[a+40>>2]=j;H[a+44>>2]=i;if((d|0)<6){d=1;break a}b=H[(e<<2)+20752>>2];i:{if((f|0)<0){d=d-1|0;c=(-1<=11?11:e)+1|0;break i}k=g;h=63-b|0;c=h&31;if((h&63)>>>0>=32){k=f>>>c|0}else{k=((1<>>c}c=(k&(-1<>2]=d;H[a+28>>2]=e;k=g;h=b&31;if((b&63)>>>0>=32){b=g<>>32-h|f<>2]=k;H[a+12>>2]=b;b=c>>31<<7|c>>>25|i;h=b;j=j&-8065|c<<7;H[a+40>>2]=j;H[a+44>>2]=b;if((d|0)<6){d=2;break a}b=H[(e<<2)+20752>>2];j:{if((f|0)<0){d=d-1|0;c=(-1<=11?11:e)+1|0;break j}g=k;i=63-b|0;c=i&31;if((i&63)>>>0>=32){g=f>>>c|0}else{g=((1<>>c}c=(g&(-1<>2]=d;H[a+28>>2]=e;l=k;g=b&31;if((b&63)>>>0>=32){i=k<>>32-g|f<>2]=g;f=i;H[a+12>>2]=f;b=c>>31<<14|c>>>18|h;i=b;k=j&-1032193|c<<14;H[a+40>>2]=k;H[a+44>>2]=b;if((d|0)<6){d=3;break a}b=H[(e<<2)+20752>>2];k:{if((f|0)<0){d=d-1|0;c=(-1<=11?11:e)+1|0;break k}j=g;h=63-b|0;c=h&31;if((h&63)>>>0>=32){j=f>>>c|0}else{j=((1<>>c}c=(j&(-1<>2]=d;H[a+28>>2]=e;j=g;h=b&31;if((b&63)>>>0>=32){b=g<>>32-h|f<>2]=g;H[a+12>>2]=b;b=c>>31<<21|c>>>11|i;j=b;k=k&-132120577|c<<21;H[a+40>>2]=k;H[a+44>>2]=b;if((d|0)<6){d=4;break a}b=H[(e<<2)+20752>>2];l:{if((f|0)<0){c=(-1<=11?11:e)+1|0;d=d-1|0;break l}h=g;i=63-b|0;c=i&31;if((i&63)>>>0>=32){i=f>>>c|0}else{i=((1<>>c}c=(i&(-1<>2]=d;H[a+28>>2]=h;i=g;e=b&31;if((b&63)>>>0>=32){b=g<>>32-e|f<>2]=g;f=b;H[a+12>>2]=b;b=j&-4|(c>>31<<28|c>>>4);j=b;k=k&268435455|c<<28;H[a+40>>2]=k;H[a+44>>2]=b;if((d|0)<6){d=5;break a}b=H[(h<<2)+20752>>2];m:{if((f|0)<0){e=(-1<=11?11:h)+1|0;i=d-1|0;break m}i=g;e=63-b|0;c=e&31;if((e&63)>>>0>=32){i=f>>>c|0}else{i=((1<>>c}e=(i&(-1<>2]=i;H[a+28>>2]=h;d=g;c=b&31;if((b&63)>>>0>=32){b=d<>>32-c|f<>2]=g;H[a+12>>2]=b;b=j&-505|e<<3;l=b;H[a+40>>2]=k;H[a+44>>2]=b;d=6;if((i|0)<6){break a}b=H[(h<<2)+20752>>2];n:{if((c|0)<0){e=(-1<=11?11:h)+1|0;d=i-1|0;break n}d=g;e=63-b|0;f=e&31;if((e&63)>>>0>=32){f=c>>>f|0}else{f=((1<>>f}e=(f&(-1<>2]=d;H[a+28>>2]=h;j=g;f=b&31;if((b&63)>>>0>=32){i=g<>>32-f|c<>2]=g;f=i;H[a+12>>2]=f;i=k;b=l&-64513|e<<10;k=b;H[a+40>>2]=i;H[a+44>>2]=b;if((d|0)<6){d=7;break a}b=H[(h<<2)+20752>>2];o:{if((f|0)<0){d=d-1|0;c=(-1<=11?11:h)+1|0;break o}j=g;e=63-b|0;c=e&31;if((e&63)>>>0>=32){j=f>>>c|0}else{j=((1<>>c}c=(j&(-1<>2]=d;H[a+28>>2]=e;d=g;e=b&31;if((b&63)>>>0>=32){b=d<>>32-e|f<>2]=g;H[a+12>>2]=b;H[a+40>>2]=i;H[a+44>>2]=k&-8257537|c<<17;d=8}H[a+32>>2]=d-1;f=H[a+44>>2];b=f>>>7|0;c=H[a+40>>2];H[a+40>>2]=(f&127)<<25|c>>>7;H[a+44>>2]=b;return c&127}function Ub(a,b,c,d,e,f,g,h,i){var j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,z=0,A=0;p=na-32|0;na=p;H[p+24>>2]=f;r=H[(N(H[d+28>>2],76)+b|0)+28>>2]+N(H[d+32>>2],152)|0;a:{if(!(H[d+40>>2]|!H[r+24>>2])){k=r+28|0;while(1){b:{if(Qb(k)){break b}b=H[d+36>>2];if(b>>>0>=K[k+24>>2]/40>>>0){Ba(i,1,2836,0);break a}b=H[k+20>>2]+N(b,40)|0;fc(H[b+32>>2]);fc(H[b+36>>2]);o=N(H[b+20>>2],H[b+16>>2]);if(!o){break b}q=o&7;b=H[b+24>>2];if(o>>>0>=8){o=o&-8;j=0;while(1){H[b+516>>2]=0;H[b+520>>2]=0;H[b+448>>2]=0;H[b+452>>2]=0;H[b+380>>2]=0;H[b+384>>2]=0;H[b+312>>2]=0;H[b+316>>2]=0;H[b+244>>2]=0;H[b+248>>2]=0;H[b+176>>2]=0;H[b+180>>2]=0;H[b+108>>2]=0;H[b+112>>2]=0;H[b+40>>2]=0;H[b+44>>2]=0;b=b+544|0;j=j+8|0;if((o|0)!=(j|0)){continue}break}if(!q){break b}}j=0;while(1){H[b+40>>2]=0;H[b+44>>2]=0;b=b+68|0;j=j+1|0;if((q|0)!=(j|0)){continue}break}}k=k+36|0;n=n+1|0;if(n>>>0>2]){continue}break}}q=f;c:{if(!(I[c|0]&2)){break c}if(h>>>0<=5){Ba(i,2,4196,0);break c}if(!(I[f|0]==255&I[f+1|0]==145)){Ba(i,2,4238,0);break c}q=f+6|0;H[p+24>>2]=q}l=Fa(20);if(!l){break a}d:{if(F[a+108|0]&1){q=H[a+40>>2];o=a+44|0;h=a+40|0;break d}if(I[c+5640|0]&2){q=H[c+5168>>2];o=c+5180|0;h=c+5168|0;break d}H[p+28>>2]=(f+h|0)-q;o=p+28|0;h=p+24|0}a=H[o>>2];H[l+12>>2]=0;H[l+16>>2]=0;H[l+8>>2]=q;H[l>>2]=q;H[l+4>>2]=a+q;if(!Ra(l,1)){hc(l);a=ic(l);db(l);a=a+q|0;b=H[h>>2];d=H[o>>2];if(I[c|0]&4){if(b+(d-a|0)>>>0<=1){Ba(i,1,4422,0);break a}if(!(I[a|0]==255&I[a+1|0]==146)){Ba(i,1,4401,0);break a}a=a+2|0}a=a-b|0;H[o>>2]=d-a;H[h>>2]=a+b;H[e>>2]=0;H[g>>2]=H[p+24>>2]-f;x=1;break a}if(H[r+24>>2]){t=r+28|0;while(1){a=H[d+36>>2];b=H[t+20>>2];e:{if(Qb(t)){break e}u=b+N(a,40)|0;z=N(H[u+20>>2],H[u+16>>2]);if(!z){break e}k=H[u+24>>2];v=0;while(1){f:{g:{if(!H[k+40>>2]){a=dc(l,H[u+32>>2],v,H[d+40>>2]+1|0);break g}a=Ra(l,1)}if(!a){H[k+36>>2]=0;break f}if(!H[k+40>>2]){b=0;while(1){a=b;b=b+1|0;if(!dc(l,H[u+36>>2],v,a)){continue}break}b=H[t+28>>2];H[k+32>>2]=3;H[k+24>>2]=b;H[k+28>>2]=(b-a|0)+1}a=1;h:{if(!Ra(l,1)){break h}a=2;if(!Ra(l,1)){break h}a=Ra(l,2);if((a|0)!=3){a=a+3|0;break h}a=Ra(l,5);if((a|0)!=31){a=a+6|0;break h}a=Ra(l,7)+37|0}H[k+36>>2]=a;b=0;while(1){a=b;b=b+1|0;if(Ra(l,1)){continue}break}H[k+32>>2]=a+H[k+32>>2];i:{a=H[k+40>>2];j:{k:{if(!a){a=H[(H[c+5584>>2]+N(H[d+28>>2],1080)|0)+16>>2];if(!H[k+48>>2]){b=Ha(H[k>>2],240);if(!b){break i}H[k>>2]=b;y(b+N(H[k+48>>2],24)|0,0,240);H[k+48>>2]=10}j=H[k>>2];kb(j);b=a&4?1:a&1?10:109;a=0;break k}b=H[k>>2];n=a-1|0;j=b+N(n,24)|0;if(H[j+4>>2]!=H[j+12>>2]){break j}n=H[(H[c+5584>>2]+N(H[d+28>>2],1080)|0)+16>>2];j=H[k+48>>2];if(j>>>0>>0){j=j+10|0;b=Ha(b,N(j,24));if(!b){break i}H[k>>2]=b;y(b+N(H[k+48>>2],24)|0,0,240);H[k+48>>2]=j;b=H[k>>2]}j=N(a,24)+b|0;kb(j);b=1;l:{if(n&4){break l}b=109;if(!(n&1)){break l}b=H[j-12>>2];b=(b|0)==1?2:(b|0)==10?2:1}}n=a;H[j+12>>2]=b}a=H[k+36>>2];if(I[(H[c+5584>>2]+N(H[d+28>>2],1080)|0)+16|0]&64){while(1){m=N(n,24);s=n?a:1;H[(m+H[k>>2]|0)+16>>2]=s;w=H[k+32>>2];j=0;b=a;if(s>>>0>=2){while(1){j=j+1|0;s=b>>>0>3;b=b>>>1|0;if(s){continue}break}}b=j+w|0;if(b>>>0>=33){H[p+16>>2]=b;Ba(i,1,15535,p+16|0);break i}j=Ra(l,b);b=H[k>>2];m=m+b|0;H[m+20>>2]=j;a=a-H[m+16>>2]|0;if((a|0)<=0){break f}j=H[(H[c+5584>>2]+N(H[d+28>>2],1080)|0)+16>>2];m=H[k+48>>2];if(m>>>0>>0){m=m+10|0;b=Ha(b,N(m,24));if(!b){break i}H[k>>2]=b;y(b+N(H[k+48>>2],24)|0,0,240);H[k+48>>2]=m;b=H[k>>2]}n=n+1|0;b=b+N(n,24)|0;kb(b);if(j&4){H[b+12>>2]=1;continue}if(j&1){j=b;b=H[b-12>>2];H[j+12>>2]=(b|0)==1?2:(b|0)==10?2:1}else{H[b+12>>2]=109}continue}}while(1){m=N(n,24);j=m+H[k>>2]|0;b=H[j+12>>2]-H[j+4>>2]|0;b=(a|0)>(b|0)?b:a;H[j+16>>2]=b;s=H[k+32>>2];j=0;if(b>>>0>=2){while(1){j=j+1|0;w=b>>>0>3;b=b>>>1|0;if(w){continue}break}}b=j+s|0;if(b>>>0>=33){H[p>>2]=b;Ba(i,1,15535,p);break i}j=Ra(l,b);b=H[k>>2];m=m+b|0;H[m+20>>2]=j;a=a-H[m+16>>2]|0;if((a|0)<=0){break f}j=H[(H[c+5584>>2]+N(H[d+28>>2],1080)|0)+16>>2];m=H[k+48>>2];if(m>>>0>>0){m=m+10|0;b=Ha(b,N(m,24));if(!b){break i}H[k>>2]=b;y(b+N(H[k+48>>2],24)|0,0,240);H[k+48>>2]=m;b=H[k>>2]}n=n+1|0;b=b+N(n,24)|0;kb(b);if(j&4){H[b+12>>2]=1;continue}if(j&1){j=b;b=H[b-12>>2];H[j+12>>2]=(b|0)==1?2:(b|0)==10?2:1}else{H[b+12>>2]=109}continue}}db(l);break a}k=k+68|0;v=v+1|0;if((z|0)!=(v|0)){continue}break}}t=t+36|0;A=A+1|0;if(A>>>0>2]){continue}break}}if(!hc(l)){db(l);break a}a=ic(l);db(l);b=a+q|0;a=H[h>>2];if(I[c|0]&4){if(a+(H[o>>2]-b|0)>>>0<=1){Ba(i,1,4422,0);break a}if(!(I[b|0]==255&I[b+1|0]==146)){Ba(i,1,4401,0);break a}b=b+2|0}if((a|0)==(b|0)){break a}H[o>>2]=H[o>>2]+(a-b|0);H[h>>2]=b;x=1;H[e>>2]=1;H[g>>2]=H[p+24>>2]-f}na=p+32|0;return x}function Gb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0;a:{if(!c){break a}b:{e=H[a+184>>2];if(!e){break b}f=H[a+96>>2];if(!f|!H[f+16>>2]|(e|0)!=H[H[f+24>>2]+40>>2]){break b}f=H[c+16>>2];if(!f){break b}n=H[c+24>>2];if(H[n+40>>2]|H[n+44>>2]){break b}i=f&7;c:{if(f>>>0>=8){p=f&-8;while(1){f=N(h,52)+n|0;H[f+404>>2]=e;H[f+352>>2]=e;H[f+300>>2]=e;H[f+248>>2]=e;H[f+196>>2]=e;H[f+144>>2]=e;H[f+92>>2]=e;H[f+40>>2]=e;h=h+8|0;g=g+8|0;if((g|0)!=(p|0)){continue}break}if(!i){break c}}while(1){H[(N(h,52)+n|0)+40>>2]=e;h=h+1|0;j=j+1|0;if((i|0)!=(j|0)){continue}break}}e=0;p=0;n=na-32|0;na=n;v=H[c+16>>2];d:{if(!v){e=1;break d}h=H[c>>2];f=h>>31;q=f;e:{if((f|0)<0){break e}i=H[c+4>>2];f=i>>31;m=f;if((f|0)<0){break e}g=H[c+8>>2];f=g>>31;s=f;if((f|0)<0){break e}j=H[c+12>>2];r=j>>31;if((r|0)<0){break e}f=H[c+24>>2];y=h-1|0;w=q-!h|0;z=i-1|0;x=m-!i|0;A=g-1|0;B=s-!g|0;C=j-1|0;D=r-!j|0;while(1){e=H[f>>2];h=e+y|0;i=e>>>0>h>>>0?w+1|0:w;j=ve(h,i,e,0);H[f+16>>2]=j;i=H[f+4>>2];h=i+z|0;g=i>>>0>h>>>0?x+1|0:x;r=ve(h,g,i,0);H[f+20>>2]=r;h=H[f+40>>2];g=h&31;if((h&63)>>>0>=32){q=1<>>32-g}s=m-1|0;g=q-!m|0;u=g;t=e>>31;l=t+B|0;o=l+1|0;g=l;l=e+A|0;g=ue(l,l>>>0>>0?o:g,e,t);e=u+(g>>31)|0;o=e+1|0;l=e;e=g+s|0;t=g>>>0>e>>>0?o:l;g=h&31;l=(j>>31)+u|0;E=l+1|0;o=l;l=j+s|0;j=j>>>0>l>>>0?E:o;if((h&63)>>>0>=32){g=t>>g}else{g=((1<>>g}e=h&31;if((h&63)>>>0>=32){e=j>>e}else{e=((1<>>e}e=g-e|0;if((e|0)<0){H[n+4>>2]=e;H[n>>2]=p;Ba(d,1,13510,n);e=0;break d}H[f+8>>2]=e;e=i>>31;g=e+D|0;o=g+1|0;l=g;g=i+C|0;j=g>>>0>>0?o:l;i=ue(g,j,i,e);e=(i>>31)+u|0;g=e+1|0;l=e;e=i+s|0;g=i>>>0>e>>>0?g:l;i=h&31;j=q+(r>>31)|0;o=j+1|0;l=j;j=m+r|0;q=j>>>0>>0?o:l;m=j-1|0;if((h&63)>>>0>=32){l=g>>i}else{l=((1<>>i}i=q-!j|0;e=h&31;if((h&63)>>>0>=32){m=i>>e}else{m=((1<>>e}e=l-m|0;if((e|0)<0){H[n+20>>2]=e;H[n+16>>2]=p;Ba(d,1,13579,n+16|0);e=0;break d}H[f+12>>2]=e;f=f+52|0;e=1;p=p+1|0;if((v|0)!=(p|0)){continue}break}break d}Ba(d,1,6720,0)}na=n+32|0;if(e){break b}return 0}e=H[a+100>>2];if(!e){e=Eb();H[a+100>>2]=e;if(!e){break a}}Ec(c,e);if(!Wa(H[a+216>>2],22,d)){break a}h=H[a+216>>2];f=H[h>>2];e=H[h+8>>2];f:{if(f){k=1;g:{if((f|0)!=1){n=f&1;i=f&-2;f=0;while(1){m=0;h:{if(!k){break h}m=0;if(!(ra[H[e>>2]](a,b,d)|0)){break h}m=(ra[H[e+4>>2]](a,b,d)|0)!=0}k=m;e=e+8|0;f=f+2|0;if((i|0)!=(f|0)){continue}break}if(!n){break g}}if(!k){k=0;break g}k=(ra[H[e>>2]](a,b,d)|0)!=0}Pa(h);if(k){break f}Ua(H[a+96>>2]);H[a+96>>2]=0;return 0}Pa(h)}f=0;b=0;d=0;i:{j:{k:{e=H[a+60>>2];if(!e){if(H[c+16>>2]){break k}k=1;break i}h=Fa(N(e,52));if(!h){break j}e=0;if(H[c+16>>2]){b=H[c+24>>2];while(1){e=N(d,52);Ca(H[(e+b|0)+44>>2]);b=H[c+24>>2];H[(e+b|0)+44>>2]=0;d=d+1|0;e=H[c+16>>2];if(d>>>0>>0){continue}break}}if(H[a+60>>2]){d=H[H[a+100>>2]+24>>2];e=0;while(1){b=h+N(e,52)|0;f=N(H[H[a+64>>2]+(e<<2)>>2],52);d=f+d|0;H[b+48>>2]=H[d+48>>2];k=H[d+44>>2];H[b+40>>2]=H[d+40>>2];H[b+44>>2]=k;k=H[d+36>>2];H[b+32>>2]=H[d+32>>2];H[b+36>>2]=k;k=H[d+28>>2];H[b+24>>2]=H[d+24>>2];H[b+28>>2]=k;k=H[d+20>>2];H[b+16>>2]=H[d+16>>2];H[b+20>>2]=k;k=H[d+12>>2];H[b+8>>2]=H[d+8>>2];H[b+12>>2]=k;k=H[d+4>>2];H[b>>2]=H[d>>2];H[b+4>>2]=k;d=H[H[a+100>>2]+24>>2];f=f+d|0;H[b+36>>2]=H[f+36>>2];H[b+44>>2]=H[f+44>>2];H[f+44>>2]=0;e=e+1|0;f=H[a+60>>2];if(e>>>0>>0){continue}break}e=H[c+16>>2]}if(e){b=H[H[a+100>>2]+24>>2];d=0;while(1){e=N(d,52);Ca(H[(e+b|0)+44>>2]);b=H[H[a+100>>2]+24>>2];H[(e+b|0)+44>>2]=0;d=d+1|0;if(d>>>0>2]){continue}break}f=H[a+60>>2]}H[c+16>>2]=f;Ca(H[c+24>>2]);H[c+24>>2]=h;k=1;break i}e=H[c+24>>2];d=H[H[a+100>>2]+24>>2];while(1){f=N(b,52);e=f+e|0;H[e+36>>2]=H[(d+f|0)+36>>2];Ca(H[e+44>>2]);e=H[c+24>>2];m=f+e|0;d=H[H[a+100>>2]+24>>2];f=f+d|0;H[m+44>>2]=H[f+44>>2];H[f+44>>2]=0;b=b+1|0;if(b>>>0>2]){continue}break}k=1;break i}Ua(H[a+96>>2]);H[a+96>>2]=0;k=0}}return k|0}function zb(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;j=(c<<2)+b|0;e=H[a>>2];f=H[a+12>>2]<<5;i=e+f|0;l=e-f|0;e=H[a+16>>2];k=H[a+28>>2];h=H[a+20>>2];m=H[a+8>>2];a:{b:{if(i&15|(b&15|d>>>0<8)){if(e>>>0>=h>>>0){break a}c:{switch(d-1|0){case 1:f=e+1|0;if(h-e&1){j=i+(e<<6)|0;e=(e<<2)+b|0;L[j>>2]=L[e>>2];L[j+4>>2]=L[e+(c<<2)>>2];e=f}if((f|0)==(h|0)){break a}while(1){f=i+(e<<6)|0;j=(e<<2)+b|0;L[f>>2]=L[j>>2];g=f;f=c<<2;L[g+4>>2]=L[f+j>>2];j=e+1|0;g=i+(j<<6)|0;j=(j<<2)+b|0;L[g>>2]=L[j>>2];L[g+4>>2]=L[f+j>>2];e=e+2|0;if((h|0)!=(e|0)){continue}break};break a;case 0:break c;default:break b}}f=e;g=h-e&3;if(g){j=0;while(1){L[i+(f<<6)>>2]=L[(f<<2)+b>>2];f=f+1|0;j=j+1|0;if((g|0)!=(j|0)){continue}break}}if(e-h>>>0>4294967292){break a}while(1){L[i+(f<<6)>>2]=L[(f<<2)+b>>2];e=f+1|0;L[i+(e<<6)>>2]=L[(e<<2)+b>>2];e=f+2|0;L[i+(e<<6)>>2]=L[(e<<2)+b>>2];e=f+3|0;L[i+(e<<6)>>2]=L[(e<<2)+b>>2];f=f+4|0;if((h|0)!=(f|0)){continue}break}break a}if(e>>>0>=h>>>0){break a}while(1){f=i+(e<<6)|0;L[f>>2]=L[(e<<2)+b>>2];g=c+e|0;L[f+4>>2]=L[(g<<2)+b>>2];g=c+g|0;L[f+8>>2]=L[(g<<2)+b>>2];g=c+g|0;L[f+12>>2]=L[(g<<2)+b>>2];g=c+g|0;L[f+16>>2]=L[(g<<2)+b>>2];g=c+g<<2;L[f+20>>2]=L[g+b>>2];g=g+j|0;L[f+24>>2]=L[g>>2];L[f+28>>2]=L[g+(c<<2)>>2];e=e+1|0;if((h|0)!=(e|0)){continue}break}break a}while(1){f=i+(e<<6)|0;L[f>>2]=L[(e<<2)+b>>2];g=c+e|0;L[f+4>>2]=L[(g<<2)+b>>2];g=c+g|0;L[f+8>>2]=L[(g<<2)+b>>2];d:{if((d|0)==3){break d}g=c+g|0;L[f+12>>2]=L[(g<<2)+b>>2];if((d|0)==4){break d}g=c+g|0;L[f+16>>2]=L[(g<<2)+b>>2];if((d|0)==5){break d}g=c+g|0;L[f+20>>2]=L[(g<<2)+b>>2];if((d|0)==6){break d}g=j+(g<<2)|0;L[f+24>>2]=L[g>>2];if((d|0)==7){break d}L[f+28>>2]=L[g+(c<<2)>>2]}e=e+1|0;if((h|0)!=(e|0)){continue}break}}b=(m<<2)+b|0;f=b+(c<<2)|0;e=H[a+24>>2];i=l+32|0;e:{if(i&15|(b&15|d>>>0<8)){if(e>>>0>=k>>>0){break e}f:{switch(d-1|0){case 1:a=e+1|0;if(k-e&1){d=i+(e<<6)|0;e=b+(e<<2)|0;L[d>>2]=L[e>>2];L[d+4>>2]=L[e+(c<<2)>>2];e=a}if((a|0)==(k|0)){break e}while(1){a=i+(e<<6)|0;d=b+(e<<2)|0;L[a>>2]=L[d>>2];f=a;a=c<<2;L[f+4>>2]=L[a+d>>2];d=e+1|0;f=i+(d<<6)|0;d=b+(d<<2)|0;L[f>>2]=L[d>>2];L[f+4>>2]=L[a+d>>2];e=e+2|0;if((k|0)!=(e|0)){continue}break};break e;case 0:c=e;a=k-e&3;if(a){f=0;while(1){L[i+(c<<6)>>2]=L[b+(c<<2)>>2];c=c+1|0;f=f+1|0;if((a|0)!=(f|0)){continue}break}}if(e-k>>>0>4294967292){break e}while(1){L[i+(c<<6)>>2]=L[b+(c<<2)>>2];a=c+1|0;L[i+(a<<6)>>2]=L[b+(a<<2)>>2];a=c+2|0;L[i+(a<<6)>>2]=L[b+(a<<2)>>2];a=c+3|0;L[i+(a<<6)>>2]=L[b+(a<<2)>>2];c=c+4|0;if((k|0)!=(c|0)){continue}break};break e;default:break f}}while(1){a=i+(e<<6)|0;L[a>>2]=L[b+(e<<2)>>2];h=c+e|0;L[a+4>>2]=L[b+(h<<2)>>2];h=c+h|0;L[a+8>>2]=L[b+(h<<2)>>2];g:{if((d|0)==3){break g}h=c+h|0;L[a+12>>2]=L[b+(h<<2)>>2];if((d|0)==4){break g}h=c+h|0;L[a+16>>2]=L[b+(h<<2)>>2];if((d|0)==5){break g}h=c+h|0;L[a+20>>2]=L[b+(h<<2)>>2];if((d|0)==6){break g}h=f+(h<<2)|0;L[a+24>>2]=L[h>>2];if((d|0)==7){break g}L[a+28>>2]=L[h+(c<<2)>>2]}e=e+1|0;if((k|0)!=(e|0)){continue}break}break e}if(e>>>0>=k>>>0){break e}while(1){a=i+(e<<6)|0;L[a>>2]=L[b+(e<<2)>>2];d=c+e|0;L[a+4>>2]=L[b+(d<<2)>>2];d=c+d|0;L[a+8>>2]=L[b+(d<<2)>>2];d=c+d|0;L[a+12>>2]=L[b+(d<<2)>>2];d=c+d|0;L[a+16>>2]=L[b+(d<<2)>>2];d=c+d<<2;L[a+20>>2]=L[d+b>>2];d=d+f|0;L[a+24>>2]=L[d>>2];L[a+28>>2]=L[d+(c<<2)>>2];e=e+1|0;if((k|0)!=(e|0)){continue}break}}}function Ib(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;f=na-176|0;na=f;a:{if(b&384){Ac(5943,11,c);break a}b:{if(!(b&1)){break b}g=H[a+96>>2];if(!g){break b}d=na-80|0;na=d;Ac(1792,13,c);F[d+79|0]=0;F[d+78|0]=9;e=H[g+4>>2];H[d+68>>2]=H[g>>2];H[d+72>>2]=e;e=d+78|0;H[d+64>>2]=e;Ga(c,7520,d- -64|0);i=H[g+12>>2];H[d+52>>2]=H[g+8>>2];H[d+56>>2]=i;H[d+48>>2]=e;Ga(c,7503,d+48|0);H[d+36>>2]=H[g+16>>2];H[d+32>>2]=e;Ga(c,7277,d+32|0);if(!(!H[g+24>>2]|!H[g+16>>2])){while(1){l=d+78|0;H[d+16>>2]=l;H[d+20>>2]=j;Ga(c,1824,d+16|0);i=H[g+24>>2];e=na-48|0;na=e;F[e+46|0]=9;F[e+47|0]=0;F[e+45|0]=9;i=N(j,52)+i|0;k=H[i+4>>2];H[e+36>>2]=H[i>>2];H[e+40>>2]=k;k=e+45|0;H[e+32>>2]=k;Ga(c,7209,e+32|0);H[e+20>>2]=H[i+24>>2];H[e+16>>2]=k;Ga(c,7455,e+16|0);H[e+4>>2]=H[i+32>>2];H[e>>2]=k;Ga(c,7428,e);na=e+48|0;H[d>>2]=l;Ga(c,1702,d);j=j+1|0;if(j>>>0>2]){continue}break}}Ac(1710,2,c);na=d+80|0}if(!(!(b&2)|!H[a+96>>2])){Ac(1931,36,c);d=H[a+112>>2];H[f+160>>2]=H[a+108>>2];H[f+164>>2]=d;Ga(c,2425,f+160|0);d=H[a+120>>2];H[f+144>>2]=H[a+116>>2];H[f+148>>2]=d;Ga(c,2391,f+144|0);d=H[a+132>>2];H[f+128>>2]=H[a+128>>2];H[f+132>>2]=d;Ga(c,2409,f+128|0);Hb(H[a+12>>2],H[H[a+96>>2]+16>>2],c);Ac(1710,2,c)}c:{if(!(b&8)|!H[a+96>>2]){break c}e=N(H[a+128>>2],H[a+132>>2]);if(!e){break c}d=H[a+180>>2];while(1){Hb(d,H[H[a+96>>2]+16>>2],c);d=d+5644|0;h=h+1|0;if((e|0)!=(h|0)){continue}break}}if(!(b&16)){break a}a=H[a+224>>2];Ac(1893,37,c);b=H[a>>2];d=H[a+4>>2];e=H[a+12>>2];H[f+120>>2]=H[a+8>>2];H[f+124>>2]=e;H[f+112>>2]=b;H[f+116>>2]=d;Ga(c,5730,f+112|0);Ac(1875,17,c);if(!(!H[a+28>>2]|!H[a+24>>2])){d=0;while(1){b=H[a+28>>2]+N(d,24)|0;e=J[b>>1];h=H[b+8>>2];g=H[b+12>>2];H[f+96>>2]=H[b+16>>2];H[f+88>>2]=h;H[f+92>>2]=g;H[f+80>>2]=e;Ga(c,7397,f+80|0);d=d+1|0;if(d>>>0>2]){continue}break}}Ac(1708,4,c);e=H[a+40>>2];d:{if(!e){break d}g=H[a+36>>2];if(!g){break d}h=0;d=0;while(1){b=e+N(d,40)|0;j=H[b+4>>2];e:{if(!j){break e}b=H[b+16>>2];if(!b){break e}i=H[b>>2];k=H[b+4>>2];if((k|0)<0){i=1}else{i=!i&(k|0)<=0}if(i|(H[b+8>>2]|H[b+12>>2])){break e}if(xc(1439)){break d}}h=h+j|0;d=d+1|0;if((g|0)!=(d|0)){continue}break}if(!h){break d}Ac(1858,16,c);if(H[a+36>>2]){h=H[a+40>>2];g=0;while(1){b=N(g,40);j=H[(b+h|0)+4>>2];H[f+68>>2]=j;H[f+64>>2]=g;Ga(c,7467,f- -64|0);h=H[a+40>>2];f:{if(!j){break f}d=0;if(!H[(b+h|0)+16>>2]){break f}while(1){e=H[(b+H[a+40>>2]|0)+16>>2]+N(d,24)|0;h=H[e>>2];i=H[e+4>>2];k=H[e+8>>2];l=H[e+12>>2];m=H[e+20>>2];H[f+56>>2]=H[e+16>>2];H[f+60>>2]=m;H[f+48>>2]=k;H[f+52>>2]=l;H[f+40>>2]=h;H[f+44>>2]=i;H[f+32>>2]=d;Ga(c,10938,f+32|0);d=d+1|0;if((j|0)!=(d|0)){continue}break}h=H[a+40>>2]}e=b+h|0;g:{if(!H[e+24>>2]){break g}d=0;if(!H[e+20>>2]){break g}while(1){e=H[(b+h|0)+24>>2]+N(d,24)|0;h=J[e>>1];j=H[e+8>>2];i=H[e+12>>2];H[f+16>>2]=H[e+16>>2];H[f+8>>2]=j;H[f+12>>2]=i;H[f>>2]=h;Ga(c,7397,f);d=d+1|0;h=H[a+40>>2];if(d>>>0>2]){continue}break}}g=g+1|0;if(g>>>0>2]){continue}break}}Ac(1708,4,c)}Ac(1710,2,c)}na=f+176|0}function oe(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;e=na-128|0;na=e;H[e+120>>2]=0;a:{if((c|0)!=8){Ba(d,1,4047,0);Ba(d,1,4047,0);break a}Da(b,a+228|0,2);Da(b+2|0,e+124|0,4);Da(b+6|0,e+116|0,1);Da(b+7|0,e+120|0,1);c=H[a+228>>2];i=H[a+128>>2];if(c>>>0>=N(i,H[a+132>>2])>>>0){H[e+112>>2]=c;Ba(d,1,7843,e+112|0);break a}f=H[a+180>>2]+N(c,5644)|0;h=(c>>>0)/(i>>>0)|0;b=H[e+116>>2];b:{g=H[a+44>>2];if((g|0)>=0&(c|0)!=(g|0)){break b}g=H[f+5588>>2]+1|0;if((g|0)==(b|0)){break b}H[e+104>>2]=g;H[e+100>>2]=b;H[e+96>>2]=c;Ba(d,1,7867,e+96|0);f=0;break a}H[f+5588>>2]=b;c:{b=H[e+124>>2];if(b-1>>>0<=12){if((b|0)!=12){break c}H[e+64>>2]=12;Ba(d,2,11864,e- -64|0);b=H[e+124>>2]}if(!b){Ba(d,4,10695,0);H[a+56>>2]=1}d:{e:{f:{g:{b=H[f+5592>>2];if(b){g=H[e+116>>2];if(g>>>0>>0){break g}H[e+52>>2]=b;H[e+48>>2]=g;Ba(d,1,5150,e+48|0);H[a+56>>2]=1;f=0;break a}g=H[e+120>>2];if(g){break f}break d}g=H[e+120>>2];if(!g){break e}}b=(I[a+92|0]>>>4&1)+g|0;H[e+120>>2]=b;g=H[e+116>>2];j=H[f+5592>>2];if(g>>>0>j-1>>>0){H[e+20>>2]=j;H[e+16>>2]=g;Ba(d,1,5051,e+16|0);H[a+56>>2]=1;f=0;break a}if(b>>>0<=g>>>0){H[e+36>>2]=b;H[e+32>>2]=g;Ba(d,1,5250,e+32|0);H[a+56>>2]=1;f=0;break a}H[f+5592>>2]=b}if((H[e+116>>2]+1|0)!=(b|0)){break d}F[a+92|0]=I[a+92|0]|1}b=H[e+124>>2];H[a+8>>2]=16;H[a+24>>2]=H[a+56>>2]?0:b-12|0;f=H[a+44>>2];h:{if((f|0)==-1){f=4;b=c-N(h,i)|0;if(!(b>>>0>2]|b>>>0>=K[a+36>>2]|h>>>0>2])){f=h>>>0>=K[a+40>>2]?4:0}F[a+92|0]=I[a+92|0]&251|f;b=H[a+228>>2];break h}b=H[a+228>>2];F[a+92|0]=I[a+92|0]&251|((f|0)!=(b|0)?4:0)}c=H[H[a+224>>2]+40>>2]+N(b,40)|0;H[c>>2]=b;H[c+12>>2]=H[e+116>>2];f=H[e+120>>2];if(!H[a+76>>2]){if(K[c+4>>2]>=f>>>0){f=1;break a}H[e>>2]=b;Ba(d,2,1612,e);H[a+76>>2]=1;f=H[e+120>>2]}c=H[H[a+224>>2]+40>>2];b=H[a+228>>2];h=c+N(b,40)|0;if(f){H[h+4>>2]=f;b=H[e+120>>2];H[h+8>>2]=b;c=H[h+16>>2];if(!c){b=Ea(b,24);H[(H[H[a+224>>2]+40>>2]+N(H[a+228>>2],40)|0)+16>>2]=b;if(b){f=1;break a}f=0;Ba(d,1,6947,0);break a}b=Ha(c,N(b,24));c=H[H[a+224>>2]+40>>2]+N(H[a+228>>2],40)|0;if(!b){Ca(H[c+16>>2]);f=0;H[(H[H[a+224>>2]+40>>2]+N(H[a+228>>2],40)|0)+16>>2]=0;Ba(d,1,6947,0);break a}H[c+16>>2]=b;f=1;break a}i:{g=H[h+16>>2];if(g){break i}H[h+8>>2]=10;g=Ea(10,24);c=H[H[a+224>>2]+40>>2];b=H[a+228>>2];h=c+N(b,40)|0;H[h+16>>2]=g;if(g){break i}f=0;H[h+8>>2]=0;Ba(d,1,6947,0);break a}f=1;b=N(b,40)+c|0;c=H[e+116>>2];if(K[b+8>>2]>c>>>0){break a}h=b;b=c+1|0;H[h+8>>2]=b;b=Ha(g,N(b,24));c=H[H[a+224>>2]+40>>2]+N(H[a+228>>2],40)|0;if(!b){Ca(H[c+16>>2]);f=0;a=H[H[a+224>>2]+40>>2]+N(H[a+228>>2],40)|0;H[a+8>>2]=0;H[a+16>>2]=0;Ba(d,1,6947,0);break a}H[c+16>>2]=b;break a}H[e+80>>2]=b;Ba(d,1,12133,e+80|0);f=0}na=e+128|0;return f|0}function nb(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0;f=H[a+8>>2];e=f+H[a+4>>2]|0;a:{if(!H[a+12>>2]){if((e|0)<2|(d|0)<=0){break a}q=e&2147483644;m=e&3;r=e&1;s=f+1|0;g=H[a>>2];o=g+(e<<2)|0;t=e-4>>>1|0;a=e-1|0;u=g+(a<<2)|0;v=N(c,f)<<2;l=e>>>0<4;w=N(a>>>1|0,c)<<2;while(1){f=H[b+v>>2];e=H[b>>2]-(f+1>>1)|0;h=0;a=0;if(!l){while(1){j=a+1|0;x=H[(N(j,c)<<2)+b>>2];i=H[(N(a+s|0,c)<<2)+b>>2];p=g+(h<<2)|0;H[p>>2]=e;k=e;e=x-((f+i|0)+2>>2)|0;H[p+4>>2]=(k+e>>1)+f;h=h+2|0;k=(a|0)!=(t|0);f=i;a=j;if(k){continue}break}}H[g+(h<<2)>>2]=e;if(r){a=H[b+w>>2]-(f+1>>1)|0;H[u>>2]=a;e=a+e>>1;a=-8}else{a=-4}H[a+o>>2]=e+f;f=0;a=0;h=0;e=0;b:{if(!l){while(1){H[(N(a,c)<<2)+b>>2]=H[g+(a<<2)>>2];e=a|1;H[(N(e,c)<<2)+b>>2]=H[g+(e<<2)>>2];e=a|2;H[(N(e,c)<<2)+b>>2]=H[g+(e<<2)>>2];e=a|3;H[(N(e,c)<<2)+b>>2]=H[g+(e<<2)>>2];a=a+4|0;h=h+4|0;if((q|0)!=(h|0)){continue}break}e=a;if(!m){break b}}while(1){H[(N(c,e)<<2)+b>>2]=H[g+(e<<2)>>2];e=e+1|0;f=f+1|0;if((m|0)!=(f|0)){continue}break}}b=b+4|0;n=n+1|0;if((n|0)!=(d|0)){continue}break}break a}c:{switch(e-1|0){case 0:if((d|0)<=0){break a}a=d&3;if(d>>>0>=4){d=d&2147483644;c=0;while(1){H[b>>2]=H[b>>2]/2;H[b+4>>2]=H[b+4>>2]/2;H[b+8>>2]=H[b+8>>2]/2;H[b+12>>2]=H[b+12>>2]/2;b=b+16|0;c=c+4|0;if((d|0)!=(c|0)){continue}break}if(!a){break a}}c=0;while(1){H[b>>2]=H[b>>2]/2;b=b+4|0;c=c+1|0;if((a|0)!=(c|0)){continue}break};break a;case 1:if((d|0)<=0){break a}a=H[a>>2];e=0;f=N(c,f)<<2;while(1){i=b+f|0;j=H[b>>2]-(H[i>>2]+1>>1)|0;H[a+4>>2]=j;i=j+H[i>>2]|0;H[a>>2]=i;H[b>>2]=i;H[(c<<2)+b>>2]=H[a+4>>2];b=b+4|0;e=e+1|0;if((e|0)!=(d|0)){continue}break};break a;default:break c}}if((e|0)<3|(d|0)<=0){break a}q=e&2147483644;m=e&3;g=H[a>>2];r=(g+(e<<2)|0)-4|0;a=e-2|0;s=g+(a<<2)|0;o=e&1;i=!o;t=((e-i|0)-4>>>1|0)+1|0;u=N(c,f)<<2;v=a-i>>>0<2;w=N((e>>>1|0)-1|0,c)<<2;x=e-1>>>0<3;while(1){l=b+u|0;f=H[l+(c<<2)>>2];a=H[l>>2];e=H[b>>2]-((f+a|0)+2>>2)|0;H[g>>2]=e+a;h=1;a=1;if(!v){while(1){p=H[(N(a,c)<<2)+b>>2];j=a+1|0;i=H[l+(N(j,c)<<2)>>2];y=g+(h<<2)|0;H[y>>2]=e;k=e;e=p-((f+i|0)+2>>2)|0;H[y+4>>2]=(k+e>>1)+f;h=h+2|0;k=(a|0)!=(t|0);a=j;f=i;if(k){continue}break}}H[g+(h<<2)>>2]=e;d:{if(!o){a=H[b+w>>2]-(f+1>>1)|0;H[s>>2]=(e+a>>1)+f;break d}a=e+f|0}H[r>>2]=a;f=0;a=0;h=0;e=0;e:{if(!x){while(1){H[(N(a,c)<<2)+b>>2]=H[g+(a<<2)>>2];e=a|1;H[(N(e,c)<<2)+b>>2]=H[g+(e<<2)>>2];e=a|2;H[(N(e,c)<<2)+b>>2]=H[g+(e<<2)>>2];e=a|3;H[(N(e,c)<<2)+b>>2]=H[g+(e<<2)>>2];a=a+4|0;h=h+4|0;if((q|0)!=(h|0)){continue}break}e=a;if(!m){break e}}while(1){H[(N(c,e)<<2)+b>>2]=H[g+(e<<2)>>2];e=e+1|0;f=f+1|0;if((m|0)!=(f|0)){continue}break}}b=b+4|0;n=n+1|0;if((n|0)!=(d|0)){continue}break}}}function Xb(a,b,c,d,e,f,g){var h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;a:{n=N(e,3);h=H[b>>2]>>>n|0;if(h&2097168){break a}h=h&495;if(!h){break a}o=a+28|0;l=o+(I[h+H[a+108>>2]|0]<<2)|0;H[a+104>>2]=l;k=H[l>>2];i=H[k>>2];h=H[a+4>>2]-i|0;H[a+4>>2]=h;j=H[a>>2];b:{if(j>>>16>>>0>>0){m=H[k+4>>2];H[a+4>>2]=i;h=h>>>0>>0;H[l>>2]=H[k+(h?8:12)>>2];k=h?m:!m;h=H[a+8>>2];while(1){c:{if(h){break c}h=H[a+16>>2];m=h+1|0;l=I[h+1|0];if(I[h|0]==255){if(l>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;j=j+65280|0;h=8;break c}H[a+16>>2]=m;j=(l<<9)+j|0;h=7;break c}H[a+16>>2]=m;h=8;j=(l<<8)+j|0}h=h-1|0;H[a+8>>2]=h;j=j<<1;H[a>>2]=j;i=i<<1;H[a+4>>2]=i;if(i>>>0<32768){continue}break}h=i;break b}j=j-(i<<16)|0;H[a>>2]=j;if(!(h&32768)){m=H[k+4>>2];i=h>>>0>>0;H[l>>2]=H[k+(i?12:8)>>2];k=i?!m:m;i=H[a+8>>2];while(1){d:{if(i){break d}i=H[a+16>>2];m=i+1|0;l=I[i+1|0];if(I[i|0]==255){if(l>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;j=j+65280|0;i=8;break d}H[a+16>>2]=m;j=(l<<9)+j|0;i=7;break d}H[a+16>>2]=m;i=8;j=(l<<8)+j|0}i=i-1|0;H[a+8>>2]=i;j=j<<1;H[a>>2]=j;h=h<<1;H[a+4>>2]=h;if(h>>>0<32768){continue}break}break b}k=H[k+4>>2]}e:{if(!k){break e}p=b-4|0;i=H[b>>2];k=H[b+4>>2]>>>n+17&4|(H[p>>2]>>>n+19&1|(i>>>n+16&64|i>>>n&170|i>>>(e?n+12|0:14)&16));m=o+(I[k+24384|0]<<2)|0;H[a+104>>2]=m;l=H[m>>2];i=H[l>>2];h=h-i|0;H[a+4>>2]=h;o=I[k+24640|0];f:{if(j>>>16>>>0>>0){k=H[l+4>>2];H[a+4>>2]=i;h=h>>>0>>0;H[m>>2]=H[l+(h?8:12)>>2];l=h?k:!k;h=H[a+8>>2];while(1){g:{if(h){break g}h=H[a+16>>2];m=h+1|0;k=I[h+1|0];if(I[h|0]==255){if(k>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;j=j+65280|0;h=8;break g}H[a+16>>2]=m;j=(k<<9)+j|0;h=7;break g}H[a+16>>2]=m;h=8;j=(k<<8)+j|0}h=h-1|0;H[a+8>>2]=h;j=j<<1;H[a>>2]=j;i=i<<1;H[a+4>>2]=i;if(i>>>0<32768){continue}break}break f}k=j-(i<<16)|0;H[a>>2]=k;if(!(h&32768)){j=H[l+4>>2];i=h>>>0>>0;H[m>>2]=H[l+(i?12:8)>>2];l=i?!j:j;j=H[a+8>>2];while(1){h:{if(j){break h}j=H[a+16>>2];m=j+1|0;i=I[j+1|0];if(I[j|0]==255){if(i>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;k=k+65280|0;j=8;break h}H[a+16>>2]=m;k=(i<<9)+k|0;j=7;break h}H[a+16>>2]=m;j=8;k=(i<<8)+k|0}j=j-1|0;H[a+8>>2]=j;k=k<<1;H[a>>2]=k;h=h<<1;H[a+4>>2]=h;if(h>>>0<32768){continue}break}break f}l=H[l+4>>2]}H[c>>2]=(l|0)==(o|0)?d:0-d|0;H[p>>2]=H[p>>2]|32<>2]=H[b>>2]|(c<<19|16)<>2]=H[b+4>>2]|8<>2]=H[a+4>>2]|32768;H[a>>2]=H[a>>2]|c<<31|65536;a=a-4|0;H[a>>2]=H[a>>2]|131072}if((e|0)!=3){break e}a=(f<<2)+b|0;H[a+4>>2]=H[a+4>>2]|1;H[a>>2]=H[a>>2]|c<<18|2;a=a-4|0;H[a>>2]=H[a>>2]|4}H[b>>2]=H[b>>2]|2097152<>2]>>>m|0;if(g&2097168){break a}n=a+28|0;k=n+(I[H[a+108>>2]+(g&495)|0]<<2)|0;H[a+104>>2]=k;j=H[k>>2];h=H[j>>2];g=H[a+4>>2]-h|0;H[a+4>>2]=g;i=H[a>>2];b:{if(i>>>16>>>0>>0){l=H[j+4>>2];H[a+4>>2]=h;g=g>>>0>>0;H[k>>2]=H[j+(g?8:12)>>2];j=g?l:!l;g=H[a+8>>2];while(1){c:{if(g){break c}g=H[a+16>>2];l=g+1|0;k=I[g+1|0];if(I[g|0]==255){if(k>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;i=i+65280|0;g=8;break c}H[a+16>>2]=l;i=(k<<9)+i|0;g=7;break c}H[a+16>>2]=l;g=8;i=(k<<8)+i|0}g=g-1|0;H[a+8>>2]=g;i=i<<1;H[a>>2]=i;h=h<<1;H[a+4>>2]=h;if(h>>>0<32768){continue}break}g=h;break b}i=i-(h<<16)|0;H[a>>2]=i;if(!(g&32768)){l=H[j+4>>2];h=g>>>0>>0;H[k>>2]=H[j+(h?12:8)>>2];j=h?!l:l;h=H[a+8>>2];while(1){d:{if(h){break d}h=H[a+16>>2];l=h+1|0;k=I[h+1|0];if(I[h|0]==255){if(k>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;i=i+65280|0;h=8;break d}H[a+16>>2]=l;i=(k<<9)+i|0;h=7;break d}H[a+16>>2]=l;h=8;i=(k<<8)+i|0}h=h-1|0;H[a+8>>2]=h;i=i<<1;H[a>>2]=i;g=g<<1;H[a+4>>2]=g;if(g>>>0<32768){continue}break}break b}j=H[j+4>>2]}if(!j){break a}j=n;n=b-4|0;h=H[b>>2];o=H[b+4>>2]>>>m+17&4|(H[n>>2]>>>m+19&1|(h>>>m+16&64|h>>>m&170|h>>>(e?m+12|0:14)&16));l=j+(I[o+24384|0]<<2)|0;H[a+104>>2]=l;k=H[l>>2];h=H[k>>2];g=g-h|0;H[a+4>>2]=g;e:{if(i>>>16>>>0>>0){j=H[k+4>>2];H[a+4>>2]=h;g=g>>>0>>0;H[l>>2]=H[k+(g?8:12)>>2];k=g?j:!j;g=H[a+8>>2];while(1){f:{if(g){break f}g=H[a+16>>2];l=g+1|0;j=I[g+1|0];if(I[g|0]==255){if(j>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;i=i+65280|0;g=8;break f}H[a+16>>2]=l;i=(j<<9)+i|0;g=7;break f}H[a+16>>2]=l;g=8;i=(j<<8)+i|0}g=g-1|0;H[a+8>>2]=g;i=i<<1;H[a>>2]=i;h=h<<1;H[a+4>>2]=h;if(h>>>0<32768){continue}break}break e}j=i-(h<<16)|0;H[a>>2]=j;if(!(g&32768)){i=H[k+4>>2];h=g>>>0>>0;H[l>>2]=H[k+(h?12:8)>>2];k=h?!i:i;i=H[a+8>>2];while(1){g:{if(i){break g}i=H[a+16>>2];l=i+1|0;h=I[i+1|0];if(I[i|0]==255){if(h>>>0>=144){H[a+12>>2]=H[a+12>>2]+1;j=j+65280|0;i=8;break g}H[a+16>>2]=l;j=(h<<9)+j|0;i=7;break g}H[a+16>>2]=l;i=8;j=(h<<8)+j|0}i=i-1|0;H[a+8>>2]=i;j=j<<1;H[a>>2]=j;g=g<<1;H[a+4>>2]=g;if(g>>>0<32768){continue}break}break e}k=H[k+4>>2]}g=c;c=I[o+24640|0];H[g>>2]=(c|0)==(k|0)?d:0-d|0;H[n>>2]=H[n>>2]|32<>2]=H[b>>2]|(d<<19|16)<>2]=H[b+4>>2]|8<>2]<<2)+b|0;H[c+4>>2]=H[c+4>>2]|32768;H[c>>2]=H[c>>2]|d<<31|65536;c=c-4|0;H[c>>2]=H[c>>2]|131072}if((e|0)!=3){break a}a=(H[a+124>>2]<<2)+b|0;H[a+4>>2]=H[a+4>>2]|4;H[a+12>>2]=H[a+12>>2]|1;H[a+8>>2]=H[a+8>>2]|d<<18|2}}function Kd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;e=na-112|0;na=e;j=1024;a:{b:{h=Ea(1,1024);if(h){l=e+92|0;k=e+108|0;while(1){c:{d:{d=e+104|0;e:{if((Ja(b,d,8,c)|0)!=8){break e}Da(d,e+88|0,4);Da(k,l,4);f=8;f:{g:{h:{i:{switch(H[e+88>>2]){case 0:d=Qa(b);g=qa;if((g|0)<0){g=1}else{g=d>>>0<4294967288&(g|0)<=0}if(g){break h}Ba(c,1,8449,0);break e;case 1:break i;default:break f}}d=e+104|0;if((Ja(b,d,8,c)|0)!=8){break e}Da(d,e+100|0,4);if(!H[e+100>>2]){break g}Ba(c,1,8449,0);break e}H[e+88>>2]=d+8;break f}Da(k,e+88|0,4);f=16}d=H[e+92>>2];if((d|0)==1785737827){b=H[a+100>>2];if(b&4){H[a+100>>2]=b|8;break e}Ba(c,1,5702,0);Ca(h);a=0;break a}i=H[e+88>>2];if(!i){Ba(c,1,3268,0);Ca(h);a=0;break a}if(f>>>0>i>>>0){H[e+4>>2]=d;H[e>>2]=i;Ba(c,1,13933,e);break b}j:{k:{l:{m:{n:{o:{p:{q:{r:{s:{if((d|0)<=1668246641){if((d|0)==1651532643){break r}if((d|0)==1667523942){break p}if((d|0)!=1668112752){break s}g=25296;break n}if((d|0)<=1783635999){if((d|0)==1668246642){break o}g=25264;if((d|0)==1768449138){break n}if((d|0)!=1718909296){break s}g=25240;break l}if((d|0)==1885564018){break q}if((d|0)==1783636e3){break m}g=25248;if((d|0)==1785737832){break l}}d=H[a+100>>2];if(d&1){break j}Ba(c,1,2062,0);Ca(h);a=0;break a}g=25280;break n}g=25288;break n}g=25304;break n}g=25272}H[e+76>>2]=d&255;H[e+64>>2]=d>>>24;H[e+72>>2]=d>>>8&255;H[e+68>>2]=d>>>16&255;Ba(c,2,2011,e- -64|0);f=i-f|0;if(I[a+100|0]&4){break k}d=H[e+92>>2];H[e+48>>2]=d>>>24;H[e+60>>2]=d&255;H[e+52>>2]=d>>>16&255;H[e+56>>2]=d>>>8&255;Ba(c,2,6771,e+48|0);H[a+100>>2]=H[a+100>>2]|2147483647;d=rb(b,f,c);if(!qa&(d|0)==(f|0)){continue}Ba(c,1,3748,0);Ca(h);a=0;break a}g=25232}f=i-f|0}d=f;f=Qa(b);i=qa;if((i|0)<0){f=1}else{f=(i|0)<=0&d>>>0>f>>>0}if(f){f=H[e+88>>2];a=H[e+92>>2];m=e,n=Qa(b),H[m+40>>2]=n;H[e+36>>2]=d;H[e+32>>2]=a&255;H[e+20>>2]=a>>>24;H[e+16>>2]=f;H[e+28>>2]=a>>>8&255;H[e+24>>2]=a>>>16&255;Ba(c,1,15680,e+16|0);break b}if(d>>>0<=j>>>0){f=h;break c}j=d;f=Ha(h,d);if(f){break c}Ca(h);Ba(c,1,2193,0);a=0;break a}if(!(d&2)){Ba(c,1,2132,0);Ca(h);a=0;break a}H[a+100>>2]=d|2147483647;d=i-f|0;f=rb(b,d,c);if(!qa&(d|0)==(f|0)){continue}if(!(I[a+100|0]&8)){break d}Ba(c,2,3748,0)}Ca(h);a=1;break a}Ba(c,1,3748,0);Ca(h);a=0;break a}if((Ja(b,f,d,c)|0)!=(d|0)){Ba(c,1,3798,0);Ca(f);a=0;break a}h=f;if(ra[H[g+4>>2]](a,f,d,c)|0){continue}break}Ca(f);a=0;break a}Ba(c,1,4923,0);a=0;break a}Ca(h);a=0}na=e+112|0;return a|0}function Yd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;f=na-16|0;na=f;if(H[a+8>>2]==16){i=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{i=H[a+12>>2]}a:{if(c>>>0<=1){Ba(d,1,4721,0);a=0;break a}Da(b,f+12|0,2);if(H[f+12>>2]){Ba(d,2,5897,0);a=1;break a}if(c>>>0<=6){Ba(d,1,4721,0);a=0;break a}Da(b+2|0,f+8|0,1);e=H[i+5628>>2];h=H[i+5632>>2];b:{c:{if(!h){break c}j=H[f+8>>2];a=e;while(1){if((j|0)!=H[a>>2]){a=a+20|0;g=g+1|0;if((h|0)!=(g|0)){continue}break c}break}j=0;if((g|0)!=(h|0)){break b}}if(H[i+5636>>2]==(h|0)){a=h+10|0;H[i+5636>>2]=a;a=Ha(e,N(a,20));if(!a){Ca(H[i+5628>>2]);H[i+5636>>2]=0;H[i+5628>>2]=0;H[i+5632>>2]=0;Ba(d,1,4747,0);a=0;break a}H[i+5628>>2]=a;e=H[i+5632>>2];j=N(H[i+5636>>2]-e|0,20);if(j){y(a+N(e,20)|0,0,j)}h=H[i+5632>>2];e=H[i+5628>>2]}a=e+N(h,20)|0;j=1}H[a>>2]=H[f+8>>2];Da(b+3|0,f+12|0,2);if(H[f+12>>2]){Ba(d,2,5897,0);a=1;break a}Da(b+5|0,f+4|0,2);e=H[f+4>>2];if(e>>>0>=2){Ba(d,2,3130,0);a=1;break a}h=c-7|0;if(e){c=b+7|0;while(1){if(h>>>0<=2){Ba(d,1,4721,0);a=0;break a}Da(c,f+12|0,1);if(H[f+12>>2]!=1){Ba(d,2,5579,0);a=1;break a}Da(c+1|0,f,2);e=H[f>>2];b=e&32767;H[a+4>>2]=b;h=h-3|0;e=(e>>>15|0)+1|0;k=N(e,b)+2|0;if(h>>>0>>0){Ba(d,1,4721,0);a=0;break a}c=c+3|0;g=0;if(b){while(1){Da(c,f+12|0,e);if(H[f+12>>2]!=(g|0)){Ba(d,2,6259,0);a=1;break a}c=c+e|0;g=g+1|0;if(g>>>0>2]){continue}break}}Da(c,f,2);e=H[f>>2];b=e&32767;H[f>>2]=b;if((b|0)!=H[a+4>>2]){Ba(d,2,3306,0);a=1;break a}e=(e>>>15|0)+1|0;l=N(e,b)+3|0;k=h-k|0;if(l>>>0>k>>>0){Ba(d,1,4721,0);a=0;break a}c=c+2|0;g=0;if(b){while(1){Da(c,f+12|0,e);if(H[f+12>>2]!=(g|0)){Ba(d,2,6259,0);a=1;break a}c=c+e|0;g=g+1|0;if(g>>>0>2]){continue}break}}Da(c,f+12|0,3);e=H[f+12>>2];H[a+8>>2]=0;H[a+12>>2]=0;F[a+16|0]=!(e&65536)|I[a+16|0]&254;h=e&255;H[f+8>>2]=h;d:{if(!h){break d}m=H[i+5620>>2];if(m){g=H[i+5616>>2];b=0;while(1){if((h|0)==H[g+8>>2]){H[a+8>>2]=g;break d}g=g+20|0;b=b+1|0;if((m|0)!=(b|0)){continue}break}}Ba(d,1,4721,0);a=0;break a}e=e>>>8&255;H[f+8>>2]=e;e:{if(!e){break e}h=H[i+5620>>2];if(h){g=H[i+5616>>2];b=0;while(1){if((e|0)==H[g+8>>2]){H[a+12>>2]=g;break e}g=g+20|0;b=b+1|0;if((h|0)!=(b|0)){continue}break}}Ba(d,1,4721,0);a=0;break a}h=k-l|0;c=c+3|0;n=n+1|0;if(n>>>0>2]){continue}break}}if(h){Ba(d,1,4721,0);a=0;break a}a=1;if(!j){break a}H[i+5632>>2]=H[i+5632>>2]+1;a=1}na=f+16|0;return a|0}function _c(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;if(K[a+44>>2]>=8){i=H[a+40>>2];l=8;while(1){k=H[a+12>>2]<<5;e=H[a>>2];g=H[a+36>>2];b=H[a+16>>2];h=H[a+20>>2];a:{if(b>>>0>=h>>>0){break a}j=e+k|0;d=b+1|0;if(h-b&1){c=j+(b<<6)|0;b=(N(b,g)<<2)+i|0;f=H[b+28>>2];H[c+24>>2]=H[b+24>>2];H[c+28>>2]=f;f=H[b+20>>2];H[c+16>>2]=H[b+16>>2];H[c+20>>2]=f;f=H[b+12>>2];H[c+8>>2]=H[b+8>>2];H[c+12>>2]=f;f=H[b+4>>2];H[c>>2]=H[b>>2];H[c+4>>2]=f;b=d}if((d|0)==(h|0)){break a}while(1){d=(N(b,g)<<2)+i|0;f=H[d+28>>2];c=j+(b<<6)|0;H[c+24>>2]=H[d+24>>2];H[c+28>>2]=f;f=H[d+20>>2];H[c+16>>2]=H[d+16>>2];H[c+20>>2]=f;f=H[d+12>>2];H[c+8>>2]=H[d+8>>2];H[c+12>>2]=f;f=H[d+4>>2];H[c>>2]=H[d>>2];H[c+4>>2]=f;d=b+1|0;c=j+(d<<6)|0;d=(N(d,g)<<2)+i|0;f=H[d+28>>2];H[c+24>>2]=H[d+24>>2];H[c+28>>2]=f;f=H[d+20>>2];H[c+16>>2]=H[d+16>>2];H[c+20>>2]=f;f=H[d+12>>2];H[c+8>>2]=H[d+8>>2];H[c+12>>2]=f;f=H[d+4>>2];H[c>>2]=H[d>>2];H[c+4>>2]=f;b=b+2|0;if((h|0)!=(b|0)){continue}break}}b=H[a+24>>2];h=H[a+28>>2];b:{if(b>>>0>=h>>>0){break b}j=(e-k|0)+32|0;k=(N(g,H[a+8>>2])<<2)+i|0;d=b+1|0;if(h-b&1){c=j+(b<<6)|0;b=k+(N(b,g)<<2)|0;e=H[b+28>>2];H[c+24>>2]=H[b+24>>2];H[c+28>>2]=e;e=H[b+20>>2];H[c+16>>2]=H[b+16>>2];H[c+20>>2]=e;e=H[b+12>>2];H[c+8>>2]=H[b+8>>2];H[c+12>>2]=e;e=H[b+4>>2];H[c>>2]=H[b>>2];H[c+4>>2]=e;b=d}if((d|0)==(h|0)){break b}while(1){d=k+(N(b,g)<<2)|0;e=H[d+28>>2];c=j+(b<<6)|0;H[c+24>>2]=H[d+24>>2];H[c+28>>2]=e;e=H[d+20>>2];H[c+16>>2]=H[d+16>>2];H[c+20>>2]=e;e=H[d+12>>2];H[c+8>>2]=H[d+8>>2];H[c+12>>2]=e;e=H[d+4>>2];H[c>>2]=H[d>>2];H[c+4>>2]=e;d=b+1|0;c=j+(d<<6)|0;d=k+(N(d,g)<<2)|0;e=H[d+28>>2];H[c+24>>2]=H[d+24>>2];H[c+28>>2]=e;e=H[d+20>>2];H[c+16>>2]=H[d+16>>2];H[c+20>>2]=e;e=H[d+12>>2];H[c+8>>2]=H[d+8>>2];H[c+12>>2]=e;e=H[d+4>>2];H[c>>2]=H[d>>2];H[c+4>>2]=e;b=b+2|0;if((h|0)!=(b|0)){continue}break}}Ta(a);b=0;if(H[a+32>>2]){while(1){d=H[a>>2]+(b<<5)|0;c=H[d+28>>2];g=(N(H[a+36>>2],b)<<2)+i|0;H[g+24>>2]=H[d+24>>2];H[g+28>>2]=c;c=H[d+20>>2];H[g+16>>2]=H[d+16>>2];H[g+20>>2]=c;c=H[d+12>>2];H[g+8>>2]=H[d+8>>2];H[g+12>>2]=c;c=H[d+4>>2];H[g>>2]=H[d>>2];H[g+4>>2]=c;b=b+1|0;if(b>>>0>2]){continue}break}}i=i+32|0;l=l+8|0;if(l>>>0<=K[a+44>>2]){continue}break}}Ca(H[a>>2]);Ca(a)}function ed(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;d=b;b=H[b>>2]+7&-8;H[d>>2]=b+16;o=a;f=H[b>>2];a=H[b+4>>2];d=H[b+8>>2];c=H[b+12>>2];p=c;h=na-32|0;na=h;b=c&65535;e=c;c=d;d=e>>>16&32767;g=d;a:{if(d-15361>>>0<=2045){b=b<<4|c>>>28;d=c<<4|a>>>28;e=g-15360|0;a=a&268435455;b:{if((a|0)==134217728&(f|0)!=0|a>>>0>134217728){c=b;d=d+1|0;b=d?c:c+1|0;break b}if(f|(a|0)!=134217728){break b}a=d;d=d+(d&1)|0;b=a>>>0>d>>>0?b+1|0:b}c=b>>>0>1048575;f=c?0:d;a=c?0:b;b=0;c=c+e|0;b=c>>>0>>0?1:b;break a}if(!(!(c|f|(a|b))|((d|0)!=32767|(k|0)!=0))){e=b<<4|c>>>28;f=c<<4|a>>>28;a=e|524288;c=2047;b=0;break a}if(g>>>0>17406){f=0;a=0;c=2047;b=0;break a}e=!(d|k);l=e?15360:15361;k=l-g|0;if((k|0)>112){f=0;a=0;c=0;b=0;break a}d=c;b=e?b:b|65536;if((g|0)!=(l|0)){m=f;c=a;i=d;e=b;l=128-k|0;c:{if(l&64){g=f;e=l+-64|0;c=e&31;if((e&63)>>>0>=32){e=f<>>32-c|a<>>0>=32){g=i<>>32-j|e<>>0>=32){e=0;i=c>>>i|0}else{e=c>>>i|0;i=((1<>>i}i=n|i;e=e|g;n=m;j=l&31;if((l&63)>>>0>=32){g=m<>>32-j|c<>2]=m;H[h+20>>2]=c;H[h+24>>2]=i;H[h+28>>2]=e;m=(H[h+16>>2]|H[h+24>>2]|(H[h+20>>2]|H[h+28>>2]))!=0}d:{if(k&64){c=d;f=k+-64|0;a=f&31;if((f&63)>>>0>=32){e=0;f=b>>>a|0}else{e=b>>>a|0;f=((1<>>a}a=e;d=0;b=0;break d}if(!k){break d}g=d;c=64-k|0;e=c&31;if((c&63)>>>0>=32){c=d<>>32-e|b<>>0>=32){g=0;a=a>>>f|0}else{g=a>>>f|0;a=((1<>>f}f=i|a;a=c|g;e=d;d=k&31;if((k&63)>>>0>=32){c=0;d=b>>>d|0}else{c=b>>>d|0;d=((1<>>d}b=c}H[h>>2]=f;H[h+4>>2]=a;H[h+8>>2]=d;H[h+12>>2]=b;a=H[h+8>>2];d=H[h+4>>2];f=a<<4|d>>>28;a=H[h+12>>2]<<4|a>>>28;c=d&268435455;b=H[h>>2]|m;e:{if((c|0)==134217728&(b|0)!=0|c>>>0>134217728){f=f+1|0;a=f?a:a+1|0;break e}if(b|(c|0)!=134217728){break e}b=a;a=f;f=f+(f&1)|0;a=a>>>0>f>>>0?b+1|0:b}c=a>>>0>1048575;a=c?a^1048576:a;b=0}na=h+32|0;u(0,f|0);u(1,a|(p&-2147483648|c<<20));q=o,r=+w(),M[q>>3]=r}function Hc(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0;q=H[a+24>>2];if(!H[q+16>>2]){return 1}r=H[q+24>>2];o=H[H[H[a+20>>2]>>2]+20>>2];while(1){d=H[r+36>>2];H[b+36>>2]=d;f=H[o+28>>2];c=f+N(d,152)|0;t=H[a+64>>2];a:{if(t){d=f+N(H[o+24>>2],152)|0;p=H[d-144>>2]-H[d-152>>2]|0;f=c+12|0;e=c+4|0;d=H[c+8>>2];g=H[c>>2];c=36;break a}f=c+148|0;e=c+140|0;d=H[c+144>>2];g=H[c+136>>2];p=d-g|0;c=52}u=H[c+o>>2];b:{c:{if(!u){break c}k=H[e>>2];l=H[f>>2];f=d-g|0;e=H[b+40>>2];c=e&31;if((e&63)>>>0>=32){c=-1<>>32-c}m=h^-1;j=H[b+20>>2];h=m+j|0;i=c^-1;c=i;j=h>>>0>>0?c+1|0:c;c=e&31;if((e&63)>>>0>=32){h=j>>>c|0}else{h=((1<>>c}c=H[b+16>>2];j=c+m|0;m=c>>>0>j>>>0?i+1|0:i;i=e&31;c=H[b+8>>2];if((e&63)>>>0>=32){i=m>>>i|0}else{i=((1<>>i}e=c+i|0;d:{if(g>>>0>i>>>0){m=g-i|0;i=0;if(e>>>0>=d>>>0){j=0;d=f;break d}d=e-g|0;j=f-d|0;break d}i=i-g|0;if(e>>>0>=d>>>0){d=f-i|0;m=0;j=0;break d}j=d-e|0;m=0;d=c}f=l-k|0;e=H[b+12>>2];g=e+h|0;e:{if(h>>>0>>0){s=k-h|0;h=0;n=0;if(g>>>0>=l>>>0){break e}n=f;f=g-k|0;n=n-f|0;break e}h=h-k|0;if(g>>>0>=l>>>0){f=f-h|0;s=0;n=0;break e}s=0;f=e;n=l-g|0}k=n;g=0;if((h|i|(j|k)|(d|f))<0){break b}k=N(h,p)+i|0;l=H[b+44>>2];h=N(c,s)+m|0;f:{g:{if(!(k|l|(h|(c|0)!=(p|0))|(c|0)!=(d|0))){if((e|0)!=(f|0)){break g}d=(t?36:52)+o|0;H[b+44>>2]=H[d>>2];H[d>>2]=0;break c}if(l){break f}}re(e,0,c);if(qa|!e){break b}c=N(c,e);if(c>>>0>1073741823){break b}c=Ia(c<<2);H[b+44>>2]=c;if(!c){break b}e=H[b+8>>2];g=H[b+12>>2];if((e|0)==(d|0)&(g|0)==(f|0)){break f}e=N(e,g)<<2;if(!e){break f}y(c,0,e)}if(!f){break c}d=d<<2;g=H[b+44>>2]+(h<<2)|0;c=(k<<2)+u|0;if((f|0)!=1){k=f&1;l=f&2147483646;f=0;while(1){h=!d;if(!h){B(g,c,d)}i=p<<2;c=i+c|0;e=(H[b+8>>2]<<2)+g|0;if(!h){B(e,c,d)}c=c+i|0;g=e+(H[b+8>>2]<<2)|0;f=f+2|0;if((l|0)!=(f|0)){continue}break}if(!k){break c}}if(!d){break c}B(g,c,d)}o=o+76|0;r=r+52|0;b=b+52|0;g=1;v=v+1|0;if(v>>>0>2]){continue}}break}return g}function wb(a){a=a|0;var b=0,c=0,d=0,e=0,f=0,g=0;if(a){a:{if(H[a>>2]){b=H[a+12>>2];if(b){jb(b);Ca(H[a+12>>2]);H[a+12>>2]=0}b=H[a+16>>2];if(b){Ca(b);H[a+16>>2]=0;H[a+20>>2]=0}Ca(H[a+64>>2]);H[a+60>>2]=0;H[a+64>>2]=0;Ca(H[a+72>>2]);H[a+72>>2]=0;Ca(H[a+88>>2]);H[a+88>>2]=0;break a}b=H[a+44>>2];if(b){Ca(b);H[a+44>>2]=0}b=H[a+32>>2];if(b){Ca(b);H[a+32>>2]=0;H[a+36>>2]=0}b=H[a+52>>2];if(!b){break a}Ca(b);H[a+52>>2]=0;H[a+56>>2]=0}Tb(H[a+232>>2]);b=H[a+180>>2];if(b){e=N(H[a+128>>2],H[a+132>>2]);if(e){while(1){jb(b);b=b+5644|0;c=c+1|0;if((e|0)!=(c|0)){continue}break}b=H[a+180>>2]}Ca(b);H[a+180>>2]=0}b=H[a+140>>2];if(b){c=H[a+136>>2];if(c){b=0;while(1){e=H[H[a+140>>2]+(b<<3)>>2];if(e){Ca(e);c=H[a+136>>2]}b=b+1|0;if(c>>>0>b>>>0){continue}break}b=H[a+140>>2]}H[a+136>>2]=0;Ca(b);H[a+140>>2]=0}Ca(H[a+160>>2]);H[a+144>>2]=0;H[a+160>>2]=0;Ca(H[a+124>>2]);H[a+124>>2]=0;if(!(I[a+212|0]&2)){Ca(H[a+192>>2])}y(a+104|0,0,112);pb(H[a+216>>2]);H[a+216>>2]=0;pb(H[a+220>>2]);H[a+216>>2]=0;d=H[a+224>>2];if(d){b=H[d+28>>2];if(b){Ca(b);H[d+28>>2]=0}c=H[d+40>>2];if(c){if(H[d+36>>2]){while(1){e=N(g,40);b=H[(e+c|0)+36>>2];if(b){Ca(b);c=H[d+40>>2];H[(e+c|0)+36>>2]=0}b=H[(c+e|0)+16>>2];if(b){Ca(b);c=H[d+40>>2];H[(e+c|0)+16>>2]=0}b=H[(c+e|0)+24>>2];if(b){Ca(b);c=H[d+40>>2];H[(e+c|0)+24>>2]=0}g=g+1|0;if(g>>>0>2]){continue}break}}Ca(c);H[d+40>>2]=0}Ca(d)}H[a+224>>2]=0;Ua(H[a+96>>2]);H[a+96>>2]=0;Ua(H[a+100>>2]);H[a+100>>2]=0;f=H[a+236>>2];if(f){b:{if(!H[f+8>>2]){break b}if(H[f+12>>2]){H[f+40>>2]=0;while(1){if(H[f+24>>2]>0){continue}break}}H[f+16>>2]=1;Ca(H[f>>2]);c=H[f+28>>2];if(!c){break b}while(1){b=H[c+4>>2];Ca(c);H[f+28>>2]=b;c=b;if(b){continue}break}}d=H[f+36>>2];if(d){g=H[d+4>>2];if((g|0)>0){b=0;while(1){e=H[d>>2]+N(b,12)|0;c=H[e+8>>2];if(c){ra[c|0](H[e+4>>2]);g=H[d+4>>2]}b=b+1|0;if((g|0)>(b|0)){continue}break}}Ca(H[d>>2]);Ca(d)}Ca(f)}H[a+236>>2]=0;Ca(a)}}function ec(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0;k=na-256|0;na=k;a:{if(!a){a=0;break a}if(!(H[a>>2]==(b|0)&H[a+4>>2]==(c|0))){H[a+4>>2]=c;H[a>>2]=b;H[k>>2]=c;H[k+128>>2]=b;e=c;g=b;while(1){o=h;h=h+1|0;j=h<<2;l=(e+1|0)/2|0;H[j+k>>2]=l;n=j+(k+128|0)|0;j=(g+1|0)/2|0;H[n>>2]=j;i=N(e,g);f=i+f|0;e=l;g=j;if(i>>>0>1){continue}break}H[a+8>>2]=f;b:{c:{d:{if(!f){b=H[a+12>>2];if(!b){break d}Ca(b);H[a+12>>2]=0;break d}f=f<<4;if(f>>>0<=K[a+16>>2]){break b}b=Ha(H[a+12>>2],f);if(b){break c}Ba(d,1,6451,0);b=H[a+12>>2];if(!b){break d}Ca(b);H[a+12>>2]=0}Ca(a);a=0;break a}H[a+12>>2]=b;c=H[a+16>>2];d=f-c|0;if(d){y(b+c|0,0,d)}H[a+16>>2]=f;c=H[a+4>>2];b=H[a>>2]}g=H[a+12>>2];if(o){l=0;e=(N(b,c)<<4)+g|0;f=e;while(1){b=l<<2;h=H[b+k>>2];e:{if((h|0)<=0){break e}j=h-1|0;d=0;f:{g:{b=H[b+(k+128|0)>>2];if((b|0)<=0){i=h&3;if(h>>>0>=4){break g}h=0;break f}while(1){c=f;f=b;while(1){h:{H[g>>2]=e;if((f|0)==1){g=g+16|0;e=e+16|0;break h}H[g+16>>2]=e;e=e+16|0;g=g+32|0;i=(f|0)>2;f=f-2|0;if(i){continue}}break}i=((d|0)==(j|0)|d)&1;f=i?e:c+(b<<4)|0;e=i?e:c;d=d+1|0;if((h|0)!=(d|0)){continue}break}break e}m=h&2147483644;h=0;c=0;while(1){n=(h|0)==(j|0);h=h+4|0;e=n?e:f;f=e;c=c+4|0;if((m|0)!=(c|0)){continue}break}if(i){break f}break e}while(1){c=f;m=((h|0)==(j|0)|h)&1;f=m?e:c+(b<<4)|0;e=m?e:c;h=h+1|0;d=d+1|0;if((i|0)!=(d|0)){continue}break}}l=l+1|0;if((o|0)!=(l|0)){continue}break}}H[g>>2]=0}b=H[a+8>>2];if(!b){break a}c=b&3;e=H[a+12>>2];if(b>>>0>=4){b=b&-4;g=0;while(1){H[e+60>>2]=0;H[e+52>>2]=999;H[e+56>>2]=0;H[e+44>>2]=0;H[e+36>>2]=999;H[e+40>>2]=0;H[e+28>>2]=0;H[e+20>>2]=999;H[e+24>>2]=0;H[e+12>>2]=0;H[e+4>>2]=999;H[e+8>>2]=0;e=e- -64|0;g=g+4|0;if((b|0)!=(g|0)){continue}break}if(!c){break a}}g=0;while(1){H[e+12>>2]=0;H[e+4>>2]=999;H[e+8>>2]=0;e=e+16|0;g=g+1|0;if((c|0)!=(g|0)){continue}break}}na=k+256|0;return a}function Xd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0;g=na-16|0;na=g;if(H[a+8>>2]==16){h=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{h=H[a+12>>2]}a:{if(!c){Ba(d,1,4259,0);break a}i=H[a+96>>2];e=1;Da(b,g+8|0,1);f=H[g+8>>2];if(f>>>0>=2){Ba(d,2,9792,0);break a}if((f+1|0)!=(c|0)){e=0;Ba(d,2,4259,0);break a}c=H[i+16>>2];b:{if(!c){break b}d=c&7;e=H[h+5584>>2];if(c>>>0>=8){i=c&-8;c=0;while(1){H[e+8636>>2]=0;H[e+7556>>2]=0;H[e+6476>>2]=0;H[e+5396>>2]=0;H[e+4316>>2]=0;H[e+3236>>2]=0;H[e+2156>>2]=0;H[e+1076>>2]=0;e=e+8640|0;c=c+8|0;if((i|0)!=(c|0)){continue}break}if(!d){break b}}c=0;while(1){H[e+1076>>2]=0;e=e+1080|0;c=c+1|0;if((d|0)!=(c|0)){continue}break}}c=H[h+5608>>2];if(c){Ca(c);H[h+5608>>2]=0;f=H[g+8>>2]}if(!f){e=1;break a}i=0;while(1){b=b+1|0;Da(b,g+12|0,1);c:{if(!H[h+5632>>2]){break c}d=H[h+5628>>2];if(H[d>>2]!=H[g+12>>2]){break c}f=H[d+4>>2];j=H[a+96>>2];if((f|0)!=H[j+16>>2]){break c}c=H[d+8>>2];if(c){e=0;f=N(f,f);if(H[c+16>>2]!=(N(f,H[(H[c>>2]<<2)+24896>>2])|0)){break a}k=Fa(f<<2);H[h+5608>>2]=k;if(!k){break a}ra[H[(H[c>>2]<<2)+25200>>2]](H[c+12>>2],k,f)}c=H[d+12>>2];if(!c){break c}e=0;d=H[j+16>>2];if(H[c+16>>2]!=(N(d,H[(H[c>>2]<<2)+24896>>2])|0)){break a}f=Fa(d<<2);if(!f){break a}ra[H[(H[c>>2]<<2)+25216>>2]](H[c+12>>2],f,d);c=H[j+16>>2];d:{if(!c){break d}j=c&7;e=H[h+5584>>2];e:{if(c>>>0<8){c=f;break e}k=c&-8;d=0;c=f;while(1){H[e+1076>>2]=H[c>>2];H[e+2156>>2]=H[c+4>>2];H[e+3236>>2]=H[c+8>>2];H[e+4316>>2]=H[c+12>>2];H[e+5396>>2]=H[c+16>>2];H[e+6476>>2]=H[c+20>>2];H[e+7556>>2]=H[c+24>>2];H[e+8636>>2]=H[c+28>>2];e=e+8640|0;c=c+32|0;d=d+8|0;if((k|0)!=(d|0)){continue}break}if(!j){break d}}d=0;while(1){H[e+1076>>2]=H[c>>2];e=e+1080|0;c=c+4|0;d=d+1|0;if((j|0)!=(d|0)){continue}break}}Ca(f)}e=1;i=i+1|0;if(i>>>0>2]){continue}break}}na=g+16|0;return e|0}function xb(a,b,c,d,e,f,g,h){var i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;m=H[H[a+24>>2]+24>>2]+N(b,52)|0;l=H[m+4>>2];k=l-1|0;o=H[a+60>>2];j=k+o|0;p=0-!l|0;i=p;r=H[H[H[a+20>>2]>>2]+20>>2]+N(b,76)|0;n=H[r+12>>2];i=ve(j,j>>>0>>0?i+1|0:i,l,0);q=i>>>0>n>>>0?n:i;j=H[m>>2];m=j-1|0;s=H[a+56>>2];n=m+s|0;o=0-!j|0;i=o;t=H[r+8>>2];i=ve(n,n>>>0>>0?i+1|0:i,j,0);n=i>>>0>t>>>0?t:i;i=p;t=H[r+4>>2];s=H[a+52>>2];k=s+k|0;i=ve(k,k>>>0>>0?i+1|0:i,l,0);k=i>>>0>>0?t:i;i=o;p=H[r>>2];l=m;m=H[a+48>>2];l=l+m|0;i=ve(l,l>>>0>>0?i+1|0:i,j,0);i=i>>>0

>>0?p:i;l=0;p=H[(H[H[a+32>>2]+5584>>2]+N(b,1080)|0)+20>>2];c=H[r+20>>2]+(c?0-c|0:-1)|0;a:{if(!c){a=n;l=i;b=k;break a}m=c-1|0;j=(d&1)<>>0>>0){a=c&31;l=i-j|0;if((c&63)>>>0>=32){i=-1<>>32-a;a=-1<>>0>>0?i+1|0:i;b=a;a=c&31;if((c&63)>>>0>=32){l=i>>>a|0}else{l=((1<>>a}}a=0;b=0;d=d>>>1<>>0>>0){b=c&31;o=k-d|0;if((c&63)>>>0>=32){i=-1<>>32-b;b=-1<>>0>>0?i+1|0:i;k=b;b=c&31;if((c&63)>>>0>=32){b=i>>>b|0}else{b=((1<>>b}}if(j>>>0>>0){a=c&31;k=n-j|0;if((c&63)>>>0>=32){i=-1<>>32-a;a=-1<>>0>>0?i+1|0:i;j=a;a=c&31;if((c&63)>>>0>=32){a=i>>>a|0}else{a=((1<>>a}}if(d>>>0>=q>>>0){q=0;break a}k=q-d|0;d=c&31;if((c&63)>>>0>=32){i=-1<>>32-d;d=-1<>>0>>0?i+1|0:i;j=d;d=c&31;if((c&63)>>>0>=32){q=i>>>d|0}else{q=((1<>>d}}c=(p|0)==1?2:3;d=c+a|0;d=(a>>>0>d>>>0?-1:d)>>>0>e>>>0;a=c+q|0;d=d&(a>>>0>>0?-1:a)>>>0>f>>>0;a=l-c|0;d=d&(a>>>0<=l>>>0?a:0)>>>0>>0;a=b-c|0;return d&(a>>>0<=b>>>0?a:0)>>>0>>0}function lb(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;g=H[a+16>>2];if(g>>>0>=32){return H[a+8>>2]}b=H[a+20>>2];a:{if((b|0)>=4){d=H[a>>2];c=H[d-3>>2];e=b-4|0;H[a+20>>2]=e;H[a>>2]=d-4;break a}if((b|0)<=0){e=b;break a}f=H[a>>2];d=24;b:{if((b|0)==1){break b}l=b&1;k=b&2147483646;while(1){h=f-1|0;H[a>>2]=h;i=I[f|0];f=f-2|0;H[a>>2]=f;H[a+20>>2]=b-1;h=I[h|0];b=b-2|0;H[a+20>>2]=b;c=i<>2]=f-1;f=I[f|0];H[a+20>>2]=b-1;c=f<>2];k=c&255;H[a+24>>2]=k>>>0>143;b=b?(c&2130706432)==2130706432?7:8:8;h=b+(c>>>0<=2415919103?8:(c&8323072)==8323072?7:8)|0;f=c>>>16&255;i=h+(f>>>0<=143?8:(c&32512)==32512?7:8)|0;d=c>>>8&255;l=i+(g+(d>>>0<=143?8:(c&127)==127?7:8)|0)|0;H[a+16>>2]=l;j=H[a+12>>2];b=f<>>24|d<>>0>=32){d=b<>>32-c;b=b<>2];b=d|j;h=b;H[a+8>>2]=g;H[a+12>>2]=b;if(l>>>0<=31){c:{if((e|0)>=4){b=H[a>>2];c=H[b-3>>2];H[a+20>>2]=e-4;H[a>>2]=b-4;break c}if((e|0)<=0){c=0;break c}b=H[a>>2];d:{if((e|0)==1){d=24;c=0;break d}i=e&1;j=e&2147483646;d=24;c=0;f=0;while(1){m=b-1|0;H[a>>2]=m;n=I[b|0];b=b-2|0;H[a>>2]=b;H[a+20>>2]=e-1;m=I[m|0];e=e-2|0;H[a+20>>2]=e;c=n<>2]=b-1;b=I[b|0];H[a+20>>2]=e-1;c=b<>2]=e>>>0>143;k=k>>>0<=143?8:(c&2130706432)==2130706432?7:8;i=k+(c>>>0<=2415919103?8:(c&8323072)==8323072?7:8)|0;f=c>>>16&255;j=i+(f>>>0<=143?8:(c&32512)==32512?7:8)|0;d=c>>>8&255;H[a+16>>2]=j+(l+(d>>>0<=143?8:(c&127)==127?7:8)|0);b=a;a=f<>>24|d<>>0>=32){c=a<>>32-e;a=a<>2]=g;H[b+12>>2]=c|h}return g}function Sc(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;j=H[a+96>>2];l=N(H[a+128>>2],H[a+132>>2]);a:{if(l){b=H[j+16>>2];m=N(b,1080);k=N(b,b)<<2;e=H[a+12>>2];b=H[a+180>>2];while(1){n=H[b+5584>>2];B(b,e,5644);H[b+5608>>2]=0;H[b+5588>>2]=-1;H[b+5168>>2]=0;H[b+5636>>2]=0;H[b+5616>>2]=0;H[b+5624>>2]=0;H[b+5628>>2]=0;H[b+5584>>2]=n;F[b+5640|0]=I[b+5640|0]&252;b:{if(!H[e+5608>>2]){break b}d=Fa(k);H[b+5608>>2]=d;if(!d){return 0}if(!k){break b}B(d,H[e+5608>>2],k)}d=N(H[e+5624>>2],20);f=Fa(d);H[b+5616>>2]=f;i=0;if(!f){break a}if(d){B(f,H[e+5616>>2],d)}g=H[e+5620>>2];if(g){d=H[e+5616>>2];f=H[b+5616>>2];h=0;while(1){if(H[d+12>>2]){g=Fa(H[d+16>>2]);H[f+12>>2]=g;if(!g){return 0}o=H[d+16>>2];if(o){B(g,H[d+12>>2],o)}g=H[e+5620>>2]}H[b+5624>>2]=H[b+5624>>2]+1;f=f+20|0;d=d+20|0;h=h+1|0;if(h>>>0>>0){continue}break}}d=N(H[e+5636>>2],20);f=Fa(d);H[b+5628>>2]=f;if(!f){break a}if(d){B(f,H[e+5628>>2],d)}i=H[e+5636>>2];H[b+5636>>2]=i;if(i){d=H[e+5628>>2];f=H[b+5628>>2];h=0;while(1){g=H[d+8>>2];if(g){H[f+8>>2]=H[b+5616>>2]+(g-H[e+5616>>2]|0)}g=H[d+12>>2];if(g){H[f+12>>2]=H[b+5616>>2]+(g-H[e+5616>>2]|0)}f=f+20|0;d=d+20|0;h=h+1|0;if((i|0)!=(h|0)){continue}break}}if(m){B(n,H[e+5584>>2],m)}b=b+5644|0;p=p+1|0;if((p|0)!=(l|0)){continue}break}}i=1;e=Ea(1,72);b=0;c:{if(!e){break c}F[e+40|0]=I[e+40|0]&254|1;d=Ea(1,4);H[e+20>>2]=d;b=e;if(d){break c}Ca(b);b=0}H[a+232>>2]=b;if(!b){return 0}f=H[a+236>>2];e=0;H[b+28>>2]=a+104;H[b+24>>2]=j;d=Ea(1,848);H[H[b+20>>2]>>2]=d;d:{if(!d){break d}d=Ea(H[j+16>>2],76);h=H[H[b+20>>2]>>2];H[h+20>>2]=d;if(!d){break d}H[h+16>>2]=H[j+16>>2];e=H[a+188>>2];H[b+44>>2]=f;H[b>>2]=e;e=1}if(e){break a}Tb(H[a+232>>2]);i=0;H[a+232>>2]=0;Ba(c,1,3668,0)}return i|0}function pe(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;e=na-80|0;na=e;H[e+76>>2]=1;j=H[a+44>>2];d=H[H[a+224>>2]+40>>2];a:{b:{if(!d|!H[d+16>>2]){break b}c:{d=d+N(j,40)|0;if(!H[d+4>>2]){d=H[a+52>>2];f=H[a+48>>2]+2|0;d=f>>>0<2?d+1|0:d;if(bb(b,f,d,c)){break c}d=0;Ba(c,1,5440,0);break a}d=H[d+16>>2];if(!bb(b,H[d>>2],H[d+4>>2],c)){Ba(c,1,5440,0);d=0;break a}if((Ja(b,H[a+16>>2],2,c)|0)!=2){Ba(c,1,2472,0);d=0;break a}Da(H[a+16>>2],e+72|0,2);if(H[e+72>>2]==65424){break c}Ba(c,1,4073,0);d=0;break a}if(H[a+8>>2]!=256){break b}H[a+8>>2]=8}f=N(H[a+132>>2],H[a+128>>2]);d:{if(!f){break d}i=f&7;h=H[a+180>>2];d=0;if(f>>>0>=8){k=f&-8;while(1){f=h+N(g,5644)|0;H[f+45096>>2]=-1;H[f+39452>>2]=-1;H[f+33808>>2]=-1;H[f+28164>>2]=-1;H[f+22520>>2]=-1;H[f+16876>>2]=-1;H[f+11232>>2]=-1;H[f+5588>>2]=-1;g=g+8|0;l=l+8|0;if((k|0)!=(l|0)){continue}break}if(!i){break d}}while(1){H[(h+N(g,5644)|0)+5588>>2]=-1;g=g+1|0;d=d+1|0;if((i|0)!=(d|0)){continue}break}}d=0;if(!cb(a,e+72|0,0,e+68|0,e- -64|0,e+60|0,e+56|0,e+52|0,e+76|0,b,c)){break a}i=j+1|0;while(1){e:{if(!H[e+76>>2]){break e}f=H[e+72>>2];if(!ib(a,f,0,0,b,c)){break a}h=H[a+128>>2];k=H[a+132>>2];g=f+1|0;H[e+32>>2]=g;H[e+36>>2]=N(h,k);Ba(c,4,11795,e+32|0);if(!Hc(H[a+232>>2],H[H[a+100>>2]+24>>2])){break a}d=H[a+180>>2]+N(f,5644)|0;h=H[d+5596>>2];if(h){Ca(h);H[d+5596>>2]=0;H[d+5600>>2]=0}H[e+16>>2]=g;Ba(c,4,16601,e+16|0);if((f|0)==(j|0)){d=H[a+224>>2];f=H[d+8>>2];d=H[d+12>>2];f=f+2|0;d=f>>>0<2?d+1|0:d;if(bb(b,f,d,c)){break e}d=0;Ba(c,1,5440,0);break a}H[e+4>>2]=i;H[e>>2]=g;Ba(c,2,13648,e);d=0;if(cb(a,e+72|0,0,e+68|0,e- -64|0,e+60|0,e+56|0,e+52|0,e+76|0,b,c)){continue}break a}break}d=Gc(a,c)}na=e+80|0;return d|0}function Ma(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;h=H[a+16>>2];if(h>>>0>=32){return H[a+8>>2]}c=H[a+24>>2];a:{if((c|0)>=4){b=H[a>>2];d=H[b>>2];e=c-4|0;H[a+24>>2]=e;H[a>>2]=b+4;break a}d=H[a+28>>2]?-1:0;if((c|0)<=0){e=c;break a}b=H[a>>2];if((c|0)!=1){j=c&1;i=c&2147483646;while(1){e=b;H[a>>2]=b+1;k=I[b|0];b=b+2|0;H[a>>2]=b;H[a+24>>2]=c-1;e=I[e+1|0];c=c-2|0;H[a+24>>2]=c;d=((255<>2]=b+1;b=I[b|0];H[a+24>>2]=c-1;d=(255<>2];i=d>>>24|0;H[a+20>>2]=(i|0)==255;c=d>>>16&255;f=d>>>8&255;b=b?7:8;d=d&255;g=b+((d|0)==255?7:8)|0;k=((f|0)==255?7:8)+g|0;j=(h+((c|0)==255?7:8)|0)+k|0;H[a+16>>2]=j;l=H[a+12>>2];b=d|(f<>>0>=32){d=b<>>32-c;b=b<>2];b=d|l;k=b;H[a+8>>2]=h;H[a+12>>2]=b;if(j>>>0<=31){b:{if((e|0)>=4){b=H[a>>2];d=H[b>>2];H[a+24>>2]=e-4;H[a>>2]=b+4;break b}f=0;d=H[a+28>>2]?-1:0;if((e|0)<=0){break b}b=H[a>>2];if((e|0)!=1){l=e&1;m=e&2147483646;g=0;while(1){c=b;H[a>>2]=b+1;n=I[b|0];b=b+2|0;H[a>>2]=b;H[a+24>>2]=e-1;c=I[c+1|0];e=e-2|0;H[a+24>>2]=e;d=((255<>2]=b+1;b=I[b|0];H[a+24>>2]=e-1;d=(255<>>24|0;H[a+20>>2]=(e|0)==255;c=d>>>16&255;f=d>>>8&255;g=(i|0)==255?7:8;d=d&255;i=g+((d|0)==255?7:8)|0;l=((f|0)==255?7:8)+i|0;H[a+16>>2]=(((c|0)==255?7:8)+j|0)+l;b=a;a=d|(f<>>0>=32){c=a<>>32-e;a=a<>2]=h;H[b+12>>2]=c|k}return h}function Lc(a,b,c,d,e){var f=0,g=0,h=0,i=0,j=0,k=0;h=na-32|0;na=h;if(H[a+8>>2]==16){f=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{f=H[a+12>>2]}a:{if(K[d>>2]<=4){a=0;Ba(e,1,2607,0);break a}f=H[f+5584>>2]+N(b,1080)|0;Da(c,f+4|0,1);g=H[f+4>>2]+1|0;H[f+4>>2]=g;if(g>>>0>=34){H[h+4>>2]=33;H[h>>2]=g;Ba(e,1,7635,h);a=0;break a}j=H[a+184>>2];if(j>>>0>=g>>>0){H[h+24>>2]=g;H[h+20>>2]=j;H[h+16>>2]=b;Ba(e,1,16423,h+16|0);H[a+8>>2]=H[a+8>>2]|32768;a=0;break a}Da(c+1|0,f+8|0,1);H[f+8>>2]=H[f+8>>2]+2;Da(c+2|0,f+12|0,1);a=H[f+12>>2]+2|0;H[f+12>>2]=a;b=H[f+8>>2];if(!(!(b>>>0>10|a>>>0>10)&a+b>>>0<13)){a=0;Ba(e,1,5468,0);break a}Da(c+3|0,f+16|0,1);if(I[f+16|0]&128){a=0;Ba(e,1,6564,0);break a}Da(c+4|0,f+20|0,1);if(K[f+20>>2]>=2){a=0;Ba(e,1,6499,0);break a}b=H[d>>2]-5|0;H[d>>2]=b;a=1;g=H[f+4>>2];if(!(F[f|0]&1)){if(!g){break a}j=g&3;d=f+944|0;e=f+812|0;b=0;c=0;if(g>>>0>=4){k=g&-4;g=0;while(1){f=c<<2;H[f+e>>2]=15;H[d+f>>2]=15;i=f|4;H[i+e>>2]=15;H[d+i>>2]=15;i=f|8;H[i+e>>2]=15;H[d+i>>2]=15;f=f|12;H[f+e>>2]=15;H[d+f>>2]=15;c=c+4|0;g=g+4|0;if((k|0)!=(g|0)){continue}break}if(!j){break a}}while(1){a=c<<2;H[a+e>>2]=15;H[a+d>>2]=15;a=1;c=c+1|0;b=b+1|0;if((j|0)!=(b|0)){continue}break}break a}if(b>>>0>=g>>>0){b:{if(!g){g=0;break b}Da(c+5|0,h+28|0,1);a=H[h+28>>2];H[f+944>>2]=a>>>4;H[f+812>>2]=a&15;g=H[f+4>>2];if(g>>>0>=2){j=f+944|0;k=f+812|0;a=c+6|0;c=1;while(1){Da(a,h+28|0,1);c:{b=H[h+28>>2];if(b>>>0>=16){g=b&15;if(g){break c}}a=0;Ba(e,1,6025,0);break a}i=c<<2;H[i+k>>2]=g;H[j+i>>2]=b>>>4;a=a+1|0;c=c+1|0;g=H[f+4>>2];if(c>>>0>>0){continue}break}}b=H[d>>2]}H[d>>2]=b-g;a=1;break a}a=0;Ba(e,1,2607,0)}na=h+32|0;return a}function gc(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0;j=na-256|0;na=j;g=Ea(1,20);a:{if(!g){Ba(c,1,6413,0);g=0;break a}H[g+4>>2]=b;H[g>>2]=a;H[j>>2]=b;H[j+128>>2]=a;while(1){p=h;h=h+1|0;e=h<<2;d=(b+1|0)/2|0;H[e+j>>2]=d;i=e+(j+128|0)|0;e=(a+1|0)/2|0;H[i>>2]=e;i=N(a,b);f=i+f|0;b=d;a=e;if(i>>>0>1){continue}break}H[g+8>>2]=f;if(!f){Ca(g);g=0;break a}d=Ea(f,16);H[g+12>>2]=d;if(!d){Ba(c,1,3564,0);Ca(g);g=0;break a}l=H[g+8>>2];H[g+16>>2]=l<<4;a=d;if(p){f=(N(H[g+4>>2],H[g>>2])<<4)+d|0;b=f;while(1){c=n<<2;e=H[c+j>>2];b:{if((e|0)<=0){break b}o=e-1|0;i=0;c:{d:{c=H[c+(j+128|0)>>2];if((c|0)<=0){k=e&3;if(e>>>0>=4){break d}h=0;break c}while(1){h=f;f=c;while(1){e:{H[a>>2]=b;if((f|0)==1){a=a+16|0;b=b+16|0;break e}H[a+16>>2]=b;b=b+16|0;a=a+32|0;k=(f|0)>2;f=f-2|0;if(k){continue}}break}k=((i|0)==(o|0)|i)&1;f=k?b:h+(c<<4)|0;b=k?b:h;i=i+1|0;if((e|0)!=(i|0)){continue}break}break b}m=e&2147483644;h=0;e=0;while(1){q=(h|0)==(o|0);h=h+4|0;f=q?b:f;b=f;e=e+4|0;if((m|0)!=(e|0)){continue}break}if(k){break c}break b}while(1){e=f;m=((h|0)==(o|0)|h)&1;f=m?b:e+(c<<4)|0;b=m?b:e;h=h+1|0;i=i+1|0;if((k|0)!=(i|0)){continue}break}}n=n+1|0;if((n|0)!=(p|0)){continue}break}}H[a>>2]=0;f:{if(!l){break f}a=l&3;if(l>>>0>=4){c=l&-4;b=0;while(1){H[d+60>>2]=0;H[d+52>>2]=999;H[d+56>>2]=0;H[d+44>>2]=0;H[d+36>>2]=999;H[d+40>>2]=0;H[d+28>>2]=0;H[d+20>>2]=999;H[d+24>>2]=0;H[d+12>>2]=0;H[d+4>>2]=999;H[d+8>>2]=0;d=d- -64|0;b=b+4|0;if((c|0)!=(b|0)){continue}break}if(!a){break f}}b=0;while(1){H[d+12>>2]=0;H[d+4>>2]=999;H[d+8>>2]=0;d=d+16|0;b=b+1|0;if((a|0)!=(b|0)){continue}break}}}na=j+256|0;return g}function Zb(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0;H[a+8>>2]=0;H[a+12>>2]=0;H[a>>2]=b;H[a+16>>2]=0;H[a+20>>2]=0;H[a+28>>2]=d;h=c-1|0;H[a+24>>2]=h;n=b&3;a:{if((c|0)<=0){e=b;b=d;break a}e=b+1|0;H[a>>2]=e;b=I[b|0]}g=b;i=8;H[a+16>>2]=8;j=(g|0)==255;H[a+20>>2]=j;H[a+8>>2]=g;H[a+12>>2]=0;b:{if((n|0)==3){break b}k=c-2|0;H[a+24>>2]=k;c:{if((c|0)<2){b=e;e=d;break c}b=e+1|0;H[a>>2]=b;e=I[e|0]}j=(e|0)==255;H[a+20>>2]=j;i=(g|0)==255?15:16;H[a+16>>2]=i;g=g|e<<8;H[a+8>>2]=g;H[a+12>>2]=0;if((n|0)==2){e=b;c=h;h=k;break b}o=c-3|0;H[a+24>>2]=o;d:{if((c|0)<3){f=b;b=d;break d}f=b+1|0;H[a>>2]=f;b=I[b|0]}j=(b|0)==255;H[a+20>>2]=j;l=((e|0)==255?7:8)+i|0;H[a+16>>2]=l;e=i&31;if((i&63)>>>0>=32){m=b<>>32-e;e=b<>2]=g;H[a+12>>2]=m;if((n|0)==1){e=f;i=l;c=k;h=o;break b}h=c-4|0;H[a+24>>2]=h;e:{if((c|0)<4){e=f;c=d;break e}e=f+1|0;H[a>>2]=e;c=I[f|0]}j=(c|0)==255;H[a+20>>2]=j;i=l+((b|0)==255?7:8)|0;H[a+16>>2]=i;b=l&31;if((l&63)>>>0>=32){f=c<>>32-b;b=c<>2]=g;H[a+12>>2]=b;c=o}f:{if((c|0)>=5){d=H[e>>2];H[a+24>>2]=c-5;H[a>>2]=e+4;break f}b=0;d=d?-1:0;if((c|0)<2){break f}while(1){c=e+1|0;H[a>>2]=c;e=I[e|0];f=h-1|0;H[a+24>>2]=f;d=(255<>>0>1;e=c;h=f;if(k){continue}break}}b=d>>>24|0;H[a+20>>2]=(b|0)==255;c=d>>>16&255;e=d>>>8&255;h=j?7:8;d=d&255;f=h+((d|0)==255?7:8)|0;k=((e|0)==255?7:8)+f|0;H[a+16>>2]=(((c|0)==255?7:8)+i|0)+k;b=d|(e<>>0>=32){d=a<>>32-b;a=a<>2]=a|g;H[c+12>>2]=d|m}function Ha(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0;if(!b){return 0}a:{if(!a){a=gb(8,b);break a}if(!b){Ca(a);a=0;break a}b:{if(b>>>0>4294967239){break b}h=b>>>0<=8?8:b+3&-4;d=h+8|0;j=a-4|0;f=j;e=H[f>>2];b=e+f|0;c=H[b>>2];c:{d:{e:{if((c|0)!=H[(b+c|0)-4>>2]){c=c+e|0;if(c>>>0>=d+16>>>0){e=H[b+4>>2];b=H[b+8>>2];H[e+8>>2]=b;H[b+4>>2]=e;b=d+f|0;c=c-d|0;H[b>>2]=c;H[(b+(c&-4)|0)-4>>2]=c|1;e=H[b>>2]-8|0;f:{if(e>>>0<=127){g=(e>>>3|0)-1|0;break f}c=Q(e);g=((e>>>29-c^4)-(c<<2)|0)+110|0;if(e>>>0<=4095){break f}c=((e>>>30-c^2)-(c<<1)|0)+71|0;g=c>>>0>=63?63:c}c=g;e=c<<4;H[b+4>>2]=e+26400;e=e+26408|0;H[b+8>>2]=H[e>>2];H[e>>2]=b;H[H[b+8>>2]+4>>2]=b;e=H[6859];b=c&31;if((c&63)>>>0>=32){c=1<>>32-b}H[6858]=g|H[6858];H[6859]=c|e;H[f>>2]=d;H[(f+(d&-4)|0)-4>>2]=d;c=1;break c}if(c>>>0>>0){break e}d=H[b+4>>2];b=H[b+8>>2];H[d+8>>2]=b;H[b+4>>2]=d;H[f>>2]=c;H[(f+(c&-4)|0)-4>>2]=c;c=1;break c}if(e>>>0>=d+16>>>0){H[f>>2]=d;H[(f+(d&-4)|0)-4>>2]=d;b=d+f|0;c=e-d|0;H[b>>2]=c;H[(b+(c&-4)|0)-4>>2]=c|1;d=H[b>>2]-8|0;g:{if(d>>>0<=127){c=(d>>>3|0)-1|0;break g}f=Q(d);c=((d>>>29-f^4)-(f<<2)|0)+110|0;if(d>>>0<=4095){break g}c=((d>>>30-f^2)-(f<<1)|0)+71|0;c=c>>>0>=63?63:c}d=c<<4;H[b+4>>2]=d+26400;d=d+26408|0;H[b+8>>2]=H[d>>2];H[d>>2]=b;H[H[b+8>>2]+4>>2]=b;d=H[6859];b=c&31;if((c&63)>>>0>=32){c=1<>>32-b;b=e}H[6858]=b|H[6858];H[6859]=c|d;c=1;break c}c=1;if(d>>>0<=e>>>0){break d}}c=0}}if(c){break a}b=gb(8,h);if(!b){break b}i=H[j>>2]-8|0;ab(b,a,h>>>0>>0?h:i);Ca(a);i=b}a=i}return a}function ne(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;f=na-16|0;na=f;H[f+12>>2]=c;k=H[a+96>>2];if(H[a+8>>2]==16){e=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{e=H[a+12>>2]}F[e+5640|0]=I[e+5640|0]|1;a:{if(c>>>0<=4){c=0;Ba(d,1,4565,0);break a}Da(b,e,1);if(K[e>>2]>=8){c=0;Ba(d,1,4531,0);break a}Da(b+1|0,f+8|0,1);c=H[f+8>>2];H[e+4>>2]=c;if((c|0)>=5){Ba(d,1,4490,0);H[e+4>>2]=-1}Da(b+2|0,e+8|0,2);g=H[e+8>>2];if(g-65536>>>0<=4294901760){H[f>>2]=g;Ba(d,1,8111,f);c=0;break a}c=H[a+188>>2];H[e+12>>2]=c?c:g;Da(b+4|0,e+16|0,1);if(K[e+16>>2]>=2){c=0;Ba(d,1,5536,0);break a}g=b+5|0;H[f+12>>2]=H[f+12>>2]-5;c=H[k+16>>2];b:{if(!c){break b}k=c&7;h=H[e>>2]&1;e=H[e+5584>>2];b=0;if(c>>>0>=8){c=c&-8;while(1){i=e+N(b,1080)|0;H[i+7560>>2]=h;H[i+6480>>2]=h;H[i+5400>>2]=h;H[i+4320>>2]=h;H[i+3240>>2]=h;H[i+2160>>2]=h;H[i+1080>>2]=h;H[i>>2]=h;b=b+8|0;j=j+8|0;if((c|0)!=(j|0)){continue}break}if(!k){break b}}while(1){H[e+N(b,1080)>>2]=h;b=b+1|0;l=l+1|0;if((k|0)!=(l|0)){continue}break}}c=0;if(!Lc(a,0,g,f+12|0,d)){Ba(d,1,4565,0);break a}if(H[f+12>>2]){Ba(d,1,4565,0);break a}if(H[a+8>>2]==16){b=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{b=H[a+12>>2]}if(K[H[a+96>>2]+16>>2]>=2){b=H[b+5584>>2];g=H[b+4>>2]<<2;l=b+944|0;e=b+812|0;j=1;c=b;while(1){H[c+1084>>2]=H[b+4>>2];H[c+1088>>2]=H[b+8>>2];H[c+1092>>2]=H[b+12>>2];H[c+1096>>2]=H[b+16>>2];H[c+1100>>2]=H[b+20>>2];d=!g;if(!d){B(c+1892|0,e,g)}if(!d){B(c+2024|0,l,g)}c=c+1080|0;j=j+1|0;if(j>>>0>2]+16>>2]){continue}break}}c=1}na=f+16|0;return c|0}function gb(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;j=a>>>0<=8?8:a;k=j+48|0;a:{b:{while(1){if(a-1&a|b>>>0>4294967239){break b}c=H[6859];e=c;h=H[6858];b=b>>>0<=8?8:b+3&-4;c:{if(b>>>0<=127){i=(b>>>3|0)-1|0;break c}d=Q(b);i=((b>>>29-d^4)-(d<<2)|0)+110|0;if(b>>>0<=4095){break c}d=((b>>>30-d^2)-(d<<1)|0)+71|0;i=d>>>0>=63?63:d}g=i;f=g&31;if((g&63)>>>0>=32){d=0;c=c>>>f|0}else{d=c>>>f|0;c=((1<>>f}if(c|d){while(1){f=d;d:{if(f|c){e=f-1|0;i=e+1|0;d=e;e=c-1|0;h=(e|0)!=-1?i:d;d=Q(f^h);d=(d|0)==32?Q(c^e)+32|0:d;e=63-d|0;qa=0-(d>>>0>63)|0;break d}qa=0;e=64}h=e;e=h&31;if((h&63)>>>0>=32){d=0;i=f>>>e|0}else{d=f>>>e|0;i=((1<>>e}g=g+h|0;c=g<<4;f=H[c+26408>>2];e=c+26400|0;e:{if((f|0)!=(e|0)){c=Cb(f,j,b);if(c){break a}c=H[f+4>>2];h=H[f+8>>2];H[c+8>>2]=h;H[h+4>>2]=c;H[f+8>>2]=e;H[f+4>>2]=H[e+4>>2];H[e+4>>2]=f;H[H[f+4>>2]+8>>2]=f;g=g+1|0;c=(d&1)<<31|i>>>1;d=d>>>1|0;break e}c=H[6859];l=27432,m=H[6858]&ye(-2,-1,g),H[l>>2]=m;H[6859]=qa&c;c=i^1}if(c|d){continue}break}h=H[6858];e=H[6859]}d=Q(e);f=63-((d|0)==32?Q(h)+32|0:d)|0;f:{if(!(e|h)){g=0;break f}c=f<<4;g=H[c+26408>>2];if(!e&h>>>0<1073741824){break f}d=98;e=c+26400|0;if((e|0)==(g|0)){break f}while(1){c=Cb(g,j,b);if(c){break a}g=H[g+8>>2];if((e|0)==(g|0)){break f}c=d;d=c-1|0;if(c){continue}break}}d=a>>>0>8;a=j;if(zc((d?k:48)+b|0)){continue}break}if(!g){break b}a=(f<<4)+26400|0;if((a|0)==(g|0)){break b}while(1){c=Cb(g,j,b);if(c){break a}g=H[g+8>>2];if((a|0)!=(g|0)){continue}break}}c=0}return c}function ud(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;e=H[a+48>>2];if(e>>>0>=b>>>0){H[a+48>>2]=e-b;H[a+36>>2]=H[a+36>>2]+b;e=c+H[a+60>>2]|0;d=b+H[a+56>>2]|0;e=d>>>0>>0?e+1|0:e;H[a+56>>2]=d;H[a+60>>2]=e;qa=c;return b|0}if(I[a+68|0]&4){H[a+48>>2]=0;H[a+36>>2]=e+H[a+36>>2];g=H[a+60>>2];c=H[a+56>>2];b=c+e|0;H[a+56>>2]=b;H[a+60>>2]=b>>>0>>0?g+1|0:g;qa=e?0:-1;return(e?e:-1)|0}if(e){H[a+48>>2]=0;H[a+36>>2]=H[a+32>>2];h=b;f=e;b=b-e|0;c=c-(e>>>0>h>>>0)|0}a:{if((c|0)>0){h=1}else{h=!!b&(c|0)>=0}if(h){while(1){h=H[a+12>>2];e=c+g|0;i=b+f|0;e=H[a+60>>2]+(i>>>0>>0?e+1|0:e)|0;j=i;i=i+H[a+56>>2]|0;e=j>>>0>i>>>0?e+1|0:e;if((e|0)==(h|0)&i>>>0>K[a+8>>2]|e>>>0>h>>>0){Ba(d,4,15630,0);H[a+48>>2]=0;H[a+36>>2]=H[a+32>>2];b=g+H[a+60>>2]|0;c=f+H[a+56>>2]|0;b=c>>>0>>0?b+1|0:b;H[a+56>>2]=c;H[a+60>>2]=b;d=H[a+8>>2];f=d-c|0;e=H[a+12>>2];g=e-((c>>>0>d>>>0)+b|0)|0;h=ra[H[a+28>>2]](d,e,H[a>>2])|0;i=H[a+68>>2];if(h){H[a+56>>2]=d;H[a+60>>2]=e}H[a+68>>2]=i|4;a=(c|0)==(d|0)&(b|0)==(e|0);b=a?-1:f;break a}e=ra[H[a+24>>2]](b,c,H[a>>2])|0;h=qa;i=h;if((e&i)==-1){Ba(d,4,15630,0);H[a+68>>2]=H[a+68>>2]|4;e=g+H[a+60>>2]|0;b=f+H[a+56>>2]|0;e=b>>>0>>0?e+1|0:e;H[a+56>>2]=b;H[a+60>>2]=e;a=!(g|f);b=a?-1:f;break a}g=g+i|0;f=e+f|0;g=f>>>0>>0?g+1|0:g;h=b;b=b-e|0;c=c-((e>>>0>h>>>0)+i|0)|0;if(!!b&(c|0)>=0|(c|0)>0){continue}break}}b=g+H[a+60>>2]|0;c=f+H[a+56>>2]|0;b=c>>>0>>0?b+1|0:b;H[a+56>>2]=c;H[a+60>>2]=b;qa=g;return f|0}qa=a?-1:g;return b|0}function yd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;e=na-80|0;na=e;a:{if(c>>>0<=2){Ba(d,1,14478,0);break a}if(I[a+124|0]){Ba(d,4,11193,0);g=1;break a}g=1;Da(b,a+40|0,1);Da(b+1|0,a+52|0,1);Da(b+2|0,a+44|0,1);f=b+3|0;b:{c:{d:{e:{f:{h=H[a+40>>2];switch(h-1|0){case 0:break f;case 1:break e;default:break d}}if(c>>>0<=6){H[e+16>>2]=c;Ba(d,1,15155,e+16|0);g=0;break a}if(!((c|0)==7|H[a+48>>2]==14)){H[e+48>>2]=c;Ba(d,2,15155,e+48|0)}Da(f,a+48|0,4);if(H[a+48>>2]!=14){break b}f=Fa(36);if(!f){g=0;Ba(d,1,7993,0);break a}H[f>>2]=14;H[e+64>>2]=0;H[e+56>>2]=0;H[e+72>>2]=0;H[e+60>>2]=0;H[e+68>>2]=0;H[e+76>>2]=0;g=4470064;H[e+52>>2]=4470064;H[f+4>>2]=1145390592;g:{if((c|0)!=7){if((c|0)==35){Da(b+7|0,e+76|0,4);Da(b+11|0,e+72|0,4);Da(b+15|0,e+68|0,4);Da(b+19|0,e- -64|0,4);Da(b+23|0,e+60|0,4);Da(b+27|0,e+56|0,4);Da(b+31|0,e+52|0,4);H[f+4>>2]=0;g=H[e+52>>2];c=H[e+56>>2];d=H[e+64>>2];i=H[e+68>>2];j=H[e+76>>2];h=H[e+72>>2];b=H[e+60>>2];break g}H[e+32>>2]=c;Ba(d,2,15191,e+32|0)}c=0;d=0;h=0;b=0}H[f+24>>2]=b;H[f+16>>2]=i;H[f+8>>2]=j;H[f+32>>2]=g;H[f+28>>2]=c;H[f+20>>2]=d;H[f+12>>2]=h;H[a+112>>2]=0;H[a+108>>2]=f;break b}b=c-3|0;H[a+112>>2]=b;d=Ea(1,b);H[a+108>>2]=d;if(!d){break c}if((c|0)<=3){break b}c=0;while(1){Da(f,e+76|0,1);F[H[a+108>>2]+c|0]=H[e+76>>2];f=f+1|0;c=c+1|0;if((b|0)!=(c|0)){continue}break}break b}if(h>>>0<3){break a}H[e>>2]=h;Ba(d,4,15950,e);break a}g=0;H[a+112>>2]=0;break a}g=1;F[a+124|0]=1}na=e+80|0;return g|0}function Ja(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0;h=H[a+48>>2];if(h>>>0>=c>>>0){if(c){B(b,H[a+36>>2],c)}H[a+36>>2]=H[a+36>>2]+c;H[a+48>>2]=H[a+48>>2]-c;b=H[a+60>>2];d=H[a+56>>2]+c|0;b=d>>>0>>0?b+1|0:b;H[a+56>>2]=d;H[a+60>>2]=b;return c}if(I[a+68|0]&4){if(h){B(b,H[a+36>>2],h)}b=H[a+48>>2];H[a+48>>2]=0;H[a+36>>2]=b+H[a+36>>2];g=H[a+60>>2];c=b;b=H[a+56>>2]+b|0;g=c>>>0>b>>>0?g+1|0:g;H[a+56>>2]=b;H[a+60>>2]=g;return h?h:-1}a:{if(h){if(h){B(b,H[a+36>>2],h)}i=H[a+32>>2];H[a+36>>2]=i;e=H[a+48>>2];H[a+48>>2]=0;f=H[a+60>>2];g=H[a+56>>2]+e|0;f=g>>>0>>0?f+1|0:f;H[a+56>>2]=g;H[a+60>>2]=f;c=c-e|0;b=b+e|0;break a}i=H[a+32>>2];H[a+36>>2]=i}b:{while(1){c:{e=H[a>>2];f=H[a+16>>2];g=H[a+64>>2];d:{if(g>>>0>c>>>0){f=ra[f|0](i,g,e)|0;H[a+48>>2]=f;if((f|0)==-1){break b}if(c>>>0>f>>>0){if(f){B(b,H[a+36>>2],f)}i=H[a+32>>2];H[a+36>>2]=i;e=H[a+48>>2];break d}if(c){B(b,H[a+36>>2],c)}H[a+36>>2]=H[a+36>>2]+c;H[a+48>>2]=H[a+48>>2]-c;b=H[a+60>>2];d=H[a+56>>2]+c|0;b=d>>>0>>0?b+1|0:b;H[a+56>>2]=d;H[a+60>>2]=b;return c+h|0}e=ra[f|0](b,c,e)|0;H[a+48>>2]=e;if((e|0)==-1){break b}if(c>>>0<=e>>>0){break c}i=H[a+32>>2];H[a+36>>2]=i;f=e}H[a+48>>2]=0;g=H[a+60>>2];j=H[a+56>>2]+e|0;g=j>>>0>>0?g+1|0:g;H[a+56>>2]=j;H[a+60>>2]=g;b=b+e|0;c=c-e|0;h=f+h|0;continue}break}H[a+48>>2]=0;H[a+36>>2]=H[a+32>>2];f=H[a+60>>2];b=H[a+56>>2]+e|0;f=b>>>0>>0?f+1|0:f;H[a+56>>2]=b;H[a+60>>2]=f;return e+h|0}Ba(d,4,15630,0);H[a+48>>2]=0;H[a+68>>2]=H[a+68>>2]|4;return h?h:-1}function Ta(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;a:{b:{if(!H[a+12>>2]){k=1;if(H[a+4>>2]>0|H[a+8>>2]>1){break b}break a}e=1;if(H[a+8>>2]>0){break b}if(H[a+4>>2]<2){break a}}b=H[a>>2];f=b+(e<<5)|0;g=H[a+16>>2];h=H[a+20>>2];if(g>>>0>>0){d=g;while(1){c=(d<<6)+f|0;L[c>>2]=L[c>>2]*O(1.2301740646362305);L[c+4>>2]=L[c+4>>2]*O(1.2301740646362305);L[c+8>>2]=L[c+8>>2]*O(1.2301740646362305);L[c+12>>2]=L[c+12>>2]*O(1.2301740646362305);L[c+16>>2]=L[c+16>>2]*O(1.2301740646362305);L[c+20>>2]=L[c+20>>2]*O(1.2301740646362305);L[c+24>>2]=L[c+24>>2]*O(1.2301740646362305);L[c+28>>2]=L[c+28>>2]*O(1.2301740646362305);d=d+1|0;if((h|0)!=(d|0)){continue}break}}i=b+(k<<5)|0;j=H[a+28>>2];c=H[a+24>>2];if(j>>>0>c>>>0){d=c;while(1){b=(d<<6)+i|0;L[b>>2]=L[b>>2]*O(1.625732421875);L[b+4>>2]=L[b+4>>2]*O(1.625732421875);L[b+8>>2]=L[b+8>>2]*O(1.625732421875);L[b+12>>2]=L[b+12>>2]*O(1.625732421875);L[b+16>>2]=L[b+16>>2]*O(1.625732421875);L[b+20>>2]=L[b+20>>2]*O(1.625732421875);L[b+24>>2]=L[b+24>>2]*O(1.625732421875);L[b+28>>2]=L[b+28>>2]*O(1.625732421875);d=d+1|0;if((j|0)!=(d|0)){continue}break}}b=f+32|0;d=H[a+8>>2];a=H[a+4>>2];e=a-e|0;e=(d|0)<(e|0)?d:e;mb(i,b,g,h,e,O(-.4435068666934967));l=i+32|0;d=d-k|0;a=(a|0)<(d|0)?a:d;mb(f,l,c,j,a,O(-.8829110860824585));mb(i,b,g,h,e,O(.05298011749982834));mb(f,l,c,j,a,O(1.5861343145370483))}}function _b(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;e=H[a+28>>2]+N(b,152)|0;c=H[e-144>>2]-H[e-152>>2]|0;d=H[e-140>>2]-H[e-148>>2]|0;f=d>>>0>=64?64:d;g=c>>>0>=64?64:c;a:{if(!(!c|!d|(!g|!f)|g>>>0>4294967295/(f>>>0)>>>2>>>0)){e=Ea(1,28);H[e+12>>2]=f;H[e+8>>2]=g;H[e+4>>2]=d;H[e>>2]=c;j=d;d=d+f|0;i=j>>>0>d>>>0;d=ve(d-1|0,i-!d|0,f,0);H[e+20>>2]=d;j=c;c=c+g|0;f=j>>>0>c>>>0;c=ve(c-1|0,f-!c|0,g,0);H[e+16>>2]=c;re(d,0,c);b:{if(qa){break b}c=Ea(4,N(c,d));H[e+24>>2]=c;if(!c){break b}break a}Ca(e)}e=0}if(!e){return 0}c:{if(b){while(1){q=N(p,152);g=q+H[a+28>>2]|0;c=H[g+24>>2];if(c){j=g+28|0;d=H[g+20>>2];f=H[g+16>>2];n=0;while(1){if(N(d,f)){i=N(n,36)+j|0;o=0;while(1){k=H[i+20>>2]+N(o,40)|0;c=H[k+20>>2];h=H[k+16>>2];if(N(c,h)){f=0;while(1){l=H[k+24>>2]+N(f,68)|0;r=H[l+60>>2];if(r){h=H[l+8>>2];d=h-H[i>>2]|0;m=H[i+16>>2];if(m&1){c=H[a+28>>2]+q|0;d=(H[c-144>>2]+d|0)-H[c-152>>2]|0}s=H[l+12>>2];c=s-H[i+4>>2]|0;if(m&2){m=H[a+28>>2]+q|0;c=(c+H[m-140>>2]|0)-H[m-148>>2]|0}h=H[l+16>>2]-h|0;if(!Za(e,d,c,d+h|0,(H[l+20>>2]-s|0)+c|0,r,1,h)){break c}h=H[k+16>>2];c=H[k+20>>2]}f=f+1|0;if(f>>>0>>0){continue}break}f=H[g+16>>2];d=H[g+20>>2]}o=o+1|0;if(o>>>0>>0){continue}break}c=H[g+24>>2]}n=n+1|0;if(n>>>0>>0){continue}break}}p=p+1|0;if((p|0)!=(b|0)){continue}break}}return e}Va(e);return 0}function $d(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;h=na-16|0;na=h;if(H[a+8>>2]==16){e=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{e=H[a+12>>2]}a:{if(c>>>0<=1){Ba(d,1,4132,0);a=0;break a}Da(b,h+12|0,2);b:{if(H[h+12>>2]){Ba(d,2,3608,0);break b}if(c>>>0<=6){Ba(d,1,4132,0);a=0;break a}Da(b+2|0,h+12|0,2);g=H[e+5616>>2];k=I[h+12|0];f=H[e+5620>>2];c:{d:{if(!f){break d}a=g;while(1){if(H[a+8>>2]!=(k|0)){a=a+20|0;i=i+1|0;if((i|0)!=(f|0)){continue}break d}break}if((f|0)!=(i|0)){break c}}if(H[e+5624>>2]==(f|0)){a=f+10|0;H[e+5624>>2]=a;a=Ha(g,N(a,20));g=H[e+5616>>2];if(!a){Ca(g);H[e+5624>>2]=0;H[e+5616>>2]=0;H[e+5620>>2]=0;Ba(d,1,4158,0);a=0;break a}e:{if((a|0)==(g|0)){break e}l=H[e+5632>>2];if(!l){break e}m=H[e+5628>>2];i=0;while(1){f=N(i,20)+m|0;j=H[f+8>>2];if(j){H[f+8>>2]=a+(j-g|0)}j=H[f+12>>2];if(j){H[f+12>>2]=a+(j-g|0)}i=i+1|0;if((l|0)!=(i|0)){continue}break}}H[e+5616>>2]=a;g=H[e+5620>>2];f=N(H[e+5624>>2]-g|0,20);if(f){y(a+N(g,20)|0,0,f)}f=H[e+5620>>2];g=H[e+5616>>2]}H[e+5620>>2]=f+1;a=N(f,20)+g|0}e=H[a+12>>2];if(e){Ca(e);H[a+12>>2]=0;H[a+16>>2]=0}H[a+8>>2]=k;e=H[h+12>>2];H[a>>2]=e>>>10&3;H[a+4>>2]=e>>>8&3;Da(b+4|0,h+12|0,2);if(H[h+12>>2]){Ba(d,2,3023,0);break b}c=c-6|0;e=Fa(c);H[a+12>>2]=e;if(!e){Ba(d,1,4132,0);a=0;break a}if(c){B(e,b+6|0,c)}H[a+16>>2]=c}a=1}na=h+16|0;return a|0}function Tb(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;if(a){b=H[a+20>>2];if(b){g=H[b>>2];if(g){d=H[g+20>>2];if(H[g+16>>2]){i=F[a+40|0]&1?16:17;while(1){c=H[d+28>>2];if(c){b=H[d+32>>2];l=(b>>>0)/152|0;j=0;if(b>>>0>=152){while(1){b=H[c+48>>2];if(b){f=H[c+52>>2];h=(f>>>0)/40|0;e=0;if(f>>>0>=40){while(1){_a(H[b+32>>2]);H[b+32>>2]=0;_a(H[b+36>>2]);H[b+36>>2]=0;ra[i|0](b);b=b+40|0;e=e+1|0;if((h|0)!=(e|0)){continue}break}b=H[c+48>>2]}Ca(b);H[c+48>>2]=0}b=H[c+84>>2];if(b){f=H[c+88>>2];h=(f>>>0)/40|0;e=0;if(f>>>0>=40){while(1){_a(H[b+32>>2]);H[b+32>>2]=0;_a(H[b+36>>2]);H[b+36>>2]=0;ra[i|0](b);b=b+40|0;e=e+1|0;if((h|0)!=(e|0)){continue}break}b=H[c+84>>2]}Ca(b);H[c+84>>2]=0}b=H[c+120>>2];if(b){f=H[c+124>>2];h=(f>>>0)/40|0;e=0;if(f>>>0>=40){while(1){_a(H[b+32>>2]);H[b+32>>2]=0;_a(H[b+36>>2]);H[b+36>>2]=0;ra[i|0](b);b=b+40|0;e=e+1|0;if((h|0)!=(e|0)){continue}break}b=H[c+120>>2]}Ca(b);H[c+120>>2]=0}c=c+152|0;j=j+1|0;if((l|0)!=(j|0)){continue}break}c=H[d+28>>2]}Ca(c);H[d+28>>2]=0}a:{if(!H[d+40>>2]){break a}b=H[d+36>>2];if(!b){break a}Ca(b);H[d+44>>2]=0;H[d+48>>2]=0;H[d+36>>2]=0;H[d+40>>2]=0}Ca(H[d+52>>2]);d=d+76|0;k=k+1|0;if(k>>>0>2]){continue}break}d=H[g+20>>2]}Ca(d);H[g+20>>2]=0;Ca(H[H[a+20>>2]>>2]);b=H[a+20>>2];H[b>>2]=0}Ca(b);H[a+20>>2]=0}Ca(H[a+68>>2]);Ca(a)}}function Rb(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0;e=H[a+24>>2];j=H[e+16>>2];if(!j){return 0}f=H[e+24>>2];e=H[H[H[a+20>>2]>>2]+20>>2];a:{b:{if(!b){b=0;while(1){c=H[f+24>>2];a=H[e+28>>2]+N(H[e+24>>2],152)|0;d=H[a-140>>2];g=H[a-144>>2]-H[a-152>>2]|0;a=H[a-148>>2];h=d-a|0;re(g,0,h);if(!(!qa|(a|0)==(d|0))){break a}a=(c>>>3|0)+((c&7)!=0)|0;c=(a|0)==3?4:a;a=!c;d=N(g,h);re(c,0,d);if(!(!qa|a)){break a}a=-1;c=N(c,d);if(c>>>0>(b^-1)>>>0){break b}e=e+76|0;f=f+52|0;b=b+c|0;a=b;i=i+1|0;if((j|0)!=(i|0)){continue}break}break b}b=0;if(!H[a+64>>2]){while(1){c=H[f+24>>2];a=H[e+28>>2]+N(H[e+24>>2],152)|0;d=H[a-4>>2];g=H[a-8>>2]-H[a-16>>2]|0;a=H[a-12>>2];h=d-a|0;re(g,0,h);if(!(!qa|(a|0)==(d|0))){break a}a=(c>>>3|0)+((c&7)!=0)|0;c=(a|0)==3?4:a;a=!c;d=N(g,h);re(c,0,d);if(!(!qa|a)){break a}a=-1;c=N(c,d);if(c>>>0>(b^-1)>>>0){break b}e=e+76|0;f=f+52|0;b=b+c|0;a=b;i=i+1|0;if((j|0)!=(i|0)){continue}break}break b}while(1){c=H[f+24>>2];a=H[e+28>>2]+N(H[e+24>>2],152)|0;d=H[a-140>>2];g=H[a-144>>2]-H[a-152>>2]|0;a=H[a-148>>2];h=d-a|0;re(g,0,h);if(!(!qa|(a|0)==(d|0))){break a}a=(c>>>3|0)+((c&7)!=0)|0;c=(a|0)==3?4:a;a=!c;d=N(g,h);re(c,0,d);if(!(!qa|a)){break a}a=-1;c=N(c,d);if(c>>>0>(b^-1)>>>0){break b}e=e+76|0;f=f+52|0;b=b+c|0;a=b;i=i+1|0;if((j|0)!=(i|0)){continue}break}}return a}return-1}function $b(a,b){var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;c=H[a+8>>2];e=c+H[a+4>>2]|0;a:{if(!H[a+12>>2]){if((e|0)<2){break a}h=(c<<2)+b|0;d=H[h>>2];c=H[b>>2]-(d+1>>1)|0;i=H[a>>2];if(e>>>0>=4){k=(e-4>>>1|0)+1|0;a=1;while(1){f=d;d=a<<2;m=H[d+b>>2];d=H[d+h>>2];j=c;l=i+(g<<2)|0;H[l>>2]=c;c=m-((d+f|0)+2>>2)|0;H[l+4>>2]=f+(j+c>>1);g=g+2|0;f=(a|0)!=(k|0);a=a+1|0;if(f){continue}break}}H[i+(g<<2)>>2]=c;if(e&1){f=e-1|0;a=H[(f<<1)+b>>2]-(d+1>>1)|0;H[i+(f<<2)>>2]=a;c=a+c>>1;j=-8}else{j=-4}a=e<<2;H[j+(a+i|0)>>2]=c+d;if(!a){break a}B(b,i,a);return}b:{switch(e-1|0){case 0:H[b>>2]=H[b>>2]/2;return;case 1:a=H[a>>2];c=(c<<2)+b|0;d=H[b>>2]-(H[c>>2]+1>>1)|0;H[a+4>>2]=d;H[a>>2]=d+H[c>>2];c=H[a+4>>2];H[b>>2]=H[a>>2];H[b+4>>2]=c;return;default:break b}}if((e|0)<3){break a}h=H[a>>2];k=(c<<2)+b|0;d=H[k+4>>2];a=H[k>>2];c=H[b>>2]-((d+a|0)+2>>2)|0;H[h>>2]=c+a;g=1;m=e-2|0;l=e&1;a=!l;if(m-a>>>0>=2){o=((e-a|0)-4>>>1|0)+1|0;a=1;while(1){f=d;p=H[(a<<2)+b>>2];j=a+1|0;d=H[k+(j<<2)>>2];i=c;n=h+(g<<2)|0;H[n>>2]=c;c=p-((d+f|0)+2>>2)|0;H[n+4>>2]=f+(i+c>>1);g=g+2|0;f=(a|0)!=(o|0);a=j;if(f){continue}break}}H[h+(g<<2)>>2]=c;c:{if(!l){g=H[((e<<1)+b|0)-4>>2]-(d+1>>1)|0;H[h+(m<<2)>>2]=(g+c>>1)+d;break c}g=c+d|0}a=e<<2;H[(a+h|0)-4>>2]=g;if(!a){break a}B(b,h,a)}}function Hb(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;d=na-256|0;na=d;if(a){Ac(1806,17,c);H[d+240>>2]=H[a>>2];Ga(c,2348,d+240|0);H[d+224>>2]=H[a+4>>2];Ga(c,2361,d+224|0);H[d+208>>2]=H[a+8>>2];Ga(c,7260,d+208|0);H[d+192>>2]=H[a+16>>2];Ga(c,2319,d+192|0);if((b|0)>0){while(1){e=H[a+5584>>2];H[d+176>>2]=h;Ga(c,1844,d+176|0);e=e+N(h,1080)|0;H[d+160>>2]=H[e>>2];Ga(c,2347,d+160|0);H[d+144>>2]=H[e+4>>2];Ga(c,7374,d+144|0);H[d+128>>2]=H[e+8>>2];Ga(c,7162,d+128|0);H[d+112>>2]=H[e+12>>2];Ga(c,7178,d+112|0);H[d+96>>2]=H[e+16>>2];Ga(c,2330,d+96|0);H[d+80>>2]=H[e+20>>2];Ga(c,7440,d+80|0);Ac(1567,23,c);if(H[e+4>>2]){i=e+944|0;j=e+812|0;f=0;while(1){g=f<<2;k=H[j+g>>2];H[d+68>>2]=H[i+g>>2];H[d+64>>2]=k;Ga(c,1693,d- -64|0);f=f+1|0;if(f>>>0>2]){continue}break}}Bc(c);H[d+48>>2]=H[e+24>>2];Ga(c,7194,d+48|0);H[d+32>>2]=H[e+804>>2];Ga(c,7243,d+32|0);i=1;Ac(1591,20,c);a:{if(H[e+24>>2]!=1){f=H[e+4>>2];if((f|0)<=0){break a}i=N(f,3)-2|0}j=e+28|0;f=0;while(1){g=j+(f<<3)|0;l=d,m=ye(H[g>>2],H[g+4>>2],32),H[l+16>>2]=m;H[d+20>>2]=qa;Ga(c,1693,d+16|0);f=f+1|0;if((i|0)!=(f|0)){continue}break}}Bc(c);H[d>>2]=H[e+808>>2];Ga(c,7226,d);Ac(1707,5,c);h=h+1|0;if((h|0)!=(b|0)){continue}break}}Ac(1708,4,c)}na=d+256|0}function se(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0;a:{b:{c:{d:{e:{f:{g:{h:{i:{j:{e=b;if(e){if(!c){break j}if(!d){break i}e=Q(d)-Q(e)|0;if(e>>>0<=31){break h}break b}if((d|0)==1|d>>>0>1){break b}b=(a>>>0)/(c>>>0)|0;oa=a-N(b,c)|0;pa=0;qa=0;return b}if(!a){break g}if(!d){break f}f=d-1|0;if(f&d){break f}oa=a;pa=e&f;a=e>>>te(d)|0;qa=0;return a}f=c-1|0;if(!(f&c)){break e}k=(Q(c)+33|0)-Q(e)|0;g=0-k|0;break c}k=e+1|0;g=63-e|0;break c}oa=0;a=(e>>>0)/(d>>>0)|0;pa=e-N(a,d)|0;qa=0;return a}e=Q(d)-Q(e)|0;if(e>>>0<31){break d}break b}oa=a&f;pa=0;if((c|0)==1){break a}c=te(c);d=c&31;if((c&63)>>>0>=32){e=0;a=b>>>d|0}else{e=b>>>d|0;a=((1<>>d}qa=e;return a}k=e+1|0;g=63-e|0}f=a;e=k&63;h=e&31;if((e&63)>>>0>=32){e=0;f=b>>>h|0}else{e=b>>>h|0;f=((1<>>h}h=g&63;g=a;i=h&31;if((h&63)>>>0>=32){j=a<>>32-i|b<>>31;f=f<<1|b>>>31;l=e;i=g-(e+(f>>>0>h>>>0)|0)|0;m=i>>31;j=m;e=f;i=c&j;f=e-i|0;e=l-((d&j)+(e>>>0>>0)|0)|0;j=b<<1|a>>>31;a=n|a<<1;b=j|o;l=m&1;n=l;k=k-1|0;if(k){continue}break}}oa=f;pa=e;j=b<<1|a>>>31;a=l|a<<1;qa=j|o;return a}oa=a;pa=b;a=0;b=0}qa=b;return a}function Kc(a,b,c,d,e){var f=0,g=0,h=0,i=0;h=na-16|0;na=h;if(H[a+8>>2]==16){a=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{a=H[a+12>>2]}f=H[d>>2];a:{if(!f){d=0;Ba(e,1,2642,0);break a}a=H[a+5584>>2];H[d>>2]=f-1;Da(c,h+12|0,1);g=N(b,1080)+a|0;a=H[h+12>>2];H[g+804>>2]=a>>>5;b=a&31;H[g+24>>2]=b;a=c+1|0;b:{c:{d:{e:{f:{switch(b|0){case 0:f=H[d>>2];break e;case 1:break d;default:break f}}f=H[d>>2]>>>1|0}if(f>>>0>=98){H[h+4>>2]=97;H[h+8>>2]=97;H[h>>2]=f;Ba(e,2,16056,h);b=H[g+24>>2]}if(b){b=f;if(b){break d}a=0;break c}if(f){b=g+28|0;c=0;while(1){Da(a,h+12|0,1);if(c>>>0<=96){e=H[h+12>>2];i=b+(c<<3)|0;H[i+4>>2]=0;H[i>>2]=e>>>3}a=a+1|0;c=c+1|0;if((f|0)!=(c|0)){continue}break}}a=H[d>>2];if(a>>>0>>0){d=0;break a}a=a-f|0;break b}e=g+28|0;c=0;while(1){Da(a,h+12|0,2);if(c>>>0<=96){f=e+(c<<3)|0;i=H[h+12>>2];H[f+4>>2]=i&2047;H[f>>2]=i>>>11}a=a+2|0;c=c+1|0;if((c|0)!=(b|0)){continue}break}a=b<<1}b=H[d>>2];if(a>>>0>b>>>0){d=0;break a}a=b-a|0}H[d>>2]=a;d=1;if(H[g+24>>2]!=1){break a}f=g+28|0;c=H[g+32>>2];e=H[g+28>>2];a=1;while(1){b=f+(a<<3)|0;H[b+12>>2]=c;H[b+4>>2]=c;g=e-((a>>>0)/3|0)|0;H[b+8>>2]=(g|0)>0?g:0;g=b;b=e-((a-1>>>0)/3|0)|0;H[g>>2]=(b|0)>0?b:0;a=a+2|0;if((a|0)!=97){continue}break}}na=h+16|0;return d}function fe(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0;f=na-32|0;na=f;g=1;a:{if(c>>>0<=1){g=0;Ba(d,1,10062,0);break a}if(H[a+76>>2]){break a}Da(b,f+28|0,1);Da(b+1|0,f+24|0,1);e=H[f+24>>2];i=e>>>4&3;if((i|0)==3){H[a+76>>2]=1;Ba(d,2,11558,0);break a}c=c-2|0;j=(e>>>5&2)+2|0;h=i+j|0;e=(c>>>0)/(h>>>0)|0;if((c|0)!=(N(e,h)|0)){H[a+76>>2]=1;Ba(d,2,11139,0);break a}if(c>>>0>>0){break a}b:{c=H[a+68>>2];if(c>>>0<=(e^-1)>>>0){c=c+e|0;if(c>>>0<536870912){break b}}H[a+76>>2]=1;Ba(d,2,9400,0);break a}h=Ha(H[a+72>>2],c<<3);if(!h){H[a+76>>2]=1;Ba(d,2,9443,0);break a}c=b+2|0;H[a+72>>2]=h;c:{if(i){k=e>>>0<=1?1:e;e=0;while(1){Da(c,f+20|0,i);b=H[f+20>>2];if(b>>>0>=N(H[a+132>>2],H[a+128>>2])>>>0){break c}b=c+i|0;Da(b,f+16|0,j);c=H[a+68>>2];g=h+(c<<3)|0;G[g>>1]=H[f+20>>2];H[g+4>>2]=H[f+16>>2];g=1;H[a+68>>2]=c+1;c=b+j|0;e=e+1|0;if((k|0)!=(e|0)){continue}break}break a}i=e>>>0<=1?1:e;b=H[a+68>>2];e=0;while(1){H[f+20>>2]=b;if(N(H[a+132>>2],H[a+128>>2])>>>0<=b>>>0){break c}Da(c,f+16|0,j);k=H[a+68>>2];g=h+(k<<3)|0;G[g>>1]=b;H[g+4>>2]=H[f+16>>2];g=1;b=k+1|0;H[a+68>>2]=b;c=c+j|0;e=e+1|0;if((i|0)!=(e|0)){continue}break}break a}H[a+76>>2]=1;H[f>>2]=b;Ba(d,2,7799,f)}na=f+32|0;return g|0}function Ad(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0;h=na-16|0;na=h;a:{if(!(I[a+100|0]&2)){Ba(d,1,11356,0);a=0;break a}H[a+104>>2]=0;b:{c:{d:{if(c){while(1){if(c>>>0<=7){Ba(d,1,3403,0);break b}g=h+12|0;Da(b,g,4);e=H[h+12>>2];Da(b+4|0,g,4);f=8;g=H[h+12>>2];e:{f:{g:{switch(e|0){case 1:if(c>>>0<16){e=3443;break c}Da(b+8|0,h+8|0,4);if(H[h+8>>2]){e=8449;break c}Da(b+12|0,h+12|0,4);e=H[h+12>>2];if(e){break f}e=3268;break c;case 0:break g;default:break e}}Ba(d,1,3268,0);break b}f=16}if(e>>>0>>0){Ba(d,1,9148,0);break b}if(c>>>0>>0){Ba(d,1,9076,0);a=0;break a}h:{i:{j=b+f|0;k=e-f|0;j:{k:{l:{m:{if((g|0)<=1668246641){if((g|0)==1651532643){break m}if((g|0)==1667523942){break k}if((g|0)!=1668112752){break i}f=25296;break j}if((g|0)==1885564018){break l}f=25264;if((g|0)==1768449138){break j}if((g|0)!=1668246642){break i}f=25272;break j}f=25280;break j}f=25288;break j}f=25304}if(ra[H[f+4>>2]](a,j,k,d)|0){break h}a=0;break a}H[a+104>>2]=H[a+104>>2]|2147483647}i=(g|0)==1768449138?1:i;b=b+e|0;c=c-e|0;if(c){continue}break}if(i){break d}}Ba(d,1,8976,0);a=0;break a}F[a+132|0]=1;H[a+100>>2]=H[a+100>>2]|4;a=1;break a}Ba(d,1,e,0)}Ba(d,1,1968,0);a=0}na=h+16|0;return a|0}function Jd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;a:{if(!Wa(H[b+8>>2],54,d)){break a}i=H[b+4>>2];f=H[i>>2];e=H[i+8>>2];b:{if(f){g=1;c:{if((f|0)!=1){k=f&1;f=f&-2;while(1){h=0;d:{if(!g){break d}h=0;if(!(ra[H[e>>2]](b,a,d)|0)){break d}h=(ra[H[e+4>>2]](b,a,d)|0)!=0}g=h;e=e+8|0;j=j+2|0;if((f|0)!=(j|0)){continue}break}if(!k){break c}}if(!g){g=0;break c}g=(ra[H[e>>2]](b,a,d)|0)!=0}Pa(i);if(g){break b}break a}Pa(i)}i=H[b+8>>2];f=H[i>>2];e=H[i+8>>2];e:{if(f){g=1;f:{if((f|0)!=1){k=f&1;f=f&-2;j=0;while(1){h=0;g:{if(!g){break g}h=0;if(!(ra[H[e>>2]](b,a,d)|0)){break g}h=(ra[H[e+4>>2]](b,a,d)|0)!=0}g=h;e=e+8|0;j=j+2|0;if((f|0)!=(j|0)){continue}break}if(!k){break f}}if(!g){g=0;break f}g=(ra[H[e>>2]](b,a,d)|0)!=0}Pa(i);if(!g){break a}break e}Pa(i)}if(!I[b+132|0]){Ba(d,1,11696,0);return 0}if(!I[b+133|0]){Ba(d,1,11667,0);return 0}l=Mb(a,H[b>>2],c,d);if(!c|!l){break a}a=H[c>>2];if(!a){break a}e=1;h:{i:{switch(H[b+48>>2]-12|0){case 5:e=2;break h;case 6:e=3;break h;case 12:e=4;break h;case 0:e=5;break h;case 4:break h;default:break i}}e=-1}H[a+20>>2]=e;c=H[b+108>>2];if(!c){break a}H[a+28>>2]=c;H[a+32>>2]=H[b+112>>2];H[b+108>>2]=0}return l|0}function Mb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;e=Eb();H[b+96>>2]=e;a:{b:{if(!e){break b}c:{if(Wa(H[b+220>>2],18,d)){if(Wa(H[b+220>>2],19,d)){break c}}break a}i=H[b+220>>2];f=H[i>>2];g=H[i+8>>2];d:{if(f){e=1;e:{if((f|0)!=1){k=f&1;f=f&-2;while(1){h=0;f:{if(!e){break f}h=0;if(!(ra[H[g>>2]](b,a,d)|0)){break f}h=(ra[H[g+4>>2]](b,a,d)|0)!=0}e=h;g=g+8|0;j=j+2|0;if((f|0)!=(j|0)){continue}break}if(!k){break e}}if(!e){e=0;break e}e=(ra[H[g>>2]](b,a,d)|0)!=0}Pa(i);if(e){break d}break a}Pa(i)}g:{if(Wa(H[b+216>>2],20,d)){if(Wa(H[b+216>>2],21,d)){break g}}break a}i=H[b+216>>2];f=H[i>>2];g=H[i+8>>2];h:{if(f){e=1;i:{if((f|0)!=1){k=f&1;f=f&-2;j=0;while(1){h=0;j:{if(!e){break j}h=0;if(!(ra[H[g>>2]](b,a,d)|0)){break j}h=(ra[H[g+4>>2]](b,a,d)|0)!=0}e=h;g=g+8|0;j=j+2|0;if((f|0)!=(j|0)){continue}break}if(!k){break i}}if(!e){e=0;break i}e=(ra[H[g>>2]](b,a,d)|0)!=0}Pa(i);if(e){break h}break a}Pa(i)}a=Eb();H[c>>2]=a;if(!a){break b}Ec(H[b+96>>2],a);l=1}return l|0}Ua(H[b+96>>2]);H[b+96>>2]=0;return 0}function zc(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0;b=H[6516];c=a+7&-8;a=c+7|0;d=a>>>0<7?1:d;e=a&-8;a=e+b|0;a:{b:{if(!(a>>>0>>0?d+1|0:d)){if(a>>>0<=sa()<<16>>>0){break b}if(ga(a|0)|0){break b}}H[6597]=48;b=-1;break a}H[6516]=a}d=b;if((d|0)!=-1){a=d+c|0;H[a-4>>2]=16;f=a-16|0;H[f>>2]=16;b=H[6856];if(b){e=H[b+8>>2]}else{e=0}c:{d:{if((e|0)==(d|0)){e=H[d-4>>2]&-2;g=d-e|0;h=H[g-4>>2];H[b+8>>2]=a;b=h&-2;a=g-b|0;if(H[(a+H[a>>2]|0)-4>>2]&1){f=H[a+4>>2];g=H[a+8>>2];H[f+8>>2]=g;H[g+4>>2]=f;b=(b+(c+e|0)|0)-16|0;H[a>>2]=b;break c}a=d-16|0;break d}H[d>>2]=16;H[d+8>>2]=a;H[d+4>>2]=b;H[d+12>>2]=16;H[6856]=d;a=d+16|0}b=f-a|0;H[a>>2]=b}H[((b&-4)+a|0)-4>>2]=b|1;c=H[a>>2]-8|0;e:{if(c>>>0<=127){b=(c>>>3|0)-1|0;break e}e=Q(c);b=((c>>>29-e^4)-(e<<2)|0)+110|0;if(c>>>0<=4095){break e}b=((c>>>30-e^2)-(e<<1)|0)+71|0;b=b>>>0>=63?63:b}c=b<<4;H[a+4>>2]=c+26400;c=c+26408|0;H[a+8>>2]=H[c>>2];H[c>>2]=a;H[H[a+8>>2]+4>>2]=a;c=H[6859];a=b&31;if((b&63)>>>0>=32){b=1<>>32-a}H[6858]=e|H[6858];H[6859]=b|c}return(d|0)!=-1}function Ec(a,b){var c=0,d=0,e=0,f=0,g=0;H[b>>2]=H[a>>2];H[b+4>>2]=H[a+4>>2];H[b+8>>2]=H[a+8>>2];H[b+12>>2]=H[a+12>>2];c=H[b+24>>2];if(c){d=H[b+16>>2];if(d){c=0;while(1){f=H[(H[b+24>>2]+N(c,52)|0)+44>>2];if(f){Ca(f);d=H[b+16>>2]}c=c+1|0;if(d>>>0>c>>>0){continue}break}c=H[b+24>>2]}Ca(c);H[b+24>>2]=0}c=H[a+16>>2];H[b+16>>2]=c;c=Fa(N(c,52));H[b+24>>2]=c;if(c){if(H[b+16>>2]){f=0;while(1){g=N(f,52);d=g+c|0;c=H[a+24>>2]+g|0;H[d+48>>2]=H[c+48>>2];e=H[c+44>>2];H[d+40>>2]=H[c+40>>2];H[d+44>>2]=e;e=H[c+36>>2];H[d+32>>2]=H[c+32>>2];H[d+36>>2]=e;e=H[c+28>>2];H[d+24>>2]=H[c+24>>2];H[d+28>>2]=e;e=H[c+20>>2];H[d+16>>2]=H[c+16>>2];H[d+20>>2]=e;e=H[c+12>>2];H[d+8>>2]=H[c+8>>2];H[d+12>>2]=e;e=H[c+4>>2];H[d>>2]=H[c>>2];H[d+4>>2]=e;c=H[b+24>>2];H[(g+c|0)+44>>2]=0;f=f+1|0;if(f>>>0>2]){continue}break}}H[b+20>>2]=H[a+20>>2];c=H[a+32>>2];H[b+32>>2]=c;a:{if(c){c=Fa(c);H[b+28>>2]=c;if(!c){H[b+28>>2]=0;H[b+32>>2]=0;return}b=H[a+32>>2];if(!b){break a}B(c,H[a+28>>2],b);return}H[b+28>>2]=0}return}H[b+16>>2]=0;H[b+24>>2]=0}function mb(a,b,c,d,e,f){var g=0,h=O(0),i=0,j=O(0);g=(c<<6)+b|0;b=c?g+-64|0:a;i=d>>>0>>0?d:e;if(i>>>0>c>>>0){h=L[b>>2];while(1){a=b;b=g;g=b-32|0;j=h;h=L[b>>2];L[g>>2]=O(O(j+h)*f)+L[g>>2];g=b-28|0;L[g>>2]=O(O(L[a+4>>2]+L[b+4>>2])*f)+L[g>>2];g=b-24|0;L[g>>2]=O(O(L[a+8>>2]+L[b+8>>2])*f)+L[g>>2];g=b-20|0;L[g>>2]=O(O(L[a+12>>2]+L[b+12>>2])*f)+L[g>>2];g=b-16|0;L[g>>2]=O(O(L[a+16>>2]+L[b+16>>2])*f)+L[g>>2];g=b-12|0;L[g>>2]=O(O(L[a+20>>2]+L[b+20>>2])*f)+L[g>>2];g=b-8|0;L[g>>2]=O(O(L[a+24>>2]+L[b+24>>2])*f)+L[g>>2];g=b-4|0;L[g>>2]=O(O(L[a+28>>2]+L[b+28>>2])*f)+L[g>>2];g=b- -64|0;c=c+1|0;if((i|0)!=(c|0)){continue}break}}if(d>>>0>e>>>0){a=g-32|0;f=O(f+f);L[a>>2]=O(L[b>>2]*f)+L[a>>2];a=g-28|0;L[a>>2]=O(L[b+4>>2]*f)+L[a>>2];a=g-24|0;L[a>>2]=O(L[b+8>>2]*f)+L[a>>2];a=g-20|0;L[a>>2]=O(L[b+12>>2]*f)+L[a>>2];a=g-16|0;L[a>>2]=O(L[b+16>>2]*f)+L[a>>2];a=g-12|0;L[a>>2]=O(L[b+20>>2]*f)+L[a>>2];a=g-8|0;L[a>>2]=O(L[b+24>>2]*f)+L[a>>2];a=g-4|0;L[a>>2]=O(L[b+28>>2]*f)+L[a>>2]}}function $c(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;if(K[a+44>>2]>=8){e=H[a+36>>2];n=e<<5;o=N(e,28);p=N(e,24);q=N(e,20);r=e<<4;i=N(e,12);j=e<<3;g=H[a+40>>2];k=8;while(1){zb(a,g,H[a+36>>2],8);Ta(a);h=H[a+32>>2];if(h){f=H[a>>2];b=0;a:{if((h|0)!=1){s=h&1;t=h&-2;l=0;while(1){c=(b<<2)+g|0;d=f+(b<<5)|0;L[c>>2]=L[d>>2];m=e<<2;L[c+m>>2]=L[d+4>>2];L[c+j>>2]=L[d+8>>2];L[c+i>>2]=L[d+12>>2];d=b|1;c=(d<<2)+g|0;d=f+(d<<5)|0;L[c>>2]=L[d>>2];L[c+m>>2]=L[d+4>>2];L[c+j>>2]=L[d+8>>2];L[c+i>>2]=L[d+12>>2];b=b+2|0;l=l+2|0;if((t|0)!=(l|0)){continue}break}if(!s){break a}}c=(b<<2)+g|0;b=f+(b<<5)|0;L[c>>2]=L[b>>2];L[c+(e<<2)>>2]=L[b+4>>2];L[c+j>>2]=L[b+8>>2];L[c+i>>2]=L[b+12>>2]}d=H[a>>2];b=0;while(1){f=(b<<2)+g|0;c=d+(b<<5)|0;L[f+r>>2]=L[c+16>>2];L[f+q>>2]=L[c+20>>2];L[f+p>>2]=L[c+24>>2];L[f+o>>2]=L[c+28>>2];b=b+1|0;if((h|0)!=(b|0)){continue}break}}g=g+n|0;k=k+8|0;if(k>>>0<=K[a+44>>2]){continue}break}}Ca(H[a>>2]);Ca(a)}function wd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;f=na-16|0;na=f;a:{if(H[a+120>>2]|c>>>0<3){break a}Da(b,f+12|0,2);k=J[f+12>>1];if(k-1025>>>0<=4294966271){H[f>>2]=k;Ba(d,1,3526,f);break a}Da(b+2|0,f+12|0,1);i=J[f+12>>1];if(!i){Ba(d,1,3174,0);break a}if(i+3>>>0>c>>>0){break a}h=Fa(N(i,k)<<2);if(!h){break a}j=Fa(i);if(!j){Ca(h);break a}l=Fa(i);if(!l){Ca(h);Ca(j);break a}g=Fa(20);if(!g){Ca(h);Ca(j);Ca(l);break a}d=b+3|0;H[g+8>>2]=j;H[g+4>>2]=l;G[g+16>>1]=k;H[g>>2]=h;m=H[f+12>>2];H[g+12>>2]=0;F[g+18|0]=m;H[a+120>>2]=g;while(1){Da(d,f+12|0,1);F[e+j|0]=(I[f+12|0]&127)+1;F[e+l|0]=(H[f+12>>2]&128)>>>7;d=d+1|0;e=e+1|0;if((i|0)!=(e|0)){continue}break}g=0;while(1){e=0;a=0;while(1){e=I[e+j|0]+7>>>3|0;e=e>>>0>=4?4:e;if((e+(d-b|0)|0)>(c|0)){e=0;break a}Da(d,f+12|0,e);H[h>>2]=H[f+12>>2];h=h+4|0;d=d+e|0;a=a+1|0;e=a&65535;if(i>>>0>e>>>0){continue}break}e=1;g=g+1|0;if((g&65535)>>>0>>0){continue}break}}na=f+16|0;return e|0}function od(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;f=-1;e=-1;if(!(I[a+68|0]&8)){f=H[a+32>>2];H[a+36>>2]=f;a:{b:{c:{e=H[a+48>>2];if(e){while(1){e=ra[H[a+20>>2]](f,e,H[a>>2])|0;if((e|0)==-1){break c}f=e+H[a+36>>2]|0;H[a+36>>2]=f;e=H[a+48>>2]-e|0;H[a+48>>2]=e;if(e){continue}break}f=H[a+32>>2]}H[a+36>>2]=f;if(!!b&(c|0)>=0|(c|0)>0){break b}f=0;e=0;break a}H[a+68>>2]=H[a+68>>2]|8;Ba(d,4,15604,0);H[a+48>>2]=0;H[a+68>>2]=H[a+68>>2]|8;qa=-1;return-1}f=0;e=0;while(1){g=ra[H[a+24>>2]](b,c,H[a>>2])|0;h=qa;i=h;if((g&h)==-1){Ba(d,4,15589,0);H[a+68>>2]=H[a+68>>2]|8;b=e+H[a+60>>2]|0;c=f+H[a+56>>2]|0;b=c>>>0>>0?b+1|0:b;H[a+56>>2]=c;H[a+60>>2]=b;a=!(e|f);b=a?-1:f;qa=a?-1:e;return b|0}e=e+i|0;f=f+g|0;e=f>>>0>>0?e+1|0:e;h=b;b=b-g|0;c=c-(i+(g>>>0>h>>>0)|0)|0;if(!!b&(c|0)>=0|(c|0)>0){continue}break}}b=e+H[a+60>>2]|0;c=f+H[a+56>>2]|0;b=c>>>0>>0?b+1|0:b;H[a+56>>2]=c;H[a+60>>2]=b}qa=e;return f|0}function xc(a){var b=0,c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0;b=a;a:{if(b&3){while(1){c=I[b|0];if(!c|(c|0)==61){break a}b=b+1|0;if(b&3){continue}break}}b:{c:{d=H[b>>2];if(((d|16843008-d)&-2139062144)!=-2139062144){break c}while(1){c=d^1027423549;if(((16843008-c|c)&-2139062144)!=-2139062144){break c}d=H[b+4>>2];c=b+4|0;b=c;if(((16843008-d|d)&-2139062144)==-2139062144){continue}break}break b}c=b}while(1){b=c;d=I[b|0];if(!d){break a}c=b+1|0;if((d|0)!=61){continue}break}}if((a|0)==(b|0)){return 0}g=b-a|0;d:{if(I[g+a|0]){break d}f=H[6860];if(!f){break d}b=H[f>>2];if(!b){break d}while(1){e:{d=a;c=b;h=g;e=0;f:{if(!g){break f}e=I[d|0];if(e){g:{while(1){i=I[c|0];if((i|0)!=(e|0)|!i){break g}h=h-1|0;if(!h){break g}c=c+1|0;e=I[d+1|0];d=d+1|0;if(e){continue}break}e=0}}else{e=0}e=e-I[c|0]|0}if(!e){b=b+g|0;if(I[b|0]==61){break e}}b=H[f+4>>2];f=f+4|0;if(b){continue}break d}break}j=b+1|0}return j}function be(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;g=na-16|0;na=g;a:{if(c>>>0<=1){Ba(d,1,3983,0);a=0;break a}if(F[a+212|0]&1){Ba(d,1,12668,0);a=0;break a}a=H[a+180>>2]+N(H[a+228>>2],5644)|0;F[a+5640|0]=I[a+5640|0]|2;Da(b,g+12|0,1);e=H[a+5164>>2];b:{if(!e){f=H[g+12>>2]+1|0;e=Ea(f,8);H[a+5164>>2]=e;if(!e){Ba(d,1,4009,0);a=0;break a}H[a+5160>>2]=f;break b}f=H[g+12>>2];if(f>>>0>2]){break b}h=e;e=f+1|0;f=Ha(h,e<<3);if(!f){Ba(d,1,4009,0);a=0;break a}H[a+5164>>2]=f;h=H[a+5160>>2];i=e-h<<3;if(i){y(f+(h<<3)|0,0,i)}H[a+5160>>2]=e;e=H[a+5164>>2]}h=e;e=H[g+12>>2];if(H[h+(e<<3)>>2]){H[g>>2]=e;Ba(d,1,7063,g);a=0;break a}c=c-1|0;e=Fa(c);a=H[a+5164>>2];f=a+(H[g+12>>2]<<3)|0;H[f>>2]=e;if(!e){Ba(d,1,4009,0);a=0;break a}H[f+4>>2]=c;if(c){B(H[a+(H[g+12>>2]<<3)>>2],b+1|0,c)}a=1}na=g+16|0;return a|0}function Cb(a,b,c){var d=0,e=0,f=0,g=0;e=a+4|0;d=(e+b|0)-1&0-b;b=H[a>>2];if(d+c>>>0<=(b+a|0)-4>>>0){f=H[a+4>>2];g=H[a+8>>2];H[f+8>>2]=g;H[g+4>>2]=f;if((d|0)!=(e|0)){d=d-e|0;f=a-(H[a-4>>2]&-2)|0;e=d+H[f>>2]|0;H[f>>2]=e;H[(f+(e&-4)|0)-4>>2]=e;a=a+d|0;b=b-d|0;H[a>>2]=b}a:{if(c+24>>>0<=b>>>0){e=a+c|0;b=(b-c|0)-8|0;H[e+8>>2]=b;g=e+8|0;H[(g+(b&-4)|0)-4>>2]=b|1;d=H[e+8>>2]-8|0;b:{if(d>>>0<=127){b=(d>>>3|0)-1|0;break b}f=Q(d);b=((d>>>29-f^4)-(f<<2)|0)+110|0;if(d>>>0<=4095){break b}b=((d>>>30-f^2)-(f<<1)|0)+71|0;b=b>>>0>=63?63:b}d=b<<4;H[e+12>>2]=d+26400;d=d+26408|0;H[e+16>>2]=H[d>>2];H[d>>2]=g;H[H[e+16>>2]+4>>2]=g;d=H[6858];f=H[6859];e=b&31;if((b&63)>>>0>=32){b=1<>>32-e}H[6858]=g|d;H[6859]=b|f;b=c+8|0;H[a>>2]=b;c=(b&-4)+a|0;break a}c=a+b|0}H[c-4>>2]=b;a=a+4|0}else{a=0}return a}function he(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;g=na-16|0;na=g;i=H[H[a+96>>2]+16>>2];h=i>>>0<257?1:2;e=(h<<1)+5|0;f=(c>>>0)/(e>>>0)|0;a:{if(!((N(e,f)|0)==(c|0)&c>>>0>=e>>>0)){Ba(d,1,4643,0);a=0;break a}if(H[a+8>>2]==16){e=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{e=H[a+12>>2]}a=0;c=I[e+5640|0];a=c&4?H[e+420>>2]+1|0:a;f=f+a|0;if(f>>>0>=32){H[g>>2]=f;Ba(d,1,7781,g);a=0;break a}F[e+5640|0]=c|4;if(a>>>0>>0){c=(e+N(a,148)|0)+424|0;while(1){Da(b,c,1);b=b+1|0;Da(b,c+4|0,h);b=b+h|0;Da(b,c+8|0,2);d=H[c+8>>2];j=H[e+8>>2];H[c+8>>2]=d>>>0>>0?d:j;Da(b+2|0,c+12|0,1);b=b+3|0;Da(b,c+16|0,h);b=b+h|0;Da(b,g+12|0,1);H[c+36>>2]=H[g+12>>2];d=H[c+16>>2];H[c+16>>2]=d>>>0>>0?d:i;c=c+148|0;b=b+1|0;a=a+1|0;if((f|0)!=(a|0)){continue}break}}H[e+420>>2]=f-1;a=1}na=g+16|0;return a|0}function jb(a){var b=0,c=0,d=0,e=0;a:{if(!a){break a}b=H[a+5164>>2];if(b){c=H[a+5160>>2];if(c){b=0;while(1){d=H[H[a+5164>>2]+(b<<3)>>2];if(d){Ca(d);c=H[a+5160>>2]}b=b+1|0;if(c>>>0>b>>>0){continue}break}b=H[a+5164>>2]}H[a+5160>>2]=0;Ca(b);H[a+5164>>2]=0}b=H[a+5172>>2];if(b){Ca(b);H[a+5172>>2]=0}b=H[a+5584>>2];if(b){Ca(b);H[a+5584>>2]=0}b=H[a+5612>>2];if(b){Ca(b);H[a+5612>>2]=0}b=H[a+5608>>2];if(b){Ca(b);H[a+5608>>2]=0}b=H[a+5628>>2];if(b){Ca(b);H[a+5636>>2]=0;H[a+5628>>2]=0;H[a+5632>>2]=0}b=H[a+5616>>2];if(b){e=H[a+5620>>2];if(e){c=0;while(1){d=H[b+12>>2];if(d){Ca(d);H[b+12>>2]=0;e=H[a+5620>>2]}b=b+20|0;c=c+1|0;if(e>>>0>c>>>0){continue}break}b=H[a+5616>>2]}Ca(b);H[a+5616>>2]=0}b=H[a+5604>>2];if(b){Ca(b);H[a+5604>>2]=0}b=H[a+5596>>2];if(!b){break a}Ca(b);H[a+5596>>2]=0;H[a+5600>>2]=0}}function zd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0;e=na-32|0;na=e;a:{if(H[a+72>>2]){Ba(d,2,7015,0);c=1;break a}if((c|0)!=14){c=0;Ba(d,1,14445,0);break a}Da(b,a+16|0,4);Da(b+4|0,a+12|0,4);Da(b+8|0,a+20|0,2);f=H[a+12>>2];b:{g=H[a+16>>2];c=H[a+20>>2];c:{if(!g){break c}c=H[a+20>>2];if(!f){break c}if(c){break b}c=0}H[e+8>>2]=c;H[e+4>>2]=g;H[e>>2]=f;Ba(d,1,14289,e);c=0;break a}if(c>>>0>=16385){c=0;Ba(d,1,14203,0);break a}c=Ea(c,12);H[a+72>>2]=c;if(!c){c=0;Ba(d,1,14240,0);break a}c=1;Da(b+10|0,a+24|0,1);Da(b+11|0,a+28|0,1);f=H[a+28>>2];if((f|0)!=7){H[e+16>>2]=f;Ba(d,4,16272,e+16|0)}Da(b+12|0,a+32|0,1);Da(b+13|0,a+36|0,1);b=H[a>>2];F[b+212|0]=I[b+212|0]&251|(H[a+24>>2]==255?4:0);b=H[a>>2];H[b+240>>2]=H[a+12>>2];H[b+244>>2]=H[a+16>>2];F[a+133|0]=1}na=e+32|0;return c|0}function qc(a,b,c,d){a:{switch(b-9|0){case 0:b=H[c>>2];H[c>>2]=b+4;H[a>>2]=H[b>>2];return;case 6:b=H[c>>2];H[c>>2]=b+4;b=G[b>>1];H[a>>2]=b;H[a+4>>2]=b>>31;return;case 7:b=H[c>>2];H[c>>2]=b+4;H[a>>2]=J[b>>1];H[a+4>>2]=0;return;case 8:b=H[c>>2];H[c>>2]=b+4;b=F[b|0];H[a>>2]=b;H[a+4>>2]=b>>31;return;case 9:b=H[c>>2];H[c>>2]=b+4;H[a>>2]=I[b|0];H[a+4>>2]=0;return;case 16:b=H[c>>2]+7&-8;H[c>>2]=b+8;M[a>>3]=M[b>>3];return;case 17:ra[d|0](a,c);default:return;case 1:case 4:case 14:b=H[c>>2];H[c>>2]=b+4;b=H[b>>2];H[a>>2]=b;H[a+4>>2]=b>>31;return;case 2:case 5:case 11:case 15:b=H[c>>2];H[c>>2]=b+4;H[a>>2]=H[b>>2];H[a+4>>2]=0;return;case 3:case 10:case 12:case 13:break a}}b=H[c>>2]+7&-8;H[c>>2]=b+8;c=H[b+4>>2];H[a>>2]=H[b>>2];H[a+4>>2]=c}function ce(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;g=na-16|0;na=g;a:{if(c>>>0<=1){Ba(d,1,4311,0);a=0;break a}F[a+212|0]=I[a+212|0]|1;Da(b,g+12|0,1);e=H[a+140>>2];b:{if(!e){f=H[g+12>>2]+1|0;e=Ea(f,8);H[a+140>>2]=e;if(!e){Ba(d,1,4337,0);a=0;break a}H[a+136>>2]=f;break b}f=H[g+12>>2];if(f>>>0>2]){break b}h=e;e=f+1|0;f=Ha(h,e<<3);if(!f){Ba(d,1,4337,0);a=0;break a}H[a+140>>2]=f;h=H[a+136>>2];i=e-h<<3;if(i){y(f+(h<<3)|0,0,i)}H[a+136>>2]=e;e=H[a+140>>2]}h=e;e=H[g+12>>2];if(H[h+(e<<3)>>2]){H[g>>2]=e;Ba(d,1,7085,g);a=0;break a}c=c-1|0;e=Fa(c);a=H[a+140>>2];f=a+(H[g+12>>2]<<3)|0;H[f>>2]=e;if(!e){Ba(d,1,4337,0);a=0;break a}H[f+4>>2]=c;if(c){B(H[a+(H[g+12>>2]<<3)>>2],b+1|0,c)}a=1}na=g+16|0;return a|0}function cd(a,b){a=a|0;b=+b;var c=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0;c=a<<3;i=c+28640|0;d=M[c+28672>>3];if(d!=0){e=M[i>>3];g=S(e,b);b=g-e;if(b<0x10000000000000000&b>=0){f=~~b>>>0;if(P(b)>=1){c=~~(b>0?R(T(b*2.3283064365386963e-10),4294967295):U((b-+(~~b>>>0>>>0))*2.3283064365386963e-10))>>>0}else{c=0}}else{c=0}if(d<0x10000000000000000&d>=0){j=~~d>>>0;if(P(d)>=1){h=~~(d>0?R(T(d*2.3283064365386963e-10),4294967295):U((d-+(~~d>>>0>>>0))*2.3283064365386963e-10))>>>0}else{h=0}}else{h=0}c=ve(f,c,j,h);f=qa;c=c+1|0;f=c?f:f+1|0;e=(+(c>>>0)+ +(f>>>0)*4294967296)*d+e;g=e-g}M[i>>3]=e;fa(a|0,+g)|0;c=(a|0)==2?27:(a|0)==1?26:14;a=c-1|0;a:{if(H[6892]>>>a&1){H[6894]=H[6894]|1<>2];if(a){ra[a|0](c)}}}function jd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0;d=na-32|0;na=d;e=H[a+28>>2];H[d+16>>2]=e;f=H[a+20>>2];H[d+28>>2]=c;H[d+24>>2]=b;b=f-e|0;H[d+20>>2]=b;f=b+c|0;i=2;b=d+16|0;a:{while(1){b:{c:{d:{if(!yc(Z(H[a+60>>2],b|0,i|0,d+12|0)|0)){g=H[d+12>>2];if((g|0)==(f|0)){break d}if((g|0)>=0){break c}break b}if((f|0)!=-1){break b}}b=H[a+44>>2];H[a+28>>2]=b;H[a+20>>2]=b;H[a+16>>2]=b+H[a+48>>2];a=c;break a}h=H[b+4>>2];j=h>>>0>>0;e=(j?8:0)+b|0;h=g-(j?h:0)|0;H[e>>2]=h+H[e>>2];b=(j?12:4)+b|0;H[b>>2]=H[b>>2]-h;f=f-g|0;i=i-j|0;b=e;continue}break}H[a+28>>2]=0;H[a+16>>2]=0;H[a+20>>2]=0;H[a>>2]=H[a>>2]|32;a=0;if((i|0)==2){break a}a=c-H[b+4>>2]|0}na=d+32|0;return a|0}function Ca(a){a=a|0;var b=0,c=0,d=0,e=0,f=0;if(a){b=a-4|0;f=H[b>>2];c=f;d=b;e=H[a-8>>2];a=e&-2;if((a|0)!=(e|0)){d=b-a|0;c=H[d+4>>2];e=H[d+8>>2];H[c+8>>2]=e;H[e+4>>2]=c;c=a+f|0}a=b+f|0;b=H[a>>2];if((b|0)!=H[(a+b|0)-4>>2]){f=H[a+4>>2];a=H[a+8>>2];H[f+8>>2]=a;H[a+4>>2]=f;c=b+c|0}H[d>>2]=c;H[((c&-4)+d|0)-4>>2]=c|1;b=H[d>>2]-8|0;a:{if(b>>>0<=127){a=(b>>>3|0)-1|0;break a}c=Q(b);a=((b>>>29-c^4)-(c<<2)|0)+110|0;if(b>>>0<=4095){break a}a=((b>>>30-c^2)-(c<<1)|0)+71|0;a=a>>>0>=63?63:a}b=a<<4;H[d+4>>2]=b+26400;b=b+26408|0;H[d+8>>2]=H[b>>2];H[b>>2]=d;H[H[d+8>>2]+4>>2]=d;b=H[6858];c=H[6859];d=a&31;if((a&63)>>>0>=32){a=1<>>32-d}H[6858]=e|b;H[6859]=a|c}}function td(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0;e=na-16|0;na=e;a:{if(H[a+116>>2]){break a}if(c>>>0<=1){Ba(d,1,8882,0);break a}Da(b,e+12|0,2);f=H[e+12>>2];h=f&65535;if(!h){Ba(d,1,8915,0);break a}if(N(h,6)+2>>>0>c>>>0){Ba(d,1,8882,0);break a}d=Fa(N(f,6));if(!d){break a}c=Fa(8);H[a+116>>2]=c;if(!c){Ca(d);break a}H[c>>2]=d;f=c;c=J[e+12>>1];G[f+4>>1]=c;if(!c){g=1;break a}c=0;while(1){g=e+12|0;Da(b+2|0,g,2);f=d+N(c,6)|0;G[f>>1]=H[e+12>>2];Da(b+4|0,g,2);G[f+2>>1]=H[e+12>>2];b=b+6|0;Da(b,g,2);G[f+4>>1]=H[e+12>>2];g=1;c=c+1|0;if(c>>>0>2]+4>>1]){continue}break}}na=e+16|0;return g|0}function Lb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;g=na-32|0;na=g;f=H[a+96>>2];a:{if(!f){Ba(d,1,13752,0);e=0;break a}f=Ea(4,H[f+16>>2]);e=0;if(!f){break a}if(b){j=H[a+96>>2];while(1){b:{e=H[(h<<2)+c>>2];c:{if(e>>>0>=K[j+16>>2]){H[g+16>>2]=e;Ba(d,1,2443,g+16|0);break c}i=f+(e<<2)|0;if(!H[i>>2]){break b}H[g>>2]=e;Ba(d,1,3487,g)}Ca(f);e=0;break a}H[i>>2]=1;h=h+1|0;if((h|0)!=(b|0)){continue}break}}Ca(f);Ca(H[a+64>>2]);d:{if(b){d=b<<2;e=Fa(d);H[a+64>>2]=e;if(!e){H[a+60>>2]=0;e=0;break a}if(!d){break d}B(e,c,d);break d}H[a+64>>2]=0}H[a+60>>2]=b;e=1}na=g+32|0;return e|0}function Fc(a){a=a|0;var b=0,c=0;if(a){wb(H[a>>2]);H[a>>2]=0;b=H[a+72>>2];if(b){Ca(b);H[a+72>>2]=0}b=H[a+68>>2];if(b){Ca(b);H[a+68>>2]=0}b=H[a+108>>2];if(b){Ca(b);H[a+108>>2]=0}b=H[a+116>>2];if(b){c=H[b>>2];if(c){Ca(c);b=H[a+116>>2];H[b>>2]=0}Ca(b);H[a+116>>2]=0}b=H[a+120>>2];if(b){c=H[b+12>>2];if(c){Ca(c);b=H[a+120>>2];H[b+12>>2]=0}c=H[b+4>>2];if(c){Ca(c);b=H[a+120>>2];H[b+4>>2]=0}c=H[b+8>>2];if(c){Ca(c);b=H[a+120>>2];H[b+8>>2]=0}c=H[b>>2];if(c){Ca(c);b=H[a+120>>2];H[b>>2]=0}Ca(b);H[a+120>>2]=0}b=H[a+4>>2];if(b){pb(b);H[a+4>>2]=0}b=H[a+8>>2];if(b){pb(b);H[a+8>>2]=0}Ca(a)}}function Jb(){var a=0,b=0,c=0;a:{a=Ea(1,256);if(a){H[a>>2]=1;H[a+208>>2]=1;F[a+212|0]=I[a+212|0]|6;b=Ea(1,5644);H[a+12>>2]=b;if(!b){break a}b=Ea(1,1e3);H[a+16>>2]=b;if(!b){break a}H[a+48>>2]=0;H[a+52>>2]=0;H[a+44>>2]=-1;H[a+20>>2]=1e3;b:{c=Ea(1,48);if(c){H[c+24>>2]=0;H[c+32>>2]=100;b=Ea(100,24);H[c+28>>2]=b;if(b){break b}Ca(c)}H[a+224>>2]=0;break a}H[c+40>>2]=0;H[a+224>>2]=c;b=qb();H[a+220>>2]=b;if(!b){break a}b=qb();H[a+216>>2]=b;if(!b){break a}c:{if(!xc(1419)){break c}}b=jc();H[a+236>>2]=b;if(!b){b=jc();H[a+236>>2]=b;if(!b){break a}}}else{a=0}return a}wb(a);return 0}function tb(a,b,c,d,e,f){var g=0,h=0,i=0,j=0,k=0,l=0;g=na-240|0;na=g;H[g+236>>2]=c;H[g+232>>2]=b;H[g>>2]=a;l=!e;a:{b:{c:{d:{if((b|0)!=1){h=a;i=1;break d}h=a;i=1;if(c){break d}e=a;break c}while(1){j=(d<<2)+f|0;e=h-H[j>>2]|0;if((Ic(e,a)|0)<=0){e=h;break c}k=l^-1;l=1;e:{if(!((k|(d|0)<2)&1)){j=H[j-8>>2];k=h-8|0;if((Ic(k,e)|0)>=0){break e}if((Ic(k-j|0,e)|0)>=0){break e}}H[(i<<2)+g>>2]=e;b=wc(b,c);ub(g+232|0,b);i=i+1|0;d=b+d|0;h=e;c=H[g+236>>2];b=H[g+232>>2];if(c|(b|0)!=1){continue}break b}break}e=h;break b}if(!l){break a}}vc(g,i);Bb(e,d,f)}na=g+240|0}function me(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;e=na-16|0;na=e;if(H[a+8>>2]==16){g=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{g=H[a+12>>2]}h=H[a+96>>2];f=K[h+16>>2]<257?1:2;a:{if(f>>>0>=c>>>0){c=0;Ba(d,1,4669,0);break a}H[e+12>>2]=(f^-1)+c;Da(b,e+8|0,f);i=H[e+8>>2];if(i>>>0>=K[h+16>>2]){c=0;Ba(d,1,14067,0);break a}c=1;b=b+f|0;Da(b,H[g+5584>>2]+N(i,1080)|0,1);if(!Lc(a,H[e+8>>2],b+1|0,e+12|0,d)){c=0;Ba(d,1,4669,0);break a}if(!H[e+12>>2]){break a}c=0;Ba(d,1,4669,0)}na=e+16|0;return c|0}function Gc(a,b){var c=0,d=0,e=0,f=0,g=0;f=na-32|0;na=f;c=H[a+60>>2];a:{b:{if(c){g=1;while(1){e=H[H[a+64>>2]+(d<<2)>>2];if(!H[(H[H[a+100>>2]+24>>2]+N(e,52)|0)+44>>2]){H[f+16>>2]=e;Ba(b,2,7604,f+16|0);g=0;c=H[a+60>>2]}d=d+1|0;if(c>>>0>d>>>0){continue}break}break b}g=1;c=H[a+100>>2];e=1;if(!H[c+16>>2]){break a}while(1){if(!H[(H[c+24>>2]+N(d,52)|0)+44>>2]){H[f>>2]=d;Ba(b,2,7604,f);g=0;c=H[a+100>>2]}d=d+1|0;if(d>>>0>2]){continue}break}}e=1;if(g){break a}Ba(b,1,2897,0);e=0}na=f+32|0;return e}function vd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0;f=na-16|0;na=f;e=H[a+120>>2];a:{if(!e){Ba(d,1,8836,0);c=0;break a}if(H[e+12>>2]){Ba(d,1,11598,0);c=0;break a}e=I[e+18|0];g=e<<2;if(g>>>0>c>>>0){Ba(d,1,8803,0);c=0;break a}g=Fa(g);c=0;if(!g){break a}if(e){d=0;while(1){c=f+12|0;Da(b,c,2);h=g+(d<<2)|0;G[h>>1]=H[f+12>>2];Da(b+2|0,c,1);F[h+2|0]=H[f+12>>2];Da(b+3|0,c,1);F[h+3|0]=H[f+12>>2];b=b+4|0;d=d+1|0;if((e|0)!=(d|0)){continue}break}}H[H[a+120>>2]+12>>2]=g;c=1}na=f+16|0;return c|0}function Zd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;e=na-16|0;na=e;g=H[H[a+96>>2]+16>>2];a:{if((g+2|0)!=(c|0)){Ba(d,1,4617,0);break a}Da(b,e+12|0,2);if(H[e+12>>2]!=(g|0)){Ba(d,1,4617,0);break a}if(!g){f=1;break a}c=b+2|0;a=H[H[a+96>>2]+24>>2];b=0;while(1){Da(c,e+8|0,1);f=H[e+8>>2];h=f&127;i=h+1|0;H[a+24>>2]=i;H[a+32>>2]=f>>>7&1;if(h>>>0>=31){H[e+4>>2]=i;H[e>>2]=b;Ba(d,1,15402,e);f=0;break a}a=a+52|0;f=1;c=c+1|0;b=b+1|0;if((g|0)!=(b|0)){continue}break}}na=e+16|0;return f|0}function tc(a,b,c,d,e){var f=0,g=0;f=na-208|0;na=f;H[f+204>>2]=c;c=f+160|0;y(c,0,40);H[f+200>>2]=H[f+204>>2];if((sc(0,b,f+200|0,f+80|0,c,d,e)|0)>=0){c=H[a>>2];H[a>>2]=c&-33;a:{b:{c:{if(!H[a+48>>2]){H[a+48>>2]=80;H[a+28>>2]=0;H[a+16>>2]=0;H[a+20>>2]=0;g=H[a+44>>2];H[a+44>>2]=f;break c}if(H[a+16>>2]){break b}}if(Db(a)){break a}}sc(a,b,f+200|0,f+80|0,f+160|0,d,e)}if(g){ra[H[a+36>>2]](a,0,0)|0;H[a+48>>2]=0;H[a+44>>2]=g;H[a+28>>2]=0;H[a+16>>2]=0;H[a+20>>2]=0}H[a>>2]=H[a>>2]|c&32}na=f+208|0}function je(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0;e=na-16|0;na=e;a:{b:{h=e+8|0;c:{if(K[H[a+96>>2]+16>>2]<=256){if(c){f=-1;g=1;break c}Ba(d,1,4695,0);a=0;break a}if(c>>>0<=1){break b}f=-2;g=2}Da(b,h,g);H[e+12>>2]=c+f;c=H[e+8>>2];f=H[H[a+96>>2]+16>>2];if(c>>>0>=f>>>0){H[e+4>>2]=f;H[e>>2]=c;Ba(d,1,7712,e);a=0;break a}if(!Kc(a,c,b+g|0,e+12|0,d)){Ba(d,1,4695,0);a=0;break a}a=1;if(!H[e+12>>2]){break a}Ba(d,1,4695,0);a=0;break a}Ba(d,1,4695,0);a=0}na=e+16|0;return a|0}function Ed(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0;f=H[a+32>>2];H[a+36>>2]=f;a:{e=H[a+48>>2];if(e){while(1){e=ra[H[a+20>>2]](f,e,H[a>>2])|0;if((e|0)==-1){break a}f=e+H[a+36>>2]|0;H[a+36>>2]=f;e=H[a+48>>2]-e|0;H[a+48>>2]=e;if(e){continue}break}f=H[a+32>>2]}H[a+48>>2]=0;H[a+36>>2]=f;if(!(ra[H[a+28>>2]](b,c,H[a>>2])|0)){H[a+68>>2]=H[a+68>>2]|8;return 0}H[a+56>>2]=b;H[a+60>>2]=c;return 1}H[a+68>>2]=H[a+68>>2]|8;Ba(d,4,15604,0);H[a+68>>2]=H[a+68>>2]|8;return 0}function Ba(a,b,c,d){var e=0,f=0;e=na-528|0;na=e;a:{if(!a){break a}b:{c:{switch(b-1|0){case 0:b=a+12|0;break b;case 1:b=a+16|0;a=a+4|0;break b;case 3:break c;default:break a}}b=a+20|0;a=a+8|0}b=H[b>>2];if(!b|!c){break a}f=H[a>>2];y(e,0,512);H[e+524>>2]=d;a=na-160|0;na=a;H[a+148>>2]=e;H[a+152>>2]=511;y(a,0,144);H[a+76>>2]=-1;H[a+36>>2]=105;H[a+80>>2]=-1;H[a+44>>2]=a+159;H[a+84>>2]=a+148;F[e|0]=0;tc(a,c,d,103,104);na=a+160|0;F[e+511|0]=0;ra[b|0](e,f)}na=e+528|0}function fc(a){var b=0,c=0,d=0;a:{if(!a){break a}b=H[a+8>>2];if(!b){break a}c=b&3;a=H[a+12>>2];if(b>>>0>=4){d=b&-4;b=0;while(1){H[a+60>>2]=0;H[a+52>>2]=999;H[a+56>>2]=0;H[a+44>>2]=0;H[a+36>>2]=999;H[a+40>>2]=0;H[a+28>>2]=0;H[a+20>>2]=999;H[a+24>>2]=0;H[a+12>>2]=0;H[a+4>>2]=999;H[a+8>>2]=0;a=a- -64|0;b=b+4|0;if((d|0)!=(b|0)){continue}break}if(!c){break a}}b=0;while(1){H[a+12>>2]=0;H[a+4>>2]=999;H[a+8>>2]=0;a=a+16|0;b=b+1|0;if((c|0)!=(b|0)){continue}break}}}function Bd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0;if(H[a+100>>2]!=1){Ba(d,1,11401,0);return 0}a:{if(c>>>0<=7){break a}Da(b,a+56|0,4);Da(b+4|0,a+60|0,4);if(c&3){break a}c=c-8|0;e=c>>>2|0;H[a+64>>2]=e;b:{if(!c){break b}c=Ea(e,4);H[a+68>>2]=c;if(!c){Ba(d,1,2235,0);return 0}if(!H[a+64>>2]){break b}d=b+8|0;c=0;while(1){Da(d,H[a+68>>2]+(c<<2)|0,4);d=d+4|0;c=c+1|0;if(c>>>0>2]){continue}break}}H[a+100>>2]=H[a+100>>2]|2;return 1}Ba(d,1,5955,0);return 0}function ke(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0;e=na-16|0;na=e;H[e+12>>2]=c;a:{if(!(!Kc(a,0,b,e+12|0,d)|H[e+12>>2])){if(H[a+8>>2]==16){b=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{b=H[a+12>>2]}f=1;if(K[H[a+96>>2]+16>>2]<2){break a}c=H[b+5584>>2];g=c+28|0;b=1;d=c;while(1){H[d+1104>>2]=H[c+24>>2];H[d+1884>>2]=H[c+804>>2];B(d+1108|0,g,776);d=d+1080|0;b=b+1|0;if(b>>>0>2]+16>>2]){continue}break}break a}Ba(d,1,4591,0)}na=e+16|0;return f|0}function dc(a,b,c,d){var e=0,f=0,g=0;f=na-128|0;na=f;e=f;b=H[b+12>>2]+(c<<4)|0;c=H[b>>2];if(c){while(1){H[e>>2]=b;e=e+4|0;b=c;c=H[c>>2];if(c){continue}break}}while(1){c=H[b+8>>2];if((g|0)>(c|0)){H[b+8>>2]=g;c=g}a:{if((c|0)>=(d|0)){break a}while(1){if(H[b+4>>2]<=(c|0)){break a}b:{if(Ra(a,1)){H[b+4>>2]=c;break b}c=c+1|0}if((c|0)<(d|0)){continue}break}}H[b+8>>2]=c;if((e|0)!=(f|0)){e=e-4|0;b=H[e>>2];g=c;continue}break}na=f+128|0;return H[b+4>>2]<(d|0)}function Ld(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0;a:{if(!Wa(H[a+8>>2],54,c)){break a}h=H[a+8>>2];d=H[h>>2];f=H[h+8>>2];b:{if(d){e=1;c:{if((d|0)!=1){j=d&1;d=d&-2;while(1){g=0;d:{if(!e){break d}g=0;if(!(ra[H[f>>2]](a,b,c)|0)){break d}g=(ra[H[f+4>>2]](a,b,c)|0)!=0}e=g;f=f+8|0;i=i+2|0;if((d|0)!=(i|0)){continue}break}if(!j){break c}}if(!e){e=0;break c}e=(ra[H[f>>2]](a,b,c)|0)!=0}Pa(h);if(!e){break a}break b}Pa(h)}k=1}return k|0}function le(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0;e=na-16|0;na=e;g=H[H[a+96>>2]+16>>2];f=g>>>0<257?1:2;a:{if((f+2|0)!=(c|0)){a=0;Ba(d,1,4285,0);break a}if(H[a+8>>2]==16){c=H[a+180>>2]+N(H[a+228>>2],5644)|0}else{c=H[a+12>>2]}Da(b,e+12|0,f);a=1;b=b+f|0;Da(b,e+8|0,1);f=H[e+12>>2];if(f>>>0>=g>>>0){H[e+4>>2]=g;H[e>>2]=f;Ba(d,1,14923,e);a=0;break a}Da(b+1|0,(H[c+5584>>2]+N(f,1080)|0)+808|0,1)}na=e+16|0;return a|0}function qe(){var a=0,b=0,c=0;while(1){b=a<<4;c=b+26400|0;H[b+26404>>2]=c;H[b+26408>>2]=c;a=a+1|0;if((a|0)!=64){continue}break}zc(48);a=na-16|0;na=a;a:{if(la(a+12|0,a+8|0)|0){break a}b=vb((H[a+12>>2]<<2)+4|0);H[6860]=b;if(!b){break a}b=vb(H[a+8>>2]);if(b){c=H[6860];H[c+(H[a+12>>2]<<2)>>2]=0;if(!(ka(c|0,b|0)|0)){break a}}H[6860]=0}na=a+16|0;H[6875]=8192;H[6873]=94240;H[6867]=42;H[6874]=65536}function ye(a,b,c){var d=0,e=0,f=0,g=0;g=c&63;f=g;e=f&31;if(f>>>0>=32){f=-1>>>e|0}else{d=-1>>>e|0;f=d|(1<>>0>=32){d=f<>>32-e|d<>>0>=32){d=-1<>>32-d}a=c&a;b=b&d;d=e&31;if(e>>>0>=32){c=0;a=b>>>d|0}else{c=b>>>d|0;a=((1<>>d}a=a|g;qa=c|f;return a} +function eb(a,b,c){var d=0;if(!H[a+12>>2]){ra[b|0](c,H[a+36>>2]);return}d=Fa(8);a:{if(!d){break a}H[d+4>>2]=c;H[d>>2]=b;b=Fa(8);if(!b){Ca(d);return}H[b>>2]=d;c=N(H[a+4>>2],100);H[a+40>>2]=c;while(1){if((c|0)>2]){continue}break}H[b+4>>2]=H[a+20>>2];H[a+20>>2]=b;H[a+24>>2]=H[a+24>>2]+1;b=H[a+28>>2];if(!b){break a}H[H[b>>2]+8>>2]=0;H[a+28>>2]=H[b+4>>2];H[a+32>>2]=H[a+32>>2]-1;Ca(b)}}function Mc(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0;H[a+184>>2]=b;d=H[a+96>>2];a:{if(!d){break a}f=H[d+24>>2];if(!f){break a}e=H[a+12>>2];if(!e|!H[e+5584>>2]){break a}e=H[d+16>>2];if(!e){return 1}d=0;while(1){if(K[(H[H[a+12>>2]+5584>>2]+N(d,1080)|0)+4>>2]<=b>>>0){Ba(c,1,9177,0);return 0}H[(N(d,52)+f|0)+40>>2]=b;g=1;d=d+1|0;if((e|0)!=(d|0)){continue}break}}return g|0}function Ac(a,b,c){var d=0,e=0,f=0;a:{d=H[c+16>>2];if(!d){if(Db(c)){break a}d=H[c+16>>2]}e=H[c+20>>2];if(d-e>>>0>>0){ra[H[c+36>>2]](c,a,b)|0;return}b:{c:{if(!b|H[c+80>>2]<0){break c}d=b;while(1){f=a+d|0;if(I[f-1|0]!=10){d=d-1|0;if(d){continue}break c}break}if(ra[H[c+36>>2]](c,a,d)>>>0>>0){break a}b=b-d|0;e=H[c+20>>2];break b}f=a}ab(e,f,b);H[c+20>>2]=H[c+20>>2]+b}}function Bc(a){var b=0,c=0;b=H[a+76>>2];if(!((b|0)>=0&(!b|H[6867]!=(b&1073741823)))){a:{if(H[a+80>>2]==10){break a}b=H[a+20>>2];if((b|0)==H[a+16>>2]){break a}H[a+20>>2]=b+1;F[b|0]=10;return}Cc(a);return}b=a+76|0;c=H[b>>2];H[b>>2]=c?c:1073741823;b:{c:{if(H[a+80>>2]==10){break c}c=H[a+20>>2];if((c|0)==H[a+16>>2]){break c}H[a+20>>2]=c+1;F[c|0]=10;break b}Cc(a)}H[b>>2]=0}function Ka(a,b,c,d,e,f,g,h){var i=0,j=0;i=+O(e-a|0);j=i*1.402;if(P(j)<2147483647){e=~~j}else{e=-2147483648}e=e+c|0;H[f>>2]=(e|0)>=0?(b|0)>(e|0)?e:b:0;j=+O(d-a|0);i=j*.344+i*.714;if(P(i)<2147483647){a=~~i}else{a=-2147483648}a=c-a|0;H[g>>2]=(a|0)>=0?(a|0)<(b|0)?a:b:0;i=j*1.772;if(P(i)<2147483647){a=~~i}else{a=-2147483648}a=a+c|0;H[h>>2]=(a|0)>=0?(a|0)<(b|0)?a:b:0}function yb(a,b){var c=0,d=0,e=0,f=0,g=0,h=0;if(a){c=H[a+4>>2];if(c){Ca(c);H[a+4>>2]=0}if(b){c=a;while(1){d=H[c+200>>2];if(d){e=0;f=H[c+196>>2];if(f){while(1){g=H[d+12>>2];if(g){Ca(g);H[d+12>>2]=0;f=H[c+196>>2]}d=d+16|0;e=e+1|0;if(e>>>0>>0){continue}break}d=H[c+200>>2]}Ca(d);H[c+200>>2]=0}c=c+240|0;h=h+1|0;if((h|0)!=(b|0)){continue}break}}Ca(a)}}function dd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0;e=H[a+84>>2];f=H[e>>2];d=H[e+4>>2];h=H[a+28>>2];g=H[a+20>>2]-h|0;g=d>>>0>>0?d:g;if(g){ab(f,h,g);f=g+H[e>>2]|0;H[e>>2]=f;d=H[e+4>>2]-g|0;H[e+4>>2]=d}d=c>>>0>d>>>0?d:c;if(d){ab(f,b,d);f=d+H[e>>2]|0;H[e>>2]=f;H[e+4>>2]=H[e+4>>2]-d}F[f|0]=0;b=H[a+44>>2];H[a+28>>2]=b;H[a+20>>2]=b;return c|0}function rd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0,h=0;e=H[c+8>>2];d=e>>>0<=1?1:e;f=H[c+4>>2];g=f-H[c>>2]|0;while(1){h=d;d=d<<1;if(h-g>>>0>>0){continue}break}if((e|0)!=(h|0)){d=Fa(h);if(!d){return-1}e=H[c>>2];if(e){if(g){B(d,e,g)}Ca(H[c>>2])}H[c+8>>2]=h;H[c>>2]=d;f=d+g|0;H[c+4>>2]=f}if(b){B(f,a,b)}H[c+4>>2]=H[c+4>>2]+b;return b|0}function Yb(a){H[a+100>>2]=20832;H[a+96>>2]=20832;H[a+92>>2]=20832;H[a+88>>2]=20832;H[a+84>>2]=20832;H[a+80>>2]=20832;H[a+76>>2]=20832;H[a+72>>2]=20832;H[a+68>>2]=20832;H[a+64>>2]=20832;H[a+60>>2]=20832;H[a+56>>2]=20832;H[a+52>>2]=20832;H[a+48>>2]=20832;H[a+44>>2]=20832;H[a+40>>2]=20832;H[a+36>>2]=20832;H[a+32>>2]=20832;H[a+28>>2]=20832}function Ra(a,b){var c=0,d=0,e=0,f=0;if((b|0)<=0){return 0}c=H[a+12>>2];d=H[a+16>>2];while(1){e=b;a:{if(d){break a}c=c<<8&65280;H[a+12>>2]=c;d=(c|0)==65280?7:8;H[a+16>>2]=d;b=H[a+8>>2];if(b>>>0>=K[a+4>>2]){break a}H[a+8>>2]=b+1;c=I[b|0]|c;H[a+12>>2]=c}d=d-1|0;H[a+16>>2]=d;b=e-1|0;f=(c>>>d&1)<>>0>1){continue}break}return f}function xd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0;f=na-16|0;na=f;e=H[a+24>>2];if((e|0)!=255){H[f>>2]=e;Ba(d,2,2678,f)}a:{b:{if(H[a+20>>2]==(c|0)){if(c){break b}e=1;break a}e=0;Ba(d,1,14510,0);break a}c=0;while(1){e=1;Da(b,(H[a+72>>2]+N(c,12)|0)+8|0,1);b=b+1|0;c=c+1|0;if(c>>>0>2]){continue}break}}na=f+16|0;return e|0}function Da(a,b,c){var d=0,e=0;H[b>>2]=0;a:{if(!c){break a}d=c&3;b=b+c|0;if(c>>>0>=4){e=c&-4;c=0;while(1){F[b-1|0]=I[a|0];F[b-2|0]=I[a+1|0];F[b-3|0]=I[a+2|0];b=b-4|0;F[b|0]=I[a+3|0];a=a+4|0;c=c+4|0;if((e|0)!=(c|0)){continue}break}if(!d){break a}}c=0;while(1){b=b-1|0;F[b|0]=I[a|0];a=a+1|0;c=c+1|0;if((d|0)!=(c|0)){continue}break}}}function de(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0;e=na-16|0;na=e;a:{if(!c){Ba(d,1,4106,0);a=0;break a}Da(b,e+12|0,1);f=c-1|0;a=1;if(!f){break a}a=0;c=0;while(1){b=b+1|0;Da(b,e+8|0,1);g=H[e+8>>2];c=g<<24>>31&(g&127|c)<<7;a=a+1|0;if((f|0)!=(a|0)){continue}break}a=1;if(!c){break a}Ba(d,1,4106,0);a=0}na=e+16|0;return a|0}function bc(a,b,c,d){var e=0,f=0,g=O(0),h=0,i=O(0),j=0,k=O(0);if(d){while(1){e=f<<2;h=e+b|0;i=L[h>>2];j=a+e|0;g=L[j>>2];e=c+e|0;k=L[e>>2];L[j>>2]=O(k*O(1.4019999504089355))+g;L[h>>2]=O(g+O(i*O(-.3441300094127655)))+O(k*O(-.714139997959137));L[e>>2]=g+O(i*O(1.7719999551773071));f=f+1|0;if((f|0)!=(d|0)){continue}break}}}function Bb(a,b,c){var d=0,e=0,f=0,g=0,h=0,i=0;f=na-240|0;na=f;H[f>>2]=a;g=1;a:{if((b|0)<2){break a}d=a;while(1){d=d-8|0;h=b-2|0;e=d-H[(h<<2)+c>>2]|0;if((Ic(a,e)|0)>=0){if((Ic(a,d)|0)>=0){break a}}i=e;e=(Ic(e,d)|0)>=0;d=e?i:d;H[(g<<2)+f>>2]=d;g=g+1|0;b=e?b-1|0:h;if((b|0)>1){continue}break}}vc(f,g);na=f+240|0}function vc(a,b){var c=0,d=0,e=0,f=0,g=0,h=0;c=8;f=na-256|0;na=f;if((b|0)>=2){h=(b<<2)+a|0;H[h>>2]=f;while(1){e=c>>>0>=256?256:c;ab(H[h>>2],H[a>>2],e);d=0;while(1){g=(d<<2)+a|0;d=d+1|0;ab(H[g>>2],H[(d<<2)+a>>2],e);H[g>>2]=H[g>>2]+e;if((b|0)!=(d|0)){continue}break}c=c-e|0;if(c){continue}break}}na=f+256|0}function Wc(a){a=a|0;var b=0,c=0,d=0,e=0;b=H[a+24>>2];if(b){c=H[a+28>>2];e=(c>>>0)/52|0;if(c>>>0>=52){while(1){c=H[b>>2];if(c){Ca(c-1|0);H[b>>2]=0}c=H[b+4>>2];if(c){Ca(c);H[b+4>>2]=0}c=H[b+8>>2];if(c){Ca(c);H[b+8>>2]=0}b=b+52|0;d=d+1|0;if((e|0)!=(d|0)){continue}break}b=H[a+24>>2]}Ca(b);H[a+24>>2]=0}}function Xc(a){a=a|0;var b=0,c=0,d=0,e=0;b=H[a+24>>2];if(b){c=H[a+28>>2];e=(c>>>0)/68|0;if(c>>>0>=68){while(1){c=H[b>>2];if(c){Ca(c);H[b>>2]=0}c=H[b+4>>2];if(c){Ca(c);H[b+4>>2]=0}Ca(H[b+60>>2]);H[b+60>>2]=0;b=b+68|0;d=d+1|0;if((e|0)!=(d|0)){continue}break}b=H[a+24>>2]}Ca(b);H[a+24>>2]=0}}function ad(a,b){a=a|0;b=b|0;var c=0,d=0;c=H[a+32>>2];b=H[a+28>>2];d=b+8|0;if(c>>>0>=d>>>0){while(1){nb(a,H[a+24>>2]+(b<<2)|0,H[a+20>>2],8);c=H[a+32>>2];b=d;d=b+8|0;if(c>>>0>=d>>>0){continue}break}}if(b>>>0>>0){nb(a,H[a+24>>2]+(b<<2)|0,H[a+20>>2],c-b|0)}Ca(H[a>>2]);Ca(a)}function Cd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0;e=na-16|0;na=e;a:{if(H[a+100>>2]){Ba(d,1,11302,0);a=0;break a}if((c|0)!=4){Ba(d,1,5991,0);a=0;break a}Da(b,e+12|0,4);if(H[e+12>>2]!=218793738){Ba(d,1,5007,0);a=0;break a}H[a+100>>2]=H[a+100>>2]|1;a=1}na=e+16|0;return a|0}function Wa(a,b,c){var d=0,e=0;a:{d=H[a>>2];e=H[a+4>>2];b:{if((d|0)!=(e|0)){e=H[a+8>>2];break b}d=e+10|0;H[a+4>>2]=d;e=Ha(H[a+8>>2],d<<2);if(!e){break a}H[a+8>>2]=e;d=H[a>>2]}H[(d<<2)+e>>2]=b;H[a>>2]=d+1;return 1}Ca(H[a+8>>2]);H[a>>2]=0;H[a+4>>2]=0;Ba(c,1,6123,0);return 0}function $a(a,b,c){var d=0,e=0,f=0,g=0;if((b|0)==1|b>>>0>1){while(1){d=a;e=b;c=c-1|0;a=ve(a,b,10,0);b=qa;f=c,g=re(a,b,246)+d|48,F[f|0]=g;if(e>>>0>9){continue}break}}if(a|b){while(1){c=c-1|0;b=(a>>>0)/10|0;F[c|0]=N(b,246)+a|48;d=a>>>0>9;a=b;if(d){continue}break}}return c}function Cc(a){var b=0,c=0,d=0;c=na-16|0;na=c;F[c+15|0]=10;b=H[a+16>>2];a:{if(!b){if(Db(a)){break a}b=H[a+16>>2]}d=b;b=H[a+20>>2];if(!((d|0)==(b|0)|H[a+80>>2]==10)){H[a+20>>2]=b+1;F[b|0]=10;break a}if((ra[H[a+36>>2]](a,c+15|0,1)|0)!=1){break a}}na=c+16|0}function rc(a){var b=0,c=0,d=0,e=0,f=0;d=H[a>>2];b=F[d|0]-48|0;if(b>>>0>9){return 0}while(1){e=-1;if(c>>>0<=214748364){c=N(c,10);e=(c^2147483647)>>>0>>0?-1:c+b|0}b=d+1|0;H[a>>2]=b;f=F[d+1|0];c=e;d=b;b=f-48|0;if(b>>>0<10){continue}break}return c} +function pc(a,b){var c=0,d=0,e=0;x(+a);d=s(1)|0;e=s(0)|0;c=d>>>20&2047;if((c|0)!=2047){if(!c){if(a==0){c=0}else{a=pc(a*0x10000000000000000,b);c=H[b>>2]+-64|0}H[b>>2]=c;return a}H[b>>2]=c-1022;u(0,e|0);u(1,d&-2146435073|1071644672);a=+w()}return a}function Qd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=O(0),f=0,g=0;d=na-16|0;na=d;if(c){while(1){Nc(a,d+12|0);e=L[d+12>>2];if(O(P(e))>2]=f;b=b+4|0;a=a+4|0;g=g+1|0;if((g|0)!=(c|0)){continue}break}}na=d+16|0}function Ua(a){var b=0,c=0,d=0;if(a){b=H[a+24>>2];if(b){c=H[a+16>>2];if(c){b=0;while(1){d=H[(H[a+24>>2]+N(b,52)|0)+44>>2];if(d){Ca(d);c=H[a+16>>2]}b=b+1|0;if(c>>>0>b>>>0){continue}break}b=H[a+24>>2]}Ca(b)}b=H[a+28>>2];if(b){Ca(b)}Ca(a)}}function Pd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0,g=0;d=na-16|0;na=d;if(c){while(1){Kb(a,d+8|0);e=M[d+8>>3];if(P(e)<2147483647){f=~~e}else{f=-2147483648}H[b>>2]=f;b=b+4|0;a=a+8|0;g=g+1|0;if((g|0)!=(c|0)){continue}break}}na=d+16|0}function qd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0,f=0;d=H[c+4>>2];e=H[c>>2]+H[c+8>>2]|0;if((d|0)==(e|0)){qa=-1;return-1}H[c+4>>2]=a+d;f=a;c=e-d|0;d=c;e=a>>>0>>0;a=c>>31;c=e&(a|0)>=(b|0)|(a|0)>(b|0);d=c?f:d;qa=c?b:a;return d|0}function ue(a,b,c,d){var e=0,f=0,g=0,h=0;f=b^d;g=f>>31;e=b>>31;a=a^e;h=a-e|0;e=(b^e)-((a>>>0>>0)+e|0)|0;a=d>>31;b=c^a;f=f>>31;a=ve(h,e,b-a|0,(a^d)-((a>>>0>b>>>0)+a|0)|0)^f;b=a-f|0;qa=(g^qa)-((a>>>0>>0)+g|0)|0;return b}function Va(a){var b=0,c=0,d=0,e=0;if(a){b=H[a+20>>2];c=H[a+16>>2];if(N(b,c)){while(1){e=H[H[a+24>>2]+(d<<2)>>2];if(e){Ca(e);c=H[a+16>>2];b=H[a+20>>2]}d=d+1|0;if(d>>>0>>0){continue}break}}Ca(H[a+24>>2]);Ca(a)}}function cc(a,b,c,d){var e=0,f=0,g=0,h=0,i=0,j=0,k=0;if(d){while(1){e=f<<2;g=e+a|0;h=c+e|0;i=H[h>>2];j=b+e|0;k=H[j>>2];e=H[g>>2]-(i+k>>2)|0;H[g>>2]=e+i;H[j>>2]=e;H[h>>2]=e+k;f=f+1|0;if((f|0)!=(d|0)){continue}break}}}function bb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0;H[a+48>>2]=0;H[a+36>>2]=H[a+32>>2];e=ra[H[a+28>>2]](b,c,H[a>>2])|0;d=H[a+68>>2];if(!e){H[a+68>>2]=d|4;return 0}H[a+56>>2]=b;H[a+60>>2]=c;H[a+68>>2]=d&-5;return 1}function Na(a,b,c,d,e){var f=0;f=na-256|0;na=f;if(!(e&73728|(c|0)<=(d|0))){d=c-d|0;c=d>>>0<256;Dc(f,b,c?d:256);if(!c){while(1){La(a,f,256);d=d-256|0;if(d>>>0>255){continue}break}}La(a,f,d)}na=f+256|0}function re(a,b,c){var d=0,e=0,f=0,g=0,h=0;e=c>>>16|0;d=a>>>16|0;h=N(e,d);f=c&65535;a=a&65535;g=N(f,a);d=(g>>>16|0)+N(d,f)|0;a=(d&65535)+N(a,e)|0;qa=h+N(b,c)+(d>>>16)+(a>>>16)|0;return g&65535|a<<16}function Db(a){var b=0;b=H[a+72>>2];H[a+72>>2]=b-1|b;b=H[a>>2];if(b&8){H[a>>2]=b|32;return-1}H[a+4>>2]=0;H[a+8>>2]=0;b=H[a+44>>2];H[a+28>>2]=b;H[a+20>>2]=b;H[a+16>>2]=b+H[a+48>>2];return 0}function hc(a){var b=0,c=0;a:{if(I[a+12|0]==255){H[a+12>>2]=65280;H[a+16>>2]=7;b=H[a+8>>2];c=0;if(b>>>0>=K[a+4>>2]){break a}H[a+8>>2]=b+1;H[a+12>>2]=I[b|0]|65280}H[a+16>>2]=0;c=1}return c}function sd(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0;e=H[c+4>>2];d=H[c>>2]+H[c+8>>2]|0;if((e|0)==(d|0)){return-1}d=d-e|0;b=b>>>0>d>>>0?d:b;if(b){B(a,e,b)}H[c+4>>2]=b+H[c+4>>2];return b|0}function Ud(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0;d=na-16|0;na=d;if(c){while(1){Nc(a,d+12|0);L[b>>2]=L[d+12>>2];b=b+4|0;a=a+4|0;e=e+1|0;if((e|0)!=(c|0)){continue}break}}na=d+16|0}function Td(a,b,c){a=a|0;b=b|0;c=c|0;var d=0,e=0;d=na-16|0;na=d;if(c){while(1){Kb(a,d+8|0);L[b>>2]=M[d+8>>3];b=b+4|0;a=a+8|0;e=e+1|0;if((e|0)!=(c|0)){continue}break}}na=d+16|0}function bd(a,b){a=a|0;b=b|0;b=H[a+28>>2];if(b>>>0>2]){while(1){$b(a,H[a+24>>2]+(N(H[a+20>>2],b)<<2)|0);b=b+1|0;if(b>>>0>2]){continue}break}}Ca(H[a>>2]);Ca(a)}function Ic(a,b){a=a|0;b=b|0;var c=0,d=0;c=H[a>>2];d=H[b>>2];a=H[a+4>>2];b=H[b+4>>2];return(c>>>0>d>>>0&(a|0)>=(b|0)|(a|0)>(b|0))-(c>>>0>>0&(a|0)<=(b|0)|(a|0)<(b|0))|0}function kd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0;e=na-16|0;na=e;a=yc(ea(H[a+60>>2],b|0,c|0,d&255,e+8|0)|0);na=e+16|0;qa=a?-1:H[e+12>>2];return(a?-1:H[e+8>>2])|0}function mc(a,b,c,d){var e=0,f=0;e=na-16|0;na=e;if(c){while(1){Da(a,e+12|0,d);L[b>>2]=K[e+12>>2];b=b+4|0;a=a+d|0;f=f+1|0;if((f|0)!=(c|0)){continue}break}}na=e+16|0}function lc(a,b,c,d){var e=0,f=0;e=na-16|0;na=e;if(c){while(1){Da(a,e+12|0,d);H[b>>2]=H[e+12>>2];b=b+4|0;a=a+d|0;f=f+1|0;if((f|0)!=(c|0)){continue}break}}na=e+16|0}function Kb(a,b){F[b+7|0]=I[a|0];F[b+6|0]=I[a+1|0];F[b+5|0]=I[a+2|0];F[b+4|0]=I[a+3|0];F[b+3|0]=I[a+4|0];F[b+2|0]=I[a+5|0];F[b+1|0]=I[a+6|0];F[b|0]=I[a+7|0]}function Qa(a){var b=0,c=0,d=0,e=0;b=H[a+12>>2];e=b;c=H[a+8>>2];if(!(b|c)){qa=0;return 0}d=H[a+56>>2];b=c-d|0;qa=e-(H[a+60>>2]+(c>>>0>>0)|0)|0;return b}function kc(a,b){var c=0;c=na-16|0;na=c;if(a){if(b&3){a=28}else{a=gb(b,a);H[c+12>>2]=a;a=a?0:48}a=a?0:H[c+12>>2]}else{a=0}na=c+16|0;return a}function Yc(a){a=a|0;var b=0;if(a){b=H[a+116>>2];if(b){Ca(b);H[a+116>>2]=0}b=H[a+120>>2];if(b){Ca(b);H[a+120>>2]=0}Ca(H[a+148>>2]);Ca(a)}} +function sb(a,b){var c=0,d=0;a:{if(b>>>0<=31){d=H[a>>2];c=a+4|0;break a}b=b-32|0;c=a}c=H[c>>2];H[a>>2]=d<>2]=c<>>32-b}function ub(a,b){var c=0,d=0;c=H[a+4>>2];a:{if(b>>>0<=31){d=H[a>>2];break a}b=b-32|0;d=c;c=0}H[a+4>>2]=c>>>b;H[a>>2]=c<<32-b|d>>>b}function ae(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;if(H[H[a+96>>2]+16>>2]<<2!=(c|0)){Ba(d,1,4464,0);a=0}else{a=1}return a|0}function jc(){var a=0,b=0;a=Ea(1,44);a:{if(a){H[a+16>>2]=0;b=Ea(1,8);H[a+36>>2]=b;if(b){break a}Ca(a)}a=0}return a}function Pb(a,b){a=a|0;b=b|0;if(!(!a|!b)){H[a+188>>2]=H[b+4>>2];H[a+184>>2]=H[b>>2];H[a+248>>2]=H[b+8248>>2]&2}}function qb(){var a=0,b=0;a=Ea(1,12);if(a){H[a+4>>2]=10;b=Ea(10,4);H[a+8>>2]=b;if(b){return a}Ca(a)}return 0}function Id(a,b,c,d,e,f,g,h,i,j,k){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;i=i|0;j=j|0;k=k|0;D()}function fb(a){var b=0;if(a){b=H[a+4>>2];if(b){ra[b|0](H[a>>2])}Ca(H[a+32>>2]);H[a+32>>2]=0;Ca(a)}}function Ob(a,b){a=a|0;b=b|0;a:{if(!a){break a}H[a+208>>2]=b;if(!b){break a}F[a+92|0]=I[a+92|0]|8}}function pd(a,b,c){a=a|0;b=b|0;c=c|0;b=H[c+8>>2];H[c+4>>2]=H[c>>2]+(a>>>0>b>>>0?b:a);return 1}function ee(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;if(c){a=1}else{Ba(d,1,4375,0);a=0}return a|0}function oc(a,b){if(!((b&-128)==57216|b>>>0<=127)){H[6597]=25;return-1}F[a|0]=b;return 1}function kb(a){H[a+16>>2]=0;H[a+20>>2]=0;H[a+8>>2]=0;H[a+12>>2]=0;H[a>>2]=0;H[a+4>>2]=0}function Uc(a,b,c){a=a|0;b=b|0;c=c|0;return!H[a+8>>2]&(H[a+216>>2]!=0&H[a+220>>2]!=0)}function Sa(a){if(H[a+12>>2]){H[a+40>>2]=0;while(1){if(H[a+24>>2]>0){continue}break}}}function Nc(a,b){F[b+3|0]=I[a|0];F[b+2|0]=I[a+1|0];F[b+1|0]=I[a+2|0];F[b|0]=I[a+3|0]}function hb(a){if(a){ra[H[(H[a+76>>2]?20:16)+a>>2]](H[a+48>>2]);H[a+48>>2]=0;Ca(a)}}function Nd(a,b){a=a|0;b=b|0;Pb(H[a>>2],b);F[a+124|0]=0;H[a+128>>2]=H[b+8248>>2]&1}function Ea(a,b){if(!a|!b){a=0}else{b=N(a,b);a=gb(8,b);if(a){Dc(a,0,b)}}return a}function Ga(a,b,c){var d=0;d=na-16|0;na=d;H[d+12>>2]=c;tc(a,b,c,0,0);na=d+16|0}function xe(a){var b=0;while(1){if(a){a=a-1&a;b=b+1|0;continue}break}return b}function _a(a){var b=0;if(a){b=H[a+12>>2];if(b){Ca(b);H[a+12>>2]=0}Ca(a)}}function Rc(a,b,c,d,e,f,g){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;g=g|0;D()}function Gd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return Lb(H[a>>2],b,c,d)|0}function Hd(a,b,c,d,e,f){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;f=f|0;D()}function wc(a,b){a=uc(a-1|0);if(!a){a=uc(b);a=a?a|32:0}return a}function Qb(a){return H[a+12>>2]==H[a+4>>2]|H[a+8>>2]==H[a>>2]}function Dd(a,b,c){a=a|0;b=b|0;c=c|0;return Mc(H[a>>2],b,c)|0}function uc(a){var b=0,c=0,d=0;return b=te(a),c=0,d=a,d?b:c}function pb(a){var b=0;if(a){b=H[a+8>>2];if(b){Ca(b)}Ca(a)}}function gd(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;qa=0;return 0}function Za(a,b,c,d,e,f,g,h){return ac(a,b,c,d,e,f,g,h,0)}function Oc(a,b,c,d,e){a=a|0;b=b|0;c=c|0;d=d|0;e=e|0;D()}function nc(a,b,c,d){return ra[H[a+44>>2]](a,b,c,d)|0}function Xa(a,b,c){H[((b<<2)+a|0)+28>>2]=(c<<5)+20832}function Fb(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return 1}function Fd(a,b,c){a=a|0;b=b|0;c=c|0;Ib(H[a>>2],b,c)}function te(a){if(a){return 31-Q(a-1^a)|0}return 32}function rb(a,b,c){return ra[H[a+40>>2]](a,b,0,c)|0}function _d(a,b,c){a=a|0;b=b|0;c=c|0;qa=-1;return-1}function Oa(a,b,c,d,e,f,g,h){ac(a,b,c,d,e,f,g,h,1)}function yc(a){if(!a){return 0}H[6597]=a;return-1}function Wd(a,b,c){a=a|0;b=b|0;c=c|0;mc(a,b,c,2)}function Vd(a,b,c){a=a|0;b=b|0;c=c|0;mc(a,b,c,4)}function Sd(a,b,c){a=a|0;b=b|0;c=c|0;lc(a,b,c,2)}function Rd(a,b,c){a=a|0;b=b|0;c=c|0;lc(a,b,c,4)}function we(a,b,c){se(a,0,b,c);qa=pa;return oa}function La(a,b,c){if(!(I[a|0]&32)){Ac(b,c,a)}}function ie(a,b,c){a=a|0;b=b|0;c=c|0;return 0}function Nb(a,b,c){a=a|0;b=b|0;c=c|0;return 1}function Jc(a,b,c){a=a|0;b=b|0;c=c|0;return-1}function ve(a,b,c,d){a=se(a,b,c,d);return a}function Fa(a){if(!a){return 0}return vb(a)}function Md(a,b){a=a|0;b=b|0;Ob(H[a>>2],b)}function Dc(a,b,c){if(c){y(a,b<<24>>24,c)}}function ic(a){return H[a+8>>2]-H[a>>2]|0}function hd(a){a=a|0;ia();ha(a+128|0);D()}function vb(a){a=a|0;return gb(8,a)|0}function md(a,b){a=a|0;b=b|0;_(a|0)}function ld(a,b){a=a|0;b=b|0;Y(a|0)}function Ab(a){return H[a+28>>2]!=2}function ab(a,b,c){if(c){B(a,b,c)}}function Vc(a,b){a=a|0;b=b|0;D()}function ob(a){return kc(a,32)}function Ia(a){return kc(a,16)}function id(a){a=a|0;ja();D()}function Eb(){return Ea(1,36)}function Sb(a,b){a=a|0;b=b|0}function db(a){if(a){Ca(a)}}function Qc(a){a=a|0;D()}function Pa(a){H[a>>2]=0} +// EMSCRIPTEN_END_FUNCS +a=I;m(n);var ra=[null,Sb,ie,_d,Jc,Jc,bb,Ed,ud,od,bd,ad,$c,_c,Zc,Yc,Xc,Wc,Nb,Uc,Tc,Sc,Pc,Ic,pe,oe,ne,me,le,ke,je,he,ge,fe,ee,de,ce,be,ae,Fb,$d,Zd,Fb,Fb,Yd,Xd,Wd,Vd,Ud,Td,Sd,Rd,Qd,Pd,Kd,Cd,Bd,Ad,zd,yd,xd,wd,vd,td,sd,rd,qd,pd,Qc,Qc,Ib,Nb,Gb,Ob,Pb,wb,Mb,Vc,Lb,Mc,Oc,Rc,ib,cb,Qc,Qc,Fd,Ld,Od,Vc,Gd,Dd,Oc,Rc,Md,Nd,Fc,Hd,Id,Jd,Sb,md,ld,fd,ed,dd,hd,id,Qc,jd,kd,Qc,gd];function sa(){return E.byteLength>>16}function xa(ya){ya=ya|0;var ta=sa()|0;var ua=ta+ya|0;if(ta{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=instantiateSync(wasmBinaryFile,info);return receiveInstance(result[0])}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};function _copy_pixels_1(compG_ptr,nb_pixels){compG_ptr>>=2;const imageData=Module.imageData=new Uint8ClampedArray(nb_pixels);const compG=HEAP32.subarray(compG_ptr,compG_ptr+nb_pixels);imageData.set(compG)}function _copy_pixels_3(compR_ptr,compG_ptr,compB_ptr,nb_pixels){compR_ptr>>=2;compG_ptr>>=2;compB_ptr>>=2;const imageData=Module.imageData=new Uint8ClampedArray(nb_pixels*3);const compR=HEAP32.subarray(compR_ptr,compR_ptr+nb_pixels);const compG=HEAP32.subarray(compG_ptr,compG_ptr+nb_pixels);const compB=HEAP32.subarray(compB_ptr,compB_ptr+nb_pixels);for(let i=0;i>=2;compG_ptr>>=2;compB_ptr>>=2;compA_ptr>>=2;const imageData=Module.imageData=new Uint8ClampedArray(nb_pixels*4);const compR=HEAP32.subarray(compR_ptr,compR_ptr+nb_pixels);const compG=HEAP32.subarray(compG_ptr,compG_ptr+nb_pixels);const compB=HEAP32.subarray(compB_ptr,compB_ptr+nb_pixels);const compA=HEAP32.subarray(compA_ptr,compA_ptr+nb_pixels);for(let i=0;i2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);return 70}var printCharBuffers=[null,[],[]];var UTF8Decoder=new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);return UTF8Decoder.decode(heapOrArray.buffer?heapOrArray.subarray(idx,endPtr):new Uint8Array(heapOrArray.slice(idx,endPtr)))};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>{if(!ptr)return"";var end=findStringEnd(HEAPU8,ptr,maxBytesToRead,ignoreNul);return UTF8Decoder.decode(HEAPU8.subarray(ptr,end))};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};function _gray_to_rgba(compG_ptr,nb_pixels){compG_ptr>>=2;const imageData=Module.imageData=new Uint8ClampedArray(nb_pixels*4);const compG=HEAP32.subarray(compG_ptr,compG_ptr+nb_pixels);for(let i=0;i>=2;compA_ptr>>=2;const imageData=Module.imageData=new Uint8ClampedArray(nb_pixels*4);const compG=HEAP32.subarray(compG_ptr,compG_ptr+nb_pixels);const compA=HEAP32.subarray(compA_ptr,compA_ptr+nb_pixels);for(let i=0;i>=2;compG_ptr>>=2;compB_ptr>>=2;const imageData=Module.imageData=new Uint8ClampedArray(nb_pixels*4);const compR=HEAP32.subarray(compR_ptr,compR_ptr+nb_pixels);const compG=HEAP32.subarray(compG_ptr,compG_ptr+nb_pixels);const compB=HEAP32.subarray(compB_ptr,compB_ptr+nb_pixels);for(let i=0;i{HEAP8.set(array,buffer)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["writeArrayToMemory"]=writeArrayToMemory;var _malloc,_free,_jp2_decode,__emscripten_timeout,dynCall_iji,dynCall_jji,dynCall_iiji,dynCall_jiji,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_malloc=Module["_malloc"]=wasmExports["t"];_free=Module["_free"]=wasmExports["u"];_jp2_decode=Module["_jp2_decode"]=wasmExports["v"];__emscripten_timeout=wasmExports["w"];dynCall_iji=wasmExports["dynCall_iji"];dynCall_jji=wasmExports["dynCall_jji"];dynCall_iiji=wasmExports["dynCall_iiji"];dynCall_jiji=wasmExports["dynCall_jiji"];memory=wasmMemory=wasmExports["r"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={n:__abort_js,m:__emscripten_runtime_keepalive_clear,j:__setitimer_js,f:_copy_pixels_1,e:_copy_pixels_3,d:_copy_pixels_4,k:_emscripten_resize_heap,o:_environ_get,p:_environ_sizes_get,i:_fd_seek,b:_fd_write,q:_gray_to_rgba,h:_graya_to_rgba,c:_jsPrintWarning,l:_proc_exit,g:_rgb_to_rgba,a:_storeErrorMessage};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=createWasm();run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +;return moduleRtn}export default OpenJPEG; diff --git a/web/public/vendor/pdfjs/wasm/qcms_bg.wasm b/web/public/vendor/pdfjs/wasm/qcms_bg.wasm new file mode 100644 index 00000000..fccd641f Binary files /dev/null and b/web/public/vendor/pdfjs/wasm/qcms_bg.wasm differ diff --git a/web/public/vendor/pdfjs/wasm/quickjs-eval.js b/web/public/vendor/pdfjs/wasm/quickjs-eval.js new file mode 100644 index 00000000..ab9ae8b5 --- /dev/null +++ b/web/public/vendor/pdfjs/wasm/quickjs-eval.js @@ -0,0 +1,16 @@ +/* THIS FILE IS GENERATED - DO NOT EDIT */ +async function QuickJS(moduleArg={}){var moduleRtn;var e=moduleArg,aa=import.meta.url,h="",m;try{h=(new URL(".",aa)).href}catch{}m=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);};var q=console.error.bind(console),r,t=!1,u,v,w,x=!1;function y(){var a=z.buffer;A=new Int8Array(a);new Int16Array(a);B=new Uint8Array(a);new Uint16Array(a);C=new Int32Array(a);D=new Uint32Array(a);new Float32Array(a);new Float64Array(a);new BigInt64Array(a);new BigUint64Array(a)} +function E(a){e.onAbort?.(a);a=`Aborted(${a})`;q(a);t=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");w?.(a);throw a;}var F;async function ba(a){if(!r)try{var b=await m(a);return new Uint8Array(b)}catch{}if(a==F&&r)a=new Uint8Array(r);else throw"both async and sync fetching of the wasm failed";return a}async function ca(a,b){try{var c=await ba(a);return await WebAssembly.instantiate(c,b)}catch(d){q(`failed to asynchronously prepare wasm: ${d}`),E(d)}} +async function da(a){var b=F;if(!r)try{var c=fetch(b,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(c,a)}catch(d){q(`wasm streaming compile failed: ${d}`),q("falling back to ArrayBuffer instantiation")}return ca(b,a)}class G{name="ExitStatus";constructor(a){this.message=`Program terminated with exit(${a})`;this.status=a}} +var C,A,D,B,H=a=>{for(;0{var a=e.preRun.shift();J.push(a)},K=!0,L=0,fa=[0,31,60,91,121,152,182,213,244,274,305,335],ha=[0,31,59,90,120,151,181,212,243,273,304,334],M={},N=a=>{if(!(a instanceof G||"unwind"==a))throw a;},O=a=>{u=a;K||0{if(!t)try{a()}catch(b){N(b)}finally{if(!(K||0{var d=B;if(0=f){if(b>= +c)break;d[b++]=f}else if(2047>=f){if(b+1>=c)break;d[b++]=192|f>>6;d[b++]=128|f&63}else if(65535>=f){if(b+2>=c)break;d[b++]=224|f>>12;d[b++]=128|f>>6&63;d[b++]=128|f&63}else{if(b+3>=c)break;d[b++]=240|f>>18;d[b++]=128|f>>12&63;d[b++]=128|f>>6&63;d[b++]=128|f&63;g++}}d[b]=0}},ja=new TextDecoder,Q=a=>{if(a){for(var b=a,c=B,d=b+void 0;c[b]&&!(b>=d);)++b;a=ja.decode(B.subarray(a,b))}else a="";return a},R=a=>{for(var b=0,c=0;c=d?b++:2047>=d?b+=2:55296<=d&&57343>= +d?(b+=4,++c):b+=3}return b},T=a=>{var b=R(a)+1,c=S(b);c&&P(a,c,b);return c};function U(){}var la=(a,b,c,d)=>{var g={string:k=>{var n=0;if(null!==k&&void 0!==k&&0!==k){n=R(k)+1;var X=V(n);P(k,X,n);n=X}return n},array:k=>{var n=V(k.length);A.set(k,n);return n}};a=e["_"+a];var f=[],p=0;if(d)for(var l=0;l{a=Q(a);b=null!==b?JSON.parse(Q(b)):[];try{const d=e.externalCall(a,b);return d?T(d):null}catch(d){return e.HEAPU8[c]=1,T(d.message)}};e.noExitRuntime&&(K=e.noExitRuntime);e.printErr&&(q=e.printErr);e.wasmBinary&&(r=e.wasmBinary);if(e.preInit)for("function"==typeof e.preInit&&(e.preInit=[e.preInit]);0{var g=!c||c.every(f=>"number"===f||"boolean"===f);return"string"!==b&&g&&!d?e["_"+a]:(...f)=>la(a,b,c,f,d)}; +e.stringToNewUTF8=T; +var S,ma,ka,V,W,z,na={e:()=>E(""),a:()=>{K=!1;L=0},b:function(a,b){a=-9007199254740992>a||9007199254740992>2]=a.getSeconds();C[b+4>>2]=a.getMinutes();C[b+8>>2]=a.getHours();C[b+12>>2]=a.getDate();C[b+16>>2]=a.getMonth();C[b+20>>2]=a.getFullYear()-1900;C[b+24>>2]=a.getDay();var c=a.getFullYear();C[b+28>>2]=(0!==c%4||0===c%100&&0!==c%400?ha:fa)[a.getMonth()]+a.getDate()-1|0;C[b+36>>2]=-(60*a.getTimezoneOffset());c=(new Date(a.getFullYear(),6,1)).getTimezoneOffset(); +var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();C[b+32>>2]=(c!=d&&a.getTimezoneOffset()==Math.min(d,c))|0},i:(a,b)=>{M[a]&&(clearTimeout(M[a].id),delete M[a]);if(!b)return 0;var c=setTimeout(()=>{delete M[a];ia(()=>ma(a,performance.now()))},b);M[a]={id:c,A:b};return 0},c:(a,b,c,d)=>{var g=(new Date).getFullYear(),f=(new Date(g,0,1)).getTimezoneOffset();g=(new Date(g,6,1)).getTimezoneOffset();D[a>>2]=60*Math.max(f,g);C[b>>2]=Number(f!=g);b=p=>{var l=Math.abs(p);return`UTC${0<=p?"-":"+"}${String(Math.floor(l/ +60)).padStart(2,"0")}${String(l%60).padStart(2,"0")}`};a=b(f);b=b(g);gDate.now(),j:a=>{var b=B.length;a>>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);a:{d=(Math.min(2147483648,65536*Math.ceil(Math.max(a,d)/65536))-z.buffer.byteLength+65535)/65536|0;try{z.grow(d);y();var g= +1;break a}catch(f){}g=void 0}if(g)return!0}return!1},m:function(a){a=Q(a);window.console.log(a)},h:function(a){a=Q(a);return Date.parse(a)},l:function(a,b,c,d){a=Q(a);b=Q(b);c=Q(c);c=`Quickjs -- ${a}: ${b}\n${c}`;0!==d?window.alert(c):window.console.error(c)},k:O},Z; +Z=await (async function(){function a(c){c=Z=c.exports;e._evalInSandbox=c.p;e._nukeSandbox=c.q;e._init=c.r;e._commFun=c.s;e._dumpMemoryUse=c.t;S=c.u;e._free=c.v;ma=c.w;ka=c.x;V=c.y;W=c.z;z=c.n;y();return Z}var b={a:na};if(e.instantiateWasm)return new Promise(c=>{e.instantiateWasm(b,(d,g)=>{c(a(d,g))})});F??=e.locateFile?e.locateFile?e.locateFile("quickjs-eval.wasm",h):h+"quickjs-eval.wasm":(new URL("quickjs-eval.wasm",import.meta.url)).href;return a((await da(b)).instance)}()); +(function(){function a(){e.calledRun=!0;if(!t){x=!0;Z.o();v?.(e);e.onRuntimeInitialized?.();if(e.postRun)for("function"==typeof e.postRun&&(e.postRun=[e.postRun]);e.postRun.length;){var b=e.postRun.shift();I.push(b)}H(I)}}if(e.preRun)for("function"==typeof e.preRun&&(e.preRun=[e.preRun]);e.preRun.length;)ea();H(J);e.setStatus?(e.setStatus("Running..."),setTimeout(()=>{setTimeout(()=>e.setStatus(""),1);a()},1)):a()})();x?moduleRtn=e:moduleRtn=new Promise((a,b)=>{v=a;w=b}); +;return moduleRtn}export default QuickJS; diff --git a/web/public/vendor/pdfjs/wasm/quickjs-eval.wasm b/web/public/vendor/pdfjs/wasm/quickjs-eval.wasm new file mode 100644 index 00000000..b5fe551a Binary files /dev/null and b/web/public/vendor/pdfjs/wasm/quickjs-eval.wasm differ diff --git a/web/src/App.tsx b/web/src/App.tsx index 74386e48..68995c29 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -31,6 +31,7 @@ import Profiles from "@/pages/Profiles"; import Catalog from "@/pages/Catalog"; import LibraryPage from "@/pages/LibraryPage"; import ItemDetail from "@/pages/ItemDetail/index"; +import EbookReader from "@/pages/EbookReader"; import PersonDetail from "@/pages/PersonDetail"; import Collections from "@/pages/Collections"; import CollectionEditor from "@/pages/CollectionEditor"; @@ -92,6 +93,7 @@ import { WatchPlaybackHost, WatchPlaybackProvider, } from "@/playback/WatchPlaybackChrome"; +import { AudiobookPlaybackProvider } from "@/pages/audiobooks/player/audiobookPlaybackContext"; import type { ReactNode } from "react"; import { buildLegacyBrowseCatalogHref, @@ -358,6 +360,14 @@ function AppRoutes() { } /> + + + + } + /> {/* Admin area — own layout, no profile required */} - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/web/src/api/client.test.ts b/web/src/api/client.test.ts index a663d158..d3f4a554 100644 --- a/web/src/api/client.test.ts +++ b/web/src/api/client.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { api, + apiBlob, bootstrapAccessToken, getAccessToken, getPersonCatalogItems, @@ -120,6 +121,51 @@ describe("client helper inventory", () => { }); }); +describe("apiBlob", () => { + beforeEach(() => { + Object.defineProperty(globalThis, "sessionStorage", { + value: { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + clear: () => {}, + }, + configurable: true, + }); + }); + + it("rejects responses whose Content-Length exceeds the in-memory cap", async () => { + const fetchMock = vi.fn(async () => { + const res = new Response("x", { status: 200 }); + // 3 GiB; Response normally derives Content-Length from the body, so + // override the header lookup instead of materializing a huge body. + vi.spyOn(res.headers, "get").mockImplementation((name) => + name.toLowerCase() === "content-length" ? String(3 * 1024 * 1024 * 1024) : null, + ); + return res; + }); + vi.stubGlobal("fetch", fetchMock); + + await expect(apiBlob("/ebooks/abc/files/1/read")).rejects.toMatchObject({ + name: "ApiClientError", + code: "response_too_large", + message: expect.stringContaining("too large to open in the browser"), + }); + }); + + it("returns the blob when Content-Length is within the cap or missing", async () => { + const fetchMock = vi.fn(async () => { + const res = new Response("epub-bytes", { status: 200 }); + vi.spyOn(res.headers, "get").mockReturnValue(null); + return res; + }); + vi.stubGlobal("fetch", fetchMock); + + const blob = await apiBlob("/ebooks/abc/files/1/read"); + await expect(blob.text()).resolves.toBe("epub-bytes"); + }); +}); + describe("api", () => { it("forwards AbortSignal from options to fetch", async () => { Object.defineProperty(globalThis, "sessionStorage", { diff --git a/web/src/api/client.ts b/web/src/api/client.ts index bed9d604..a19ff289 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -202,6 +202,14 @@ async function refreshAccessToken( } export class ApiClientError extends Error { + /** + * Raw parsed JSON body of the error response, when the body parsed as JSON. + * Carries fields the normalized `details: ApiError` does not surface (e.g. + * plugin validation `field_errors` / `form_error` on a 400). Undefined for + * non-JSON or empty bodies. + */ + public body?: unknown; + constructor( public status: number, public code: string, @@ -257,14 +265,32 @@ function hasHeader(headers: Record, name: string): boolean { return Object.keys(headers).some((key) => key.toLowerCase() === target); } -async function parseApiError(res: Response): Promise { +interface ParsedApiError { + /** Normalized error with guaranteed `error`/`message` fields. */ + apiErr: ApiError; + /** Raw parsed JSON body, or undefined when the body wasn't JSON/empty. */ + raw?: unknown; +} + +async function parseApiError(res: Response): Promise { let apiErr: Partial = {}; + let raw: unknown; try { - apiErr = await res.json(); + raw = await res.json(); + if (raw && typeof raw === "object") { + apiErr = raw as Partial; + } } catch { // response wasn't JSON } - return normalizeApiError(apiErr, res); + return { apiErr: normalizeApiError(apiErr, res), raw }; +} + +/** Builds an ApiClientError from a parsed error response, attaching the raw body. */ +function apiClientErrorFrom(status: number, parsed: ParsedApiError): ApiClientError { + const err = new ApiClientError(status, parsed.apiErr.error, parsed.apiErr.message, parsed.apiErr); + err.body = parsed.raw; + return err; } export interface RestoredUserSession { @@ -304,8 +330,7 @@ export async function restoreUserSession({ } if (!res.ok) { - const apiErr = await parseApiError(res); - throw new ApiClientError(res.status, apiErr.error, apiErr.message, apiErr); + throw apiClientErrorFrom(res.status, await parseApiError(res)); } return { @@ -335,12 +360,12 @@ export async function api(path: string, options: RequestInit = {}): Promise { return headers; } +/** + * Fire-and-forget API request that survives page unload (pagehide / tab close). + * Sends the same auth, profile, and device headers as `api`, plus `keepalive` + * so the browser finishes the request after the document is gone. The response + * is intentionally ignored: no token refresh or error handling is possible + * while the page is unloading. + */ +export function apiKeepalive(path: string, options: RequestInit = {}): void { + const headers = buildApiHeaders(options); + void fetch(`/api/v1${path}`, { ...options, headers, keepalive: true }).catch(() => { + // Best-effort write during unload; nothing left to recover into. + }); +} + /** Downloads a binary API response and triggers a browser file save. */ export async function apiDownload( path: string, @@ -400,8 +439,7 @@ export async function apiDownload( } if (!res.ok) { - const apiErr = await parseApiError(res); - throw new ApiClientError(res.status, apiErr.error, apiErr.message, apiErr); + throw apiClientErrorFrom(res.status, await parseApiError(res)); } const blob = await res.blob(); @@ -413,6 +451,51 @@ export async function apiDownload( URL.revokeObjectURL(url); } +/** + * apiBlob buffers the entire response body in memory, so cap what it will + * accept; beyond this a download is the right tool, not an in-tab blob. + */ +export const API_BLOB_MAX_BYTES = 512 * 1024 * 1024; + +export async function apiBlob(path: string, options: RequestInit = {}): Promise { + let headers = buildApiHeaders(options); + let res = await fetch(`/api/v1${path}`, { ...options, headers }); + + if (res.status === 401 && getRefreshToken()) { + if (!refreshPromise) { + refreshPromise = attemptRefresh().finally(() => { + refreshPromise = null; + }); + } + const refreshed = await refreshPromise; + if (refreshed) { + headers = buildApiHeaders(options); + headers["Authorization"] = `Bearer ${accessToken}`; + res = await fetch(`/api/v1${path}`, { ...options, headers }); + } + } + + if (!res.ok) { + throw apiClientErrorFrom(res.status, await parseApiError(res)); + } + + // Reject oversized bodies up front instead of crashing the tab while + // buffering them. When the header is absent, proceed; streaming byte counts + // are not worth the complexity here. + const contentLength = Number(res.headers.get("Content-Length")); + if (Number.isFinite(contentLength) && contentLength > API_BLOB_MAX_BYTES) { + const sizeMiB = Math.round(contentLength / (1024 * 1024)); + const limitMiB = Math.round(API_BLOB_MAX_BYTES / (1024 * 1024)); + throw new ApiClientError( + res.status, + "response_too_large", + `This file is too large to open in the browser (${sizeMiB} MiB, limit ${limitMiB} MiB). Download it instead.`, + ); + } + + return res.blob(); +} + // People API export async function searchPeople(query: string, limit = 20): Promise { const params = new URLSearchParams({ q: query, limit: String(limit) }); diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 738c843e..ae62dc73 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -1,4 +1,5 @@ import { getDefaultQuerySortOrder, normalizeQuerySortField } from "@/lib/querySortOptions"; +import type { SchemaOption } from "@/components/admin/plugins/schemaFormUtils"; // Auth export interface LoginRequest { @@ -568,6 +569,16 @@ export interface AudiobookDetailExtension { }; } +export interface EbookDetailExtension { + authors: AudiobookPerson[]; + publisher?: string; + series?: AudiobookSeriesGroup; + related: { + also_by_author: AudiobookRelatedItem[]; + similar: AudiobookRelatedItem[]; + }; +} + // Seasons / Watched State export interface LeafItemUserData { played: boolean; @@ -644,11 +655,14 @@ export interface BrowseItemSortMetrics { progress_ratio?: number; viewed_at?: string; play_count?: number; + author?: string; + narrator?: string; + series_name?: string; } export interface BrowseItem { content_id: string; - type: "movie" | "series" | "season" | "episode" | "audiobook"; + type: "movie" | "series" | "season" | "episode" | "audiobook" | "ebook"; title: string; series_title?: string; season_number?: number | null; @@ -722,6 +736,23 @@ export interface CatalogFiltersResponse extends ItemFiltersResponse { series?: string[]; } +// Grouped audiobook browse (Library tab Authors / Narrators / Series axes). +// `name` round-trips into the matching catalog filter field (author / +// narrator / series), which match case-insensitively. +export interface AudiobookGroup { + name: string; + item_count: number; + total_duration_seconds: number; + in_progress_count: number; + finished_count: number; + poster_urls: string[]; +} + +export interface AudiobookGroupsResponse { + total: number; + groups: AudiobookGroup[]; +} + // Item Detail export interface FileVersion { file_id: number; @@ -917,7 +948,7 @@ export type SetMarkersRequest = Partial; + // Two-tier plugin-driven connection model. Generic fields above are owned by + // the host; the arr-specific config now lives in plugin_config. Optional for + // backward-compatible reads of legacy rows that predate the refactor. + capability_id?: string; + installation_id?: number; + supported_media_types?: string[]; + plugin_config?: Record; last_check_at?: string | null; last_check_status?: string; last_check_error?: string; updated_at?: string; } -export interface RequestIntegrationRootFolder { - path: string; - free_space?: number; - total_space?: number; - accessible: boolean; -} +export type RequestIntegrationOptions = Record; -export interface RequestIntegrationQualityProfile { - id: number; - name: string; -} - -export interface RequestIntegrationTag { - id: number; - label: string; -} - -export interface RequestIntegrationOptions { - kind: string; - root_folders: RequestIntegrationRootFolder[]; - quality_profiles: RequestIntegrationQualityProfile[]; - tags: RequestIntegrationTag[]; +export interface RequestIntegrationValidationError { + error: "validation_failed"; + field_errors?: Record; + form_error?: string; } export interface LoadRequestIntegrationOptionsRequest { - kind: "radarr" | "sonarr"; + // Vestigial: the backend resolves the plugin via installation_id + + // plugin_config and ignores this. Optional and no longer sent by the client. + kind?: "radarr" | "sonarr"; base_url: string; api_key_ref?: string; + // For unsaved connections the host cannot backfill from a stored row, so the + // body must carry enough to resolve the plugin (installation + capability + + // arr-specific config) when testing the connection. + capability_id?: string; + installation_id?: number; + plugin_config?: Record; } export interface RequestIntegrationsResponse { @@ -2684,6 +2703,7 @@ export interface AdminSettingsConnectionCheckRequest { export interface PluginAdminForm { fields: PluginAdminFormField[]; submit_label?: string; + sections?: PluginAdminFormSection[]; } export interface PluginAdminFormFieldOption { @@ -2692,11 +2712,36 @@ export interface PluginAdminFormFieldOption { description?: string; } +export interface PluginAdminFormCondition { + field: string; + equals: string[]; +} + +export interface PluginAdminFormValidation { + has_min?: boolean; + min?: number; + has_max?: boolean; + max?: number; + pattern?: string; + min_length?: number; + max_length?: number; +} + +export interface PluginAdminFormSection { + key: string; + title: string; + description?: string; + collapsible: boolean; + collapsed_default: boolean; + field_keys: string[]; + show_when?: PluginAdminFormCondition[]; +} + export interface PluginAdminFormField { key: string; label: string; description?: string; - control: "TEXT" | "TEXTAREA" | "PASSWORD" | "NUMBER" | "SWITCH" | "SELECT"; + control: "TEXT" | "TEXTAREA" | "PASSWORD" | "NUMBER" | "SWITCH" | "SELECT" | "MULTI_SELECT"; placeholder?: string; required: boolean; secret: boolean; @@ -2704,6 +2749,10 @@ export interface PluginAdminFormField { default_value?: unknown; options?: PluginAdminFormFieldOption[]; rows?: number; + dynamic_options?: boolean; + show_when?: PluginAdminFormCondition[]; + validation?: PluginAdminFormValidation; + exclusive_group_field?: string; } export interface PluginCapability { @@ -2948,7 +2997,7 @@ export interface SectionItemUpcomingEvent { export interface SectionItem { content_id: string; - type: "movie" | "series" | "season" | "episode" | "audiobook"; + type: "movie" | "series" | "season" | "episode" | "audiobook" | "ebook"; title: string; series_id?: string; series_title?: string; @@ -3108,7 +3157,9 @@ export function normalizeQueryDefinition(value?: QueryDefinitionInput | null): Q value?.media_scope === "movie" || value?.media_scope === "series" || value?.media_scope === "episode" || - value?.media_scope === "audiobook" + value?.media_scope === "audiobook" || + value?.media_scope === "ebook" || + value?.media_scope === "video" ? value.media_scope : undefined, match: value?.match === "any" ? "any" : "all", @@ -3170,7 +3221,9 @@ export function queryDefinitionFromSectionConfig( ? "episode" : config.media_scope === "audiobook" || config.filter_type === "audiobook" ? "audiobook" - : undefined; + : config.media_scope === "ebook" || config.filter_type === "ebook" + ? "ebook" + : undefined; const legacySortField = typeof config.sort === "string" ? config.sort : undefined; const legacySortOrder = typeof config.order === "string" ? config.order : undefined; @@ -3329,6 +3382,15 @@ export interface RateLimitConfig { ip_requests_per_minute: number; ip_burst: number; auth_endpoints: Record; + /** Whether a limiter is running in this process (GET responses only). */ + active?: boolean; + /** Backend the running limiter uses; may differ from `backend` until restart. */ + active_backend?: string; +} + +export interface RateLimitUpdateResponse { + status: string; + restart_required?: boolean; } // IP visibility diff --git a/web/src/components/AdminLayout.tsx b/web/src/components/AdminLayout.tsx index 5b8186ea..6fead98d 100644 --- a/web/src/components/AdminLayout.tsx +++ b/web/src/components/AdminLayout.tsx @@ -7,11 +7,14 @@ import { useDocumentTitle } from "@/hooks/useDocumentTitle"; import { resolveAdminDocumentTitle } from "@/lib/documentTitle"; import { Menu } from "lucide-react"; import { useWatchPlaybackController } from "@/playback/watchPlaybackContext"; +import { useAudiobookPlaybackController } from "@/pages/audiobooks/player/audiobookPlaybackContext"; export default function AdminLayout() { const [mobileOpen, setMobileOpen] = useState(false); const location = useLocation(); const { isBackgroundBarVisible } = useWatchPlaybackController(); + const audiobookPlayback = useAudiobookPlaybackController(); + const hasBackgroundBar = isBackgroundBarVisible || audiobookPlayback?.isBackgroundBarVisible; useDocumentTitle(resolveAdminDocumentTitle(location.pathname)); @@ -67,7 +70,7 @@ export default function AdminLayout() {

diff --git a/web/src/components/AdminSidebar.test.tsx b/web/src/components/AdminSidebar.test.tsx index 44adddb1..bf340c3d 100644 --- a/web/src/components/AdminSidebar.test.tsx +++ b/web/src/components/AdminSidebar.test.tsx @@ -54,32 +54,40 @@ function renderSidebar() { } describe("AdminSidebar", () => { - it("includes a Sections link in the manage navigation", () => { + it("renders the grouped navigation sections", () => { + const markup = renderSidebar(); + + for (const section of ["Overview", "Content", "Automation", "Users", "System"]) { + expect(markup).toContain(`>${section}<`); + } + }); + + it("includes a Sections link in the content navigation", () => { const markup = renderSidebar(); expect(markup).toContain('href="/admin/sections"'); expect(markup).toContain(">Sections<"); }); - it("includes a Maintenance link in the server navigation", () => { + it("includes a Maintenance link in the system navigation", () => { const markup = renderSidebar(); expect(markup).toContain('href="/admin/maintenance"'); expect(markup).toContain(">Maintenance<"); }); - it("includes a Recommendations link in the server navigation", () => { + it("includes a Recommendations link in the automation navigation", () => { const markup = renderSidebar(); expect(markup).toContain('href="/admin/recommendations"'); expect(markup).toContain(">Recommendations<"); }); - it("includes a Marker History link in the users navigation", () => { + it("includes a Markers link in the automation navigation", () => { const markup = renderSidebar(); expect(markup).toContain('href="/admin/marker-history"'); - expect(markup).toContain(">Marker History<"); + expect(markup).toContain(">Markers<"); }); it("renders the build identifier in the footer", () => { diff --git a/web/src/components/AdminSidebar.tsx b/web/src/components/AdminSidebar.tsx index 44f7db1e..ae753aaf 100644 --- a/web/src/components/AdminSidebar.tsx +++ b/web/src/components/AdminSidebar.tsx @@ -22,8 +22,11 @@ import { Puzzle, Send, RefreshCw, + SkipForward, } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; +import { SideNavItem, SideNavSection } from "@/components/SideNav"; import { SiloBrand } from "@/components/SiloBrand"; import { navigateToPluginRoute } from "@/lib/buildPluginHref"; import { useAdminPluginInstallations } from "@/hooks/queries/admin/plugins"; @@ -33,7 +36,7 @@ import { pluginRouteHref } from "@/lib/pluginRouteHref"; interface SidebarItem { label: string; - icon: ReactNode; + icon: LucideIcon; href: string; exact?: boolean; badge?: ReactNode; @@ -72,133 +75,60 @@ export default function AdminSidebar({ onNavigate }: AdminSidebarProps) { buildDisplay = buildInfo.data.display; } + // Grouped by admin intent: monitoring, curating the catalog, background + // processing the server runs on its own, people and their data, and the + // server installation itself. const sections: SidebarSection[] = [ { label: "Overview", items: [ - { - label: "Dashboard", - icon: , - href: "/admin", - exact: true, - }, + { label: "Dashboard", icon: LayoutDashboard, href: "/admin", exact: true }, { label: "Activity", - icon: , + icon: Radio, href: "/admin/activity", badge: sessionCount > 0 ? {sessionCount} live : undefined, }, - { - label: "Logs", - icon: , - href: "/admin/logs", - }, + { label: "Logs", icon: ScrollText, href: "/admin/logs" }, ], }, { label: "Content", items: [ - { - label: "Libraries", - icon: , - href: "/admin/libraries", - }, - { - label: "Collections", - icon: , - href: "/admin/collections", - }, - { - label: "Requests", - icon: , - href: "/admin/requests", - }, - { - label: "Autoscan", - icon: , - href: "/admin/autoscan", - }, - { - label: "Sections", - icon: , - href: "/admin/sections", - }, - { - label: "Subtitles", - icon: , - href: "/admin/subtitles", - }, + { label: "Libraries", icon: Library, href: "/admin/libraries" }, + { label: "Collections", icon: LayoutPanelTop, href: "/admin/collections" }, + { label: "Sections", icon: PanelsTopLeft, href: "/admin/sections" }, + { label: "Requests", icon: Send, href: "/admin/requests" }, + ], + }, + { + label: "Automation", + items: [ + { label: "Autoscan", icon: RefreshCw, href: "/admin/autoscan" }, + { label: "Scheduled Tasks", icon: CalendarClock, href: "/admin/tasks" }, + { label: "Subtitles", icon: Captions, href: "/admin/subtitles" }, + { label: "Markers", icon: SkipForward, href: "/admin/marker-history" }, + { label: "Recommendations", icon: Bot, href: "/admin/recommendations" }, ], }, { label: "Users", items: [ - { - label: "Users", - icon: , - href: "/admin/users", - }, - { - label: "Devices", - icon: , - href: "/admin/devices", - }, - { - label: "Playback History", - icon: , - href: "/admin/history", - }, - { - label: "Marker History", - icon: , - href: "/admin/marker-history", - }, - { - label: "History Import", - icon: , - href: "/admin/history-import", - }, + { label: "Users", icon: Users, href: "/admin/users" }, + { label: "Devices", icon: MonitorSmartphone, href: "/admin/devices" }, + { label: "Playback History", icon: History, href: "/admin/history" }, + { label: "History Import", icon: Download, href: "/admin/history-import" }, ], }, { - label: "Server", + label: "System", items: [ - { - label: "Scheduled Tasks", - icon: , - href: "/admin/tasks", - }, - { - label: "Nodes", - icon: , - href: "/admin/nodes", - }, - { - label: "Maintenance", - icon: , - href: "/admin/maintenance", - }, - { - label: "Plugins", - icon: , - href: "/admin/plugins", - }, - { - label: "Settings", - icon: , - href: "/admin/settings", - }, - { - label: "Recommendations", - icon: , - href: "/admin/recommendations", - }, - { - label: "API Keys", - icon: , - href: "/admin/api-keys", - }, + { label: "Settings", icon: SlidersHorizontal, href: "/admin/settings" }, + { label: "Plugins", icon: Blocks, href: "/admin/plugins" }, + { label: "Nodes", icon: Server, href: "/admin/nodes" }, + { label: "API Keys", icon: KeyRound, href: "/admin/api-keys" }, + { label: "Maintenance", icon: Wrench, href: "/admin/maintenance" }, ], }, ]; @@ -216,7 +146,7 @@ export default function AdminSidebar({ onNavigate }: AdminSidebarProps) { if (!route.navigable || route.navigation_kind !== "admin") continue; adminPluginItems.push({ label: route.navigation_label || inst.plugin_id, - icon: , + icon: Puzzle, href: pluginRouteHref(inst.id, route.path), external: true, }); @@ -250,70 +180,36 @@ export default function AdminSidebar({ onNavigate }: AdminSidebarProps) { className="sidebar-scroll flex-1 space-y-5 overflow-y-auto px-3" > {sections.map((section) => ( - + + {section.items.map((item) => + item.external ? ( + { + e.preventDefault(); + void navigateToPluginRoute(item.href); + onNavigate?.(); + }} + /> + ) : ( + + ), + )} + ))} diff --git a/web/src/components/ContinueWatchingCard.test.tsx b/web/src/components/ContinueWatchingCard.test.tsx index bd4a3f18..ea7bafd3 100644 --- a/web/src/components/ContinueWatchingCard.test.tsx +++ b/web/src/components/ContinueWatchingCard.test.tsx @@ -116,6 +116,44 @@ describe("ContinueWatchingCard", () => { expect(markup).toContain('src="/movie-poster.jpg"'); }); + it("links ebook continue rows to the reader and shows percent read", () => { + const queryClient = new QueryClient(); + const markup = renderToStaticMarkup( + + + + + , + ); + + expect(markup).toContain('href="/reader/ebook/ebook%20001?libraryId=12"'); + expect(markup).toContain('href="/item/ebook%20001?libraryId=12"'); + expect(markup).toContain("42% read"); + expect(markup).not.toContain('href="/watch/ebook'); + expect(markup).not.toContain("0 min left"); + }); + it("renders separate watch and item links alongside episodic metadata", () => { const queryClient = new QueryClient(); const markup = renderToStaticMarkup( @@ -176,10 +214,87 @@ describe("ContinueWatchingCard", () => { expect(markup).toContain('href="/watch/ep-001"'); expect(markup).toContain('href="/item/ep-001"'); + expect(markup).toContain('href="/item/series-1"'); + expect(markup).toContain('aria-label="Play Breaking Bad"'); expect(markup).toContain("Breaking Bad"); expect(markup).toContain("Season 1 Episode 1"); expect(markup).toContain("Pilot"); expect(markup).toContain("58 min left"); expect(markup).toContain("More actions"); }); + + it("links the poster to the item page and reserves playback for the play button", () => { + const queryClient = new QueryClient(); + const markup = renderToStaticMarkup( + + + + + , + ); + + // The poster link renders first and must navigate to the item page; the + // watch href is reserved for the explicit play button. + const posterLinkIndex = markup.indexOf('href="/item/movie-001"'); + const playLinkIndex = markup.indexOf('href="/watch/movie-001"'); + expect(posterLinkIndex).toBeGreaterThan(-1); + expect(playLinkIndex).toBeGreaterThan(-1); + expect(posterLinkIndex).toBeLessThan(playLinkIndex); + expect(markup).toContain('aria-label="Play Apex"'); + }); + + it("routes audiobook continue cards to the audiobook detail player", () => { + const queryClient = new QueryClient(); + const markup = renderToStaticMarkup( + + + + + , + ); + + expect(markup).toContain('href="/item/book-001?libraryId=7&play=1"'); + expect(markup).not.toContain('href="/watch/book-001'); + }); }); diff --git a/web/src/components/ContinueWatchingCard.tsx b/web/src/components/ContinueWatchingCard.tsx index e528d65d..97a90599 100644 --- a/web/src/components/ContinueWatchingCard.tsx +++ b/web/src/components/ContinueWatchingCard.tsx @@ -1,5 +1,5 @@ import ViewTransitionLink from "@/components/ViewTransitionLink"; -import { Play } from "lucide-react"; +import { BookOpen, Play } from "lucide-react"; import { useCallback } from "react"; import { useLocation } from "react-router"; import type { ItemDetail, SectionItem } from "@/api/types"; @@ -7,9 +7,11 @@ import type { ProgressEntry } from "@/api/types"; import MediaItemMenu from "@/components/MediaItemMenu"; import CardOverlays from "@/components/overlays/CardOverlays"; import { overlayDataFromSectionItem, type CardOverlayPrefs } from "@/lib/overlays"; +import { formatListeningTimeLeft } from "@/lib/audiobooks/duration"; import { upcomingBadgeClass, upcomingBadgeLabel } from "@/lib/upcomingEventPresentation"; import { useWatchPlaybackController } from "@/playback/watchPlaybackContext"; import { parseWatchHref } from "@/pages/watchRouteHelpers"; +import { buildItemHref, buildMediaPlayHref, isVideoWatchHref } from "@/lib/mediaNavigation"; type ContinueWatchingCardProps = ( | { @@ -31,13 +33,20 @@ type ContinueWatchingCardProps = ( export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { const location = useLocation(); const playbackController = useWatchPlaybackController(); - const libraryQuery = props.libraryId ? `?libraryId=${props.libraryId}` : ""; const card = "sectionItem" in props && props.sectionItem ? { - watchHref: `/watch/${props.sectionItem.content_id}${libraryQuery}`, - itemHref: `/item/${props.sectionItem.content_id}${libraryQuery}`, + watchHref: buildMediaPlayHref({ + contentId: props.sectionItem.content_id, + type: props.sectionItem.type, + libraryId: props.libraryId, + }), + itemHref: buildItemHref({ + contentId: props.sectionItem.content_id, + libraryId: props.libraryId, + }), title: props.sectionItem.title, + seriesId: props.sectionItem.series_id, seriesTitle: props.sectionItem.series_title, seasonNumber: props.sectionItem.season_number, episodeNumber: props.sectionItem.episode_number, @@ -48,9 +57,17 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { type: props.sectionItem.type, } : { - watchHref: `/watch/${props.detail.content_id}${libraryQuery}`, - itemHref: `/item/${props.detail.content_id}${libraryQuery}`, + watchHref: buildMediaPlayHref({ + contentId: props.detail.content_id, + type: props.detail.type, + libraryId: props.libraryId, + }), + itemHref: buildItemHref({ + contentId: props.detail.content_id, + libraryId: props.libraryId, + }), title: props.detail.title, + seriesId: props.detail.series_id, seriesTitle: props.detail.series_title, seasonNumber: props.detail.season_number, episodeNumber: props.detail.episode_number, @@ -69,6 +86,7 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { ? { itemId: props.sectionItem.content_id, surface: "next_up" as const, + mediaType: props.sectionItem.type, seriesId: props.sectionItem.series_id, } : undefined @@ -76,19 +94,28 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { ? { itemId: props.sectionItem.content_id, surface: "continue_watching" as const, + mediaType: props.sectionItem.type, progressUpdatedAt: props.sectionItem.progress_updated_at, } : undefined : { itemId: props.detail.content_id, surface: "continue_watching" as const, + mediaType: props.detail.type, progressUpdatedAt: props.progress.updated_at, }; const progressPercent = card.durationSeconds > 0 ? (card.positionSeconds / card.durationSeconds) * 100 : 0; const hasPartialProgress = progressPercent > 0 && progressPercent < 100; const hasEpisodeMeta = card.seasonNumber != null && card.episodeNumber != null; - const heading = hasEpisodeMeta && card.seriesTitle ? card.seriesTitle : card.title; + const headingIsSeries = hasEpisodeMeta && !!card.seriesTitle; + const heading = headingIsSeries ? card.seriesTitle : card.title; + // The heading shows the series title for episodes, so it should navigate to + // the series page; everything else heads to the item's own page. + const headingHref = + headingIsSeries && card.seriesId + ? buildItemHref({ contentId: card.seriesId, libraryId: props.libraryId }) + : card.itemHref; const episodeLabel = hasEpisodeMeta ? `Season ${card.seasonNumber} Episode ${card.episodeNumber}` : null; @@ -105,9 +132,13 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { ? premiereBadge ? null : "Next Episode" - : card.durationSeconds > 0 - ? `${Math.round((card.durationSeconds - card.positionSeconds) / 60)} min left` - : "\u00A0"; + : card.type === "ebook" + ? `${Math.round(Math.min(Math.max(progressPercent, 0), 100))}% read` + : card.durationSeconds > 0 + ? card.type === "audiobook" + ? formatListeningTimeLeft(card.positionSeconds, card.durationSeconds) + : `${Math.round((card.durationSeconds - card.positionSeconds) / 60)} min left` + : "\u00A0"; const handleWatchClick = useCallback( (event: React.MouseEvent) => { if ( @@ -121,6 +152,10 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { return; } + if (!isVideoWatchHref(card.watchHref)) { + return; + } + const parsed = parseWatchHref(card.watchHref); if (!parsed) { return; @@ -174,18 +209,14 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { return (
-
- +
+
{imageSrc ? ( {heading} ) : ( @@ -202,12 +233,8 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { /> )} - {/* Play overlay */} -
-
- -
-
+ {/* Hover dim behind the play button */} +
{/* Progress bar */} {!isNextUp && progressPercent > 0 && ( @@ -223,6 +250,18 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { )}
+ + {card.type === "ebook" ? ( + + ) : ( + + )} + {/* Info */} - -
{heading}
- {episodeMeta &&
{episodeMeta}
} +
+ + {heading} + + {episodeMeta && ( + + {episodeMeta} + + )} {premiereBadge && (
)} - {timeLeftLabel &&
{timeLeftLabel}
} - + {timeLeftLabel && + (timeLeftLabel === "\u00A0" ? ( +
{timeLeftLabel}
+ ) : ( + + {timeLeftLabel} + + ))} +
); } diff --git a/web/src/components/DownloadVersionPicker.test.tsx b/web/src/components/DownloadVersionPicker.test.tsx new file mode 100644 index 00000000..38c40eb7 --- /dev/null +++ b/web/src/components/DownloadVersionPicker.test.tsx @@ -0,0 +1,83 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { FileVersion } from "@/api/types"; + +import DownloadVersionPicker from "./DownloadVersionPicker"; + +vi.mock("@/hooks/queries/downloads", () => ({ + buildDirectDownloadUrl: (fileId: number) => `/api/downloads/files/${fileId}`, +})); + +function version(overrides: Partial): FileVersion { + return { + file_id: overrides.file_id ?? 1, + file_name: overrides.file_name, + file_path: overrides.file_path, + resolution: overrides.resolution ?? "", + codec_video: overrides.codec_video ?? "", + codec_audio: overrides.codec_audio ?? "", + hdr: overrides.hdr ?? false, + container: overrides.container ?? "", + file_size: overrides.file_size ?? 0, + duration: overrides.duration ?? 0, + bitrate: overrides.bitrate ?? 0, + }; +} + +describe("DownloadVersionPicker", () => { + it("uses file-size copy instead of quality copy for multi-file downloads", () => { + render( + undefined} + title="A Psalm for the Wild-Built" + versions={[ + version({ file_id: 1, container: "epub", file_size: 512 * 1024 }), + version({ file_id: 2, container: "pdf", file_size: 2 * 1024 * 1024 }), + ]} + />, + ); + + expect(screen.getByText("Larger files require more storage space.")).toBeTruthy(); + expect(screen.queryByText("Higher quality files require more storage space.")).toBeNull(); + }); + + it("describes download choices as files instead of versions", () => { + render( + undefined} + title="A Psalm for the Wild-Built" + versions={[version({ file_id: 1, container: "epub", file_size: 512 * 1024 })]} + />, + ); + + expect( + screen.getByText("Choose a file to download. Make sure you have enough disk space."), + ).toBeTruthy(); + expect( + screen.queryByText("Choose a version to download. Make sure you have enough disk space."), + ).toBeNull(); + }); + + it("allows ebook download rows to show page counts", () => { + render( + undefined} + title="A Psalm for the Wild-Built" + versions={[ + version({ + file_id: 1, + container: "cbz", + file_size: 25 * 1024 ** 2, + duration: 48, + }), + ]} + summaryBuilder={(file) => `${file.container.toUpperCase()} · 48 pages`} + />, + ); + + expect(screen.getByText("CBZ · 48 pages")).toBeTruthy(); + }); +}); diff --git a/web/src/components/DownloadVersionPicker.tsx b/web/src/components/DownloadVersionPicker.tsx index 4b2502d7..36cbe2aa 100644 --- a/web/src/components/DownloadVersionPicker.tsx +++ b/web/src/components/DownloadVersionPicker.tsx @@ -18,6 +18,7 @@ interface DownloadVersionPickerProps { onOpenChange: (open: boolean) => void; versions: FileVersion[]; title?: string; + summaryBuilder?: (version: FileVersion) => string; } export default function DownloadVersionPicker({ @@ -25,6 +26,7 @@ export default function DownloadVersionPicker({ onOpenChange, versions, title, + summaryBuilder, }: DownloadVersionPickerProps) { const sorted = sortByResolution(versions); const [downloading, setDownloading] = useState(null); @@ -60,14 +62,14 @@ export default function DownloadVersionPicker({ Download{title ? `: ${title}` : ""} - Choose a version to download. Make sure you have enough disk space. + Choose a file to download. Make sure you have enough disk space.
{sorted.map((version) => { - const quality = buildQualitySummary(version); - const size = formatFileSize(version.file_size); + const quality = summaryBuilder?.(version) || buildQualitySummary(version); + const size = summaryBuilder ? "" : formatFileSize(version.file_size); return ( + )} + + ); +} diff --git a/web/src/components/WatchTonightCard.test.tsx b/web/src/components/WatchTonightCard.test.tsx new file mode 100644 index 00000000..0ac7dfd7 --- /dev/null +++ b/web/src/components/WatchTonightCard.test.tsx @@ -0,0 +1,77 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { MemoryRouter } from "react-router"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import WatchTonightCard from "./WatchTonightCard"; + +vi.mock("@/playback/watchPlaybackContext", () => ({ + useWatchPlaybackController: () => ({ startPlayback: vi.fn() }), +})); + +describe("WatchTonightCard", () => { + it("links ebook cards to the reader and shows percent read", () => { + const markup = renderToStaticMarkup( + + {}} + /> + , + ); + + expect(markup).toContain('href="/item/ebook%201"'); + expect(markup).toContain('href="/reader/ebook/ebook%201"'); + expect(markup).toContain("57% read"); + expect(markup).not.toContain('href="/watch/ebook'); + expect(markup).not.toContain("0 min left"); + }); + + it("uses listen copy for audiobook play links", () => { + render( + + {}} + item={{ + content_id: "book-1", + type: "audiobook", + title: "Book One", + year: 2026, + genres: [], + status: "matched", + rating_imdb: null, + overview: "", + poster_url: "", + poster_thumbhash: "", + backdrop_url: "", + backdrop_thumbhash: "", + logo_url: "", + watch_tonight_source: "recommendation", + }} + /> + , + ); + + expect(screen.getByRole("link", { name: "Listen Book One" })).toHaveAttribute( + "href", + "/item/book-1?play=1", + ); + }); +}); diff --git a/web/src/components/WatchTonightCard.tsx b/web/src/components/WatchTonightCard.tsx index 3d68935a..e7d23639 100644 --- a/web/src/components/WatchTonightCard.tsx +++ b/web/src/components/WatchTonightCard.tsx @@ -1,4 +1,4 @@ -import { Play } from "lucide-react"; +import { BookOpen, Play } from "lucide-react"; import { useCallback } from "react"; import { useLocation } from "react-router"; import type { WatchTonightItem } from "@/hooks/queries/recommendations"; @@ -6,6 +6,7 @@ import ViewTransitionLink from "@/components/ViewTransitionLink"; import { Badge } from "@/components/ui/badge"; import { useWatchPlaybackController } from "@/playback/watchPlaybackContext"; import { parseWatchHref } from "@/pages/watchRouteHelpers"; +import { buildItemHref, buildMediaPlayHref, isVideoWatchHref } from "@/lib/mediaNavigation"; const sourceLabels: Record = { continue_watching: "Continue", @@ -22,8 +23,8 @@ export default function WatchTonightCard({ item, onPlay }: WatchTonightCardProps const location = useLocation(); const playbackController = useWatchPlaybackController(); - const watchHref = `/watch/${item.content_id}`; - const itemHref = `/item/${item.content_id}`; + const watchHref = buildMediaPlayHref({ contentId: item.content_id, type: item.type }); + const itemHref = buildItemHref({ contentId: item.content_id }); const source = item.watch_tonight_source ?? ""; const isInProgress = source === "continue_watching"; const isNextUp = source === "next_up"; @@ -40,9 +41,12 @@ export default function WatchTonightCard({ item, onPlay }: WatchTonightCardProps const subtitle = isNextUp ? "Next Episode" - : isInProgress && (item.duration_seconds ?? 0) > 0 - ? `${Math.max(0, Math.round(((item.duration_seconds ?? 0) - (item.position_seconds ?? 0)) / 60))} min left` - : item.genres?.slice(0, 2).join(", ") || "\u00A0"; + : isInProgress && item.type === "ebook" + ? `${Math.round(Math.min(Math.max(progressPercent, 0), 100))}% read` + : isInProgress && (item.duration_seconds ?? 0) > 0 + ? `${Math.max(0, Math.round(((item.duration_seconds ?? 0) - (item.position_seconds ?? 0)) / 60))} min left` + : item.genres?.slice(0, 2).join(", ") || "\u00A0"; + const playVerb = item.type === "ebook" ? "Read" : item.type === "audiobook" ? "Listen" : "Play"; const handlePlayClick = useCallback( (event: React.MouseEvent) => { @@ -57,6 +61,10 @@ export default function WatchTonightCard({ item, onPlay }: WatchTonightCardProps return; } + if (!isVideoWatchHref(watchHref)) { + return; + } + const parsed = parseWatchHref(watchHref); if (!parsed) return; @@ -81,7 +89,7 @@ export default function WatchTonightCard({ item, onPlay }: WatchTonightCardProps
@@ -108,7 +116,11 @@ export default function WatchTonightCard({ item, onPlay }: WatchTonightCardProps {/* Play overlay */}
- + {item.type === "ebook" ? ( + + ) : ( + + )}
diff --git a/web/src/components/WatchTonightDialog.tsx b/web/src/components/WatchTonightDialog.tsx index 68877a5f..e0722978 100644 --- a/web/src/components/WatchTonightDialog.tsx +++ b/web/src/components/WatchTonightDialog.tsx @@ -115,7 +115,7 @@ export default function WatchTonightDialog({ open, onOpenChange }: WatchTonightD

Pick Up Where I Left Off

- Continue watching or start next episodes + Continue in-progress titles or start what is next

diff --git a/web/src/components/admin/TaskStatusBadge.tsx b/web/src/components/admin/TaskStatusBadge.tsx index 4dfa3b90..93c4d830 100644 --- a/web/src/components/admin/TaskStatusBadge.tsx +++ b/web/src/components/admin/TaskStatusBadge.tsx @@ -1,11 +1,6 @@ import type { ExecutionResult } from "@/api/types"; import { Badge } from "@/components/ui/badge"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; interface TaskStatusBadgeProps { diff --git a/web/src/components/admin/libraries/LibraryEditorDialog.tsx b/web/src/components/admin/libraries/LibraryEditorDialog.tsx new file mode 100644 index 00000000..825b4038 --- /dev/null +++ b/web/src/components/admin/libraries/LibraryEditorDialog.tsx @@ -0,0 +1,226 @@ +import { useState } from "react"; +import type { FormEvent } from "react"; +import { Database, FolderOpen, Settings2, SlidersHorizontal } from "lucide-react"; + +import type { Library } from "@/api/types"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { cn } from "@/lib/utils"; + +import { AdvancedFields, FolderFields, GeneralFields, MetadataFields } from "./LibraryFormSections"; +import { libraryTypeMeta } from "./libraryTypes"; +import { LibraryPosterSection } from "./LibraryPosterSection"; +import { useLibraryForm } from "./useLibraryForm"; +import type { LibraryFormErrors } from "./useLibraryForm"; + +type SectionId = "general" | "folders" | "metadata" | "advanced"; + +const SECTIONS: Array<{ + id: SectionId; + label: string; + icon: typeof Settings2; + title: string; + description: string; +}> = [ + { + id: "general", + label: "General", + icon: Settings2, + title: "General", + description: "Name this library and choose the kind of media it holds.", + }, + { + id: "folders", + label: "Folders", + icon: FolderOpen, + title: "Folders", + description: "Silo scans these folders for media and watches them for changes.", + }, + { + id: "metadata", + label: "Metadata", + icon: Database, + title: "Metadata", + description: "Control where artwork and descriptions come from, and in which language.", + }, + { + id: "advanced", + label: "Advanced", + icon: SlidersHorizontal, + title: "Advanced", + description: "Optional background processing for this library.", + }, +]; + +function sectionForErrors(errors: LibraryFormErrors): SectionId | null { + if (errors.name) return "general"; + if (errors.paths) return "folders"; + return null; +} + +export interface LibraryEditorDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + library: Library | null; + chapterThumbnailsSupported: boolean; +} + +export function LibraryEditorDialog({ + open, + onOpenChange, + library, + chapterThumbnailsSupported, +}: LibraryEditorDialogProps) { + return ( + + + onOpenChange(false)} + /> + + + ); +} + +function LibraryEditorBody({ + library, + chapterThumbnailsSupported, + onClose, +}: { + library: Library | null; + chapterThumbnailsSupported: boolean; + onClose: () => void; +}) { + const [section, setSection] = useState("general"); + const form = useLibraryForm({ library, onClose }); + + const typeMeta = libraryTypeMeta(form.type); + const folderCount = form.paths.filter((p) => p.trim()).length; + const errorSections = new Set(); + if (form.errors.name) errorSections.add("general"); + if (form.errors.paths) errorSections.add("folders"); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + const result = form.submit(); + if (!result.ok) { + const target = sectionForErrors(result.errors); + if (target) setSection(target); + } + } + + return ( +
+ +
+
+ +
+
+ {library ? "Edit Library" : "Add Library"} + + {library + ? `Configure how “${library.name}” is scanned and matched.` + : "Set up a new library from folders on your server."} + +
+
+
+ + setSection(value as SectionId)} + orientation="vertical" + className="min-h-0 flex-1 gap-0" + > +
+ + {SECTIONS.map(({ id, label, icon: Icon }) => ( + + + {label} + {errorSections.has(id) ? ( + + ) : id === "folders" && folderCount > 0 ? ( + + {folderCount} + + ) : null} + + ))} + +
+
+ {SECTIONS.map(({ id, title, description }) => ( + +
+

{title}

+

{description}

+
+ {id === "general" && ( + : null} + /> + )} + {id === "folders" && } + {id === "metadata" && } + {id === "advanced" && ( + + )} +
+ ))} +
+
+ +
+ {errorSections.size > 0 ? ( +

+ {[form.errors.name, form.errors.paths].filter(Boolean).join(" ")} +

+ ) : null} + + + + +
+
+ ); +} diff --git a/web/src/components/admin/libraries/LibraryForm.test.tsx b/web/src/components/admin/libraries/LibraryForm.test.tsx new file mode 100644 index 00000000..328454bd --- /dev/null +++ b/web/src/components/admin/libraries/LibraryForm.test.tsx @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { contentLevelsForType } from "./useLibraryForm"; + +describe("contentLevelsForType", () => { + it("maps ebook libraries to the ebook metadata content level", () => { + expect(contentLevelsForType("ebooks")).toEqual(["ebook"]); + expect(contentLevelsForType("ebook")).toEqual(["ebook"]); + }); + + it("includes ebook metadata providers for mixed libraries", () => { + expect(contentLevelsForType("mixed")).toEqual([ + "movie", + "series", + "season", + "episode", + "audiobook", + "ebook", + ]); + }); +}); diff --git a/web/src/components/admin/libraries/LibraryForm.tsx b/web/src/components/admin/libraries/LibraryForm.tsx index cf946bf4..ad39b515 100644 --- a/web/src/components/admin/libraries/LibraryForm.tsx +++ b/web/src/components/admin/libraries/LibraryForm.tsx @@ -1,52 +1,10 @@ -import { useMemo, useState } from "react"; import type { FormEvent, ReactNode } from "react"; -import { - ArrowDown, - ArrowUp, - ChevronDown, - ChevronRight, - FolderOpen, - Plus, - Trash2, -} from "lucide-react"; -import type { CreateLibraryRequest, Library } from "@/api/types"; +import type { Library } from "@/api/types"; import { Button } from "@/components/ui/button"; -import FolderBrowser from "@/components/FolderBrowser"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import PathAutocompleteInput from "@/components/PathAutocompleteInput"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { - useCreateLibrary, - useLibraryProviders, - useSetLibraryProviders, - useUpdateLibrary, -} from "@/hooks/queries/admin/libraries"; -import { useAdminPlugins } from "@/hooks/queries/admin/plugins"; -import { cn } from "@/lib/utils"; -import { LANGUAGES } from "@/player/utils/languageNames"; -type LevelChainItem = { - plugin_installation_id: number; - capability_id: string; - provider_slug: string; - enabled: boolean; -}; - -type MetadataProvider = { - plugin_installation_id: number; - capability_id: string; - slug: string; - defaultPriority: Record; -}; +import { AdvancedFields, FolderFields, GeneralFields, MetadataFields } from "./LibraryFormSections"; +import { useLibraryForm } from "./useLibraryForm"; export interface LibraryFormProps { library: Library | null; @@ -56,189 +14,24 @@ export interface LibraryFormProps { resetAfterCreate?: boolean; submitLabel?: string; savingLabel?: string; - extraContent?: ReactNode; } -function contentLevelsForType(libraryType: string): string[] { - switch (libraryType) { - case "series": - return ["series", "season", "episode"]; - case "movies": - return ["movie"]; - case "mixed": - return ["movie", "series", "season", "episode"]; - case "audiobooks": - return ["audiobook"]; - case "podcasts": - return ["podcast", "podcast_episode"]; - default: - return []; - } -} - -function buildDefaultLevelChains( - metadataProviders: MetadataProvider[], - libraryType: string, -): Record { - const defaultChain: Record = {}; - for (const level of contentLevelsForType(libraryType)) { - const sorted = [...metadataProviders].sort((a, b) => { - const pa = a.defaultPriority[level] ?? 0; - const pb = b.defaultPriority[level] ?? 0; - if ((pa === 0) !== (pb === 0)) return pa === 0 ? 1 : -1; - return pa - pb; - }); - defaultChain[level] = sorted.map((provider) => ({ - plugin_installation_id: provider.plugin_installation_id, - capability_id: provider.capability_id, - provider_slug: provider.slug, - enabled: (provider.defaultPriority[level] ?? 0) > 0, - })); - } - return defaultChain; -} - -function buildLevelChainsFromServer( - currentChain: { - levels?: Record< - string, - Array<{ - plugin_installation_id: number; - capability_id: string; - provider_slug: string; - enabled: boolean; - }> - >; - } | null, - metadataProviders: MetadataProvider[], - libraryType: string, -): Record { - const mapped: Record = {}; - if (currentChain?.levels) { - for (const [level, entries] of Object.entries(currentChain.levels)) { - mapped[level] = entries.map((entry) => ({ - plugin_installation_id: entry.plugin_installation_id, - capability_id: entry.capability_id, - provider_slug: entry.provider_slug, - enabled: entry.enabled, - })); - } - } - - const defaults = buildDefaultLevelChains(metadataProviders, libraryType); - for (const level of contentLevelsForType(libraryType)) { - if (!mapped[level] || mapped[level].length === 0) { - mapped[level] = defaults[level] ?? []; - } - } - return mapped; -} - -function contentLevelLabel(level: string): string { - return level - .split("_") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -function buildProviderChainBody(activeLevelChains: Record) { - return { - levels: Object.fromEntries( - Object.entries(activeLevelChains).map(([level, items]) => [ - level, - items.map((item, i) => ({ - plugin_installation_id: item.plugin_installation_id, - capability_id: item.capability_id, - priority: i, - enabled: item.enabled, - })), - ]), - ), - }; -} - -function ProviderLevelSection({ - level, - items, - onReorder, - onToggleEnabled, -}: { - level: string; - items: LevelChainItem[]; - onReorder: (items: LevelChainItem[]) => void; - onToggleEnabled: (index: number) => void; -}) { - const [collapsed, setCollapsed] = useState(false); - - const moveItem = (index: number, direction: -1 | 1) => { - const newItems = [...items]; - const target = index + direction; - if (target < 0 || target >= newItems.length) return; - [newItems[index], newItems[target]] = [newItems[target]!, newItems[index]!]; - onReorder(newItems); - }; - +function FormSection({ title, children }: { title: string; children: ReactNode }) { return ( -
- - {!collapsed && ( -
- {items.map((item, i) => ( -
- onToggleEnabled(i)} - className="h-3.5 w-3.5" - style={{ accentColor: "var(--primary)" }} - /> - {item.provider_slug} -
- - -
- {i + 1} -
- ))} -
- )} -
+
+

+ {title} +

+ {children} +
); } +/** + * Inline (non-dialog) library form used by the setup wizard. The admin + * Libraries page uses LibraryEditorDialog, which renders the same sections + * behind a left-hand navigation rail. + */ export function LibraryForm({ library, chapterThumbnailsSupported, @@ -247,324 +40,30 @@ export function LibraryForm({ resetAfterCreate = false, submitLabel = "Save", savingLabel = "Saving...", - extraContent, }: LibraryFormProps) { - const [name, setName] = useState(library?.name ?? ""); - const [paths, setPaths] = useState(library?.paths?.length ? library.paths : [""]); - const [type, setType] = useState(library?.type ?? "movies"); - const [enabled, setEnabled] = useState(library?.enabled ?? true); - const [metadataLanguage, setMetadataLanguage] = useState(library?.metadata_language ?? "en"); - const [chapterThumbnailsEnabled, setChapterThumbnailsEnabled] = useState( - library?.chapter_thumbnails_enabled ?? false, - ); - const [introDetectionEnabled, setIntroDetectionEnabled] = useState( - library?.intro_detection_enabled ?? false, - ); - const [levelChains, setLevelChains] = useState>({}); - const [chainDirty, setChainDirty] = useState(false); - const [browserOpen, setBrowserOpen] = useState(false); + const form = useLibraryForm({ library, onClose, onSaved, resetAfterCreate }); - const createMutation = useCreateLibrary(); - const updateMutation = useUpdateLibrary(); - const setChainMutation = useSetLibraryProviders(); - const { installations } = useAdminPlugins(); - const { data: currentChain } = useLibraryProviders(library?.id ?? null); - - const metadataProviders = useMemo(() => { - const result: MetadataProvider[] = []; - for (const inst of installations) { - if (!inst.enabled) continue; - for (const cap of inst.capabilities ?? []) { - if (cap.type === "metadata_provider.v1") { - const dp = - (cap.metadata?.default_priority as Record) ?? - ((cap.metadata?.metadata as Record)?.default_priority as Record< - string, - number - >) ?? - {}; - result.push({ - plugin_installation_id: inst.id, - capability_id: cap.id, - slug: cap.display_name || cap.id, - defaultPriority: dp, - }); - } - } - } - return result; - }, [installations]); - - const isPending = - createMutation.isPending || updateMutation.isPending || setChainMutation.isPending; - - const defaultLevelChains = useMemo( - () => buildDefaultLevelChains(metadataProviders, type), - [metadataProviders, type], - ); - const resolvedLevelChains = useMemo(() => { - if (!library) { - return defaultLevelChains; - } - if (currentChain === undefined) { - return levelChains; - } - return buildLevelChainsFromServer(currentChain, metadataProviders, type); - }, [currentChain, defaultLevelChains, levelChains, library, metadataProviders, type]); - const activeLevelChains = chainDirty ? levelChains : resolvedLevelChains; - - function updatePath(index: number, value: string) { - const next = [...paths]; - next[index] = value; - setPaths(next); - } - - function addPath() { - setPaths([...paths, ""]); - } - - function removePath(index: number) { - setPaths(paths.filter((_, i) => i !== index)); - } - - function handleBrowseSelect(selectedPaths: string[]) { - const merged = [...paths.filter((path) => path.trim())]; - for (const selectedPath of selectedPaths) { - if (!merged.includes(selectedPath)) { - merged.push(selectedPath); - } - } - setPaths(merged.length > 0 ? merged : [""]); - setBrowserOpen(false); - } - - function handleTypeChange(newType: string) { - setType(newType); - if (!library) { - setLevelChains(buildDefaultLevelChains(metadataProviders, newType)); - setChainDirty(true); - } - } - - function finishCreate(created: Library) { - onSaved?.(created); - if (resetAfterCreate) { - setName(""); - setPaths([""]); - } else { - onClose?.(); - } - } - - async function handleSubmit(e: FormEvent) { + function handleSubmit(e: FormEvent) { e.preventDefault(); - const filteredPaths = paths.filter((p) => p.trim()); - if (filteredPaths.length === 0) return; - - const body: CreateLibraryRequest = { - name, - paths: filteredPaths, - type, - enabled, - metadata_language: metadataLanguage, - chapter_thumbnails_enabled: chapterThumbnailsEnabled, - intro_detection_enabled: introDetectionEnabled, - }; - - if (library) { - updateMutation.mutate( - { id: library.id, body }, - { - onSuccess: () => { - if (chainDirty) { - setChainMutation.mutate( - { - id: library.id, - body: buildProviderChainBody(activeLevelChains), - }, - { onSuccess: () => onClose?.() }, - ); - } else { - onClose?.(); - } - }, - }, - ); - return; - } - - createMutation.mutate(body, { - onSuccess: (created) => { - if (chainDirty) { - setChainMutation.mutate( - { - id: created.id, - body: buildProviderChainBody(activeLevelChains), - }, - { onSuccess: () => finishCreate(created) }, - ); - } else { - finishCreate(created); - } - }, - }); + form.submit(); } return ( -
-
-
- - setName(e.target.value)} required /> -
-
- - -
-
-
- - {paths.map((p, i) => ( -
- updatePath(i, value)} - placeholder="/mnt/media/movies" - required - /> - {paths.length > 1 && ( - - )} -
- ))} -
- - -
- path.trim())} - /> -
-
-
- - -
-
- - -
-
-
-
-
- -

- Stores chapter preview images in the configured public asset S3 bucket. Chapter - markers and chapter menus still work without thumbnails. -

- {!chapterThumbnailsSupported ? ( -

- Public asset S3 storage is required before this can be enabled. -

- ) : null} -
- -
-
-
-
-
- -

- Runs background audio analysis for episodes in this library. Embedded intro chapters - are used when available. -

-
- -
-
- {extraContent} - - {contentLevelsForType(type).length > 0 && ( -
-

Metadata Providers

- {contentLevelsForType(type).map((level) => { - const items = activeLevelChains[level] ?? []; - return ( - { - setLevelChains({ ...activeLevelChains, [level]: newItems }); - setChainDirty(true); - }} - onToggleEnabled={(index) => { - setLevelChains((prev) => { - const source = prev[level] ?? activeLevelChains[level] ?? []; - const updated = [...source]; - updated[index] = { ...updated[index]!, enabled: !updated[index]!.enabled }; - return { ...prev, [level]: updated }; - }); - setChainDirty(true); - }} - /> - ); - })} -
- )} - -
); diff --git a/web/src/components/admin/libraries/LibraryFormSections.tsx b/web/src/components/admin/libraries/LibraryFormSections.tsx new file mode 100644 index 00000000..b9c2d895 --- /dev/null +++ b/web/src/components/admin/libraries/LibraryFormSections.tsx @@ -0,0 +1,359 @@ +import { useState } from "react"; +import type { ReactNode } from "react"; +import { + ArrowDown, + ArrowUp, + ChevronDown, + ChevronRight, + FolderOpen, + FolderSearch, + Plus, + Trash2, +} from "lucide-react"; + +import FolderBrowser from "@/components/FolderBrowser"; +import PathAutocompleteInput from "@/components/PathAutocompleteInput"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { LANGUAGES } from "@/player/utils/languageNames"; + +import { LIBRARY_TYPES } from "./libraryTypes"; +import { contentLevelLabel } from "./useLibraryForm"; +import type { LevelChainItem, LibraryFormController } from "./useLibraryForm"; + +function SettingCard({ + htmlFor, + title, + description, + children, + footer, +}: { + htmlFor?: string; + title: string; + description: string; + children: ReactNode; + footer?: ReactNode; +}) { + return ( +
+
+
+ +

{description}

+ {footer} +
+ {children} +
+
+ ); +} + +export function GeneralFields({ + form, + posterSlot, +}: { + form: LibraryFormController; + posterSlot?: ReactNode; +}) { + return ( +
+
+ + form.setName(e.target.value)} + placeholder="e.g. Movies" + aria-invalid={form.errors.name ? true : undefined} + /> + {form.errors.name ?

{form.errors.name}

: null} +
+
+ +
+ {LIBRARY_TYPES.map(({ value, label, icon: Icon }) => { + const selected = form.type === value; + return ( + + ); + })} +
+ {form.library && form.type !== form.library.type ? ( +

+ Changing the type of an existing library may require a full rescan to rematch items. +

+ ) : null} +
+ + + + {posterSlot} +
+ ); +} + +export function FolderFields({ form }: { form: LibraryFormController }) { + const [browserOpen, setBrowserOpen] = useState(false); + + return ( +
+
+ {form.paths.map((path, i) => ( +
+ + form.updatePath(i, value)} + placeholder="/mnt/media/movies" + aria-invalid={form.errors.paths ? true : undefined} + /> + {form.paths.length > 1 && ( + + )} +
+ ))} +
+ {form.errors.paths ?

{form.errors.paths}

: null} +
+ + +
+ { + form.mergeBrowsedPaths(selected); + setBrowserOpen(false); + }} + existingPaths={form.paths.filter((path) => path.trim())} + /> +
+ ); +} + +function ProviderLevelSection({ + level, + items, + onReorder, + onToggleEnabled, +}: { + level: string; + items: LevelChainItem[]; + onReorder: (items: LevelChainItem[]) => void; + onToggleEnabled: (index: number) => void; +}) { + const [collapsed, setCollapsed] = useState(false); + + const moveItem = (index: number, direction: -1 | 1) => { + const newItems = [...items]; + const target = index + direction; + if (target < 0 || target >= newItems.length) return; + [newItems[index], newItems[target]] = [newItems[target]!, newItems[index]!]; + onReorder(newItems); + }; + + return ( +
+ + {!collapsed && ( +
+ {items.map((item, i) => ( +
+ onToggleEnabled(i)} + className="h-3.5 w-3.5" + style={{ accentColor: "var(--primary)" }} + /> + {item.provider_slug} +
+ + +
+ {i + 1} +
+ ))} +
+ )} +
+ ); +} + +export function MetadataFields({ form }: { form: LibraryFormController }) { + return ( +
+
+ + +

+ Preferred language for titles, summaries, and artwork fetched from providers. +

+
+ {form.contentLevels.length > 0 && ( +
+ +

+ Providers are asked in order from top to bottom. Uncheck a provider to skip it for that + level. +

+ {form.hasMetadataProviders ? ( + form.contentLevels.map((level) => ( + form.reorderLevel(level, newItems)} + onToggleEnabled={(index) => form.toggleLevelProvider(level, index)} + /> + )) + ) : ( +

+ No metadata provider plugins are installed. Install one under Admin → Plugins to fetch + artwork and descriptions. +

+ )} +
+ )} +
+ ); +} + +export function AdvancedFields({ + form, + chapterThumbnailsSupported, +}: { + form: LibraryFormController; + chapterThumbnailsSupported: boolean; +}) { + return ( +
+ + Public asset S3 storage is required before this can be enabled. +

+ ) : null + } + > + +
+ + + +
+ ); +} diff --git a/web/src/components/admin/libraries/LibraryPosterSection.tsx b/web/src/components/admin/libraries/LibraryPosterSection.tsx new file mode 100644 index 00000000..cd945dac --- /dev/null +++ b/web/src/components/admin/libraries/LibraryPosterSection.tsx @@ -0,0 +1,73 @@ +import type { ChangeEvent } from "react"; +import { ImageIcon, Trash2 } from "lucide-react"; + +import type { Library } from "@/api/types"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { useDeleteLibraryPoster, useUploadLibraryPoster } from "@/hooks/queries/admin/libraries"; + +export function LibraryPosterSection({ library }: { library: Library }) { + const uploadMutation = useUploadLibraryPoster(); + const deleteMutation = useDeleteLibraryPoster(); + const fileInputId = `poster-upload-${library.id}`; + + function handleFileChange(e: ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + uploadMutation.mutate({ id: library.id, file }); + e.target.value = ""; + } + + return ( +
+ +
+ {library.poster_url ? ( + {`${library.name} + ) : ( +
+ +
+ )} + + + {library.poster_url && ( + + )} +
+
+ ); +} diff --git a/web/src/components/admin/libraries/libraryTypes.ts b/web/src/components/admin/libraries/libraryTypes.ts new file mode 100644 index 00000000..bf226b52 --- /dev/null +++ b/web/src/components/admin/libraries/libraryTypes.ts @@ -0,0 +1,14 @@ +import { BookHeadphones, BookOpen, Film, Layers, Podcast, Tv } from "lucide-react"; + +export const LIBRARY_TYPES = [ + { value: "movies", label: "Movies", icon: Film }, + { value: "series", label: "Series", icon: Tv }, + { value: "mixed", label: "Mixed", icon: Layers }, + { value: "audiobooks", label: "Audiobooks", icon: BookHeadphones }, + { value: "ebooks", label: "Ebooks", icon: BookOpen }, + { value: "podcasts", label: "Podcasts", icon: Podcast }, +] as const; + +export function libraryTypeMeta(type: string) { + return LIBRARY_TYPES.find((t) => t.value === type) ?? LIBRARY_TYPES[0]; +} diff --git a/web/src/components/admin/libraries/useLibraryForm.ts b/web/src/components/admin/libraries/useLibraryForm.ts new file mode 100644 index 00000000..164b7b47 --- /dev/null +++ b/web/src/components/admin/libraries/useLibraryForm.ts @@ -0,0 +1,361 @@ +import { useMemo, useState } from "react"; + +import type { CreateLibraryRequest, Library } from "@/api/types"; +import { + useCreateLibrary, + useLibraryProviders, + useSetLibraryProviders, + useUpdateLibrary, +} from "@/hooks/queries/admin/libraries"; +import { useAdminPlugins } from "@/hooks/queries/admin/plugins"; + +export type LevelChainItem = { + plugin_installation_id: number; + capability_id: string; + provider_slug: string; + enabled: boolean; +}; + +type MetadataProvider = { + plugin_installation_id: number; + capability_id: string; + slug: string; + defaultPriority: Record; +}; + +export interface LibraryFormErrors { + name?: string; + paths?: string; +} + +export interface UseLibraryFormOptions { + library: Library | null; + onClose?: () => void; + onSaved?: (library: Library) => void; + resetAfterCreate?: boolean; +} + +export function contentLevelsForType(libraryType: string): string[] { + switch (libraryType) { + case "series": + return ["series", "season", "episode"]; + case "movies": + return ["movie"]; + case "mixed": + return ["movie", "series", "season", "episode", "audiobook", "ebook"]; + case "audiobooks": + return ["audiobook"]; + case "ebooks": + case "ebook": + return ["ebook"]; + case "podcasts": + return ["podcast", "podcast_episode"]; + default: + return []; + } +} + +export function contentLevelLabel(level: string): string { + return level + .split("_") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +function buildDefaultLevelChains( + metadataProviders: MetadataProvider[], + libraryType: string, +): Record { + const defaultChain: Record = {}; + for (const level of contentLevelsForType(libraryType)) { + const sorted = [...metadataProviders].sort((a, b) => { + const pa = a.defaultPriority[level] ?? 0; + const pb = b.defaultPriority[level] ?? 0; + if ((pa === 0) !== (pb === 0)) return pa === 0 ? 1 : -1; + return pa - pb; + }); + defaultChain[level] = sorted.map((provider) => ({ + plugin_installation_id: provider.plugin_installation_id, + capability_id: provider.capability_id, + provider_slug: provider.slug, + enabled: (provider.defaultPriority[level] ?? 0) > 0, + })); + } + return defaultChain; +} + +function buildLevelChainsFromServer( + currentChain: { + levels?: Record< + string, + Array<{ + plugin_installation_id: number; + capability_id: string; + provider_slug: string; + enabled: boolean; + }> + >; + } | null, + metadataProviders: MetadataProvider[], + libraryType: string, +): Record { + const mapped: Record = {}; + if (currentChain?.levels) { + for (const [level, entries] of Object.entries(currentChain.levels)) { + mapped[level] = entries.map((entry) => ({ + plugin_installation_id: entry.plugin_installation_id, + capability_id: entry.capability_id, + provider_slug: entry.provider_slug, + enabled: entry.enabled, + })); + } + } + + const defaults = buildDefaultLevelChains(metadataProviders, libraryType); + for (const level of contentLevelsForType(libraryType)) { + if (!mapped[level] || mapped[level].length === 0) { + mapped[level] = defaults[level] ?? []; + } + } + return mapped; +} + +function buildProviderChainBody(activeLevelChains: Record) { + return { + levels: Object.fromEntries( + Object.entries(activeLevelChains).map(([level, items]) => [ + level, + items.map((item, i) => ({ + plugin_installation_id: item.plugin_installation_id, + capability_id: item.capability_id, + priority: i, + enabled: item.enabled, + })), + ]), + ), + }; +} + +export function useLibraryForm({ + library, + onClose, + onSaved, + resetAfterCreate = false, +}: UseLibraryFormOptions) { + const [name, setName] = useState(library?.name ?? ""); + const [paths, setPaths] = useState(library?.paths?.length ? library.paths : [""]); + const [type, setType] = useState(library?.type ?? "movies"); + const [enabled, setEnabled] = useState(library?.enabled ?? true); + const [metadataLanguage, setMetadataLanguage] = useState(library?.metadata_language ?? "en"); + const [chapterThumbnailsEnabled, setChapterThumbnailsEnabled] = useState( + library?.chapter_thumbnails_enabled ?? false, + ); + const [introDetectionEnabled, setIntroDetectionEnabled] = useState( + library?.intro_detection_enabled ?? false, + ); + const [levelChains, setLevelChains] = useState>({}); + const [chainDirty, setChainDirty] = useState(false); + const [submitAttempted, setSubmitAttempted] = useState(false); + + const createMutation = useCreateLibrary(); + const updateMutation = useUpdateLibrary(); + const setChainMutation = useSetLibraryProviders(); + const { installations } = useAdminPlugins(); + const { data: currentChain } = useLibraryProviders(library?.id ?? null); + + const metadataProviders = useMemo(() => { + const result: MetadataProvider[] = []; + for (const inst of installations) { + if (!inst.enabled) continue; + for (const cap of inst.capabilities ?? []) { + if (cap.type === "metadata_provider.v1") { + const dp = + (cap.metadata?.default_priority as Record) ?? + ((cap.metadata?.metadata as Record)?.default_priority as Record< + string, + number + >) ?? + {}; + result.push({ + plugin_installation_id: inst.id, + capability_id: cap.id, + slug: cap.display_name || cap.id, + defaultPriority: dp, + }); + } + } + } + return result; + }, [installations]); + + const isPending = + createMutation.isPending || updateMutation.isPending || setChainMutation.isPending; + + const defaultLevelChains = useMemo( + () => buildDefaultLevelChains(metadataProviders, type), + [metadataProviders, type], + ); + const resolvedLevelChains = useMemo(() => { + if (!library) { + return defaultLevelChains; + } + if (currentChain === undefined) { + return levelChains; + } + return buildLevelChainsFromServer(currentChain, metadataProviders, type); + }, [currentChain, defaultLevelChains, levelChains, library, metadataProviders, type]); + const activeLevelChains = chainDirty ? levelChains : resolvedLevelChains; + + const allErrors = useMemo(() => { + const next: LibraryFormErrors = {}; + if (!name.trim()) next.name = "Give this library a name."; + if (!paths.some((p) => p.trim())) next.paths = "Add at least one folder to scan."; + return next; + }, [name, paths]); + const errors: LibraryFormErrors = submitAttempted ? allErrors : {}; + + function updatePath(index: number, value: string) { + const next = [...paths]; + next[index] = value; + setPaths(next); + } + + function addPath() { + setPaths([...paths, ""]); + } + + function removePath(index: number) { + setPaths(paths.filter((_, i) => i !== index)); + } + + function mergeBrowsedPaths(selectedPaths: string[]) { + const merged = [...paths.filter((path) => path.trim())]; + for (const selectedPath of selectedPaths) { + if (!merged.includes(selectedPath)) { + merged.push(selectedPath); + } + } + setPaths(merged.length > 0 ? merged : [""]); + } + + function handleTypeChange(newType: string) { + setType(newType); + if (!library) { + setLevelChains(buildDefaultLevelChains(metadataProviders, newType)); + setChainDirty(true); + } + } + + function reorderLevel(level: string, items: LevelChainItem[]) { + setLevelChains({ ...activeLevelChains, [level]: items }); + setChainDirty(true); + } + + function toggleLevelProvider(level: string, index: number) { + setLevelChains((prev) => { + const source = prev[level] ?? activeLevelChains[level] ?? []; + const updated = [...source]; + updated[index] = { ...updated[index]!, enabled: !updated[index]!.enabled }; + return { ...prev, [level]: updated }; + }); + setChainDirty(true); + } + + function finishCreate(created: Library) { + onSaved?.(created); + if (resetAfterCreate) { + setName(""); + setPaths([""]); + setSubmitAttempted(false); + } else { + onClose?.(); + } + } + + function submit(): { ok: boolean; errors: LibraryFormErrors } { + setSubmitAttempted(true); + if (allErrors.name || allErrors.paths) { + return { ok: false, errors: allErrors }; + } + + const body: CreateLibraryRequest = { + name: name.trim(), + paths: paths.filter((p) => p.trim()), + type, + enabled, + metadata_language: metadataLanguage, + chapter_thumbnails_enabled: chapterThumbnailsEnabled, + intro_detection_enabled: introDetectionEnabled, + }; + + if (library) { + updateMutation.mutate( + { id: library.id, body }, + { + onSuccess: () => { + if (chainDirty) { + setChainMutation.mutate( + { + id: library.id, + body: buildProviderChainBody(activeLevelChains), + }, + { onSuccess: () => onClose?.() }, + ); + } else { + onClose?.(); + } + }, + }, + ); + return { ok: true, errors: {} }; + } + + createMutation.mutate(body, { + onSuccess: (created) => { + if (chainDirty) { + setChainMutation.mutate( + { + id: created.id, + body: buildProviderChainBody(activeLevelChains), + }, + { onSuccess: () => finishCreate(created) }, + ); + } else { + finishCreate(created); + } + }, + }); + return { ok: true, errors: {} }; + } + + return { + library, + name, + setName, + paths, + updatePath, + addPath, + removePath, + mergeBrowsedPaths, + type, + handleTypeChange, + enabled, + setEnabled, + metadataLanguage, + setMetadataLanguage, + chapterThumbnailsEnabled, + setChapterThumbnailsEnabled, + introDetectionEnabled, + setIntroDetectionEnabled, + contentLevels: contentLevelsForType(type), + activeLevelChains, + reorderLevel, + toggleLevelProvider, + hasMetadataProviders: metadataProviders.length > 0, + errors, + isPending, + submit, + }; +} + +export type LibraryFormController = ReturnType; diff --git a/web/src/components/admin/plugins/PluginConfigForm.tsx b/web/src/components/admin/plugins/PluginConfigForm.tsx index 2c3acbb2..acf9f754 100644 --- a/web/src/components/admin/plugins/PluginConfigForm.tsx +++ b/web/src/components/admin/plugins/PluginConfigForm.tsx @@ -2,21 +2,16 @@ import { useEffect, useMemo, useState } from "react"; import type { ConnectionCheckResponse, + PluginAdminForm, PluginAdminFormField, PluginConfigSchema, } from "@/api/types"; import { ConnectionCheckAction } from "@/components/admin/ConnectionCheckAction"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; + +import { SchemaForm } from "./SchemaForm"; +import { buildSchemaValues } from "./schemaFormUtils"; type PluginConfigValue = Record; @@ -124,37 +119,6 @@ function valueForField(field: SupportedField, configValue?: PluginConfigValue): return defaultValueForField(field); } -function buildPayload(fields: SupportedField[], draft: Record) { - const payload: PluginConfigValue = {}; - for (const field of fields) { - const value = draft[field.key]; - if (field.control === "SWITCH") { - payload[field.key] = Boolean(value); - continue; - } - if (typeof value !== "string") { - continue; - } - const trimmed = value.trim(); - if (trimmed === "") { - continue; - } - if ( - field.control === "NUMBER" || - field.inferredType === "number" || - field.inferredType === "integer" - ) { - const numeric = Number(trimmed); - if (!Number.isNaN(numeric)) { - payload[field.key] = numeric; - continue; - } - } - payload[field.key] = value; - } - return payload; -} - export function PluginConfigForm({ schema, value, @@ -174,18 +138,23 @@ export function PluginConfigForm({ const supported = fields.length > 0 && (schema.admin_form?.fields?.length ? true : parsedFallback.supported); - const [draft, setDraft] = useState>(() => + const descriptor = useMemo( + () => schema.admin_form ?? { fields }, + [schema.admin_form, fields], + ); + + const [values, setValues] = useState(() => Object.fromEntries(fields.map((field) => [field.key, valueForField(field, value)])), ); const [testResult, setTestResult] = useState(null); useEffect(() => { - setDraft(Object.fromEntries(fields.map((field) => [field.key, valueForField(field, value)]))); + setValues(Object.fromEntries(fields.map((field) => [field.key, valueForField(field, value)]))); }, [fields, value]); - function setField(key: string, nextValue: string | boolean) { + function handleChange(next: PluginConfigValue) { setTestResult(null); - setDraft((current) => ({ ...current, [key]: nextValue })); + setValues(next); } async function handleTest() { @@ -194,7 +163,7 @@ export function PluginConfigForm({ } try { - setTestResult(await onTest(schema.key, buildPayload(fields, draft))); + setTestResult(await onTest(schema.key, buildSchemaValues(descriptor, values))); } catch (error) { setTestResult({ success: false, @@ -223,69 +192,12 @@ export function PluginConfigForm({ ) : null}
-
- {fields.map((field) => ( -
-
- - {field.description ? ( -

{field.description}

- ) : null} -
- - {field.control === "SWITCH" ? ( -
- setField(field.key, checked)} - /> - {field.label || humanizeKey(field.key)} -
- ) : field.control === "SELECT" ? ( - - ) : field.control === "TEXTAREA" || field.multiline ? ( -