diff --git a/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md b/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md index b606f9e4b..c4c335b00 100644 --- a/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md +++ b/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md @@ -33,20 +33,178 @@ issue a load request, which will first look for local images on-disk and then co found, use the stored binary data from the network cache to populate the in-memory pixmap cache under the card's cache key. If it is not found, it will then proceed with issuing a network request. -The size of both of these caches can be configured by the user in the "Card Sources" settings page. +The size of both of these caches can be configured by the user on the "Storage" settings page. # PixmapCacheKeys and ProviderIDs -TODO +Every card picture that is loaded ends up in the QPixmapCache under a key that identifies the exact printing it belongs +to. The key is produced by ExactCard::getPixmapCacheKey() and has the following shape: + +```text +card__ +``` + +For example, the _Example Card_ printing with provider ID `0b23cdc8-d413-4fb1-8470-474221b10fe2` is stored +under `card_Example Card_0b23cdc8-d413-4fb1-8470-474221b10fe2`. If the printing has no provider ID, the key +drops the suffix and falls back to `card_`. + +The **provider ID** is the Scryfall UUID of the printing. Oracle maps the `scryfallId` of every printing to the `uuid` +property when building the card database, and deck files persist it as the `uuid` attribute of each card entry. Because +the provider ID is part of the pixmap cache key, two different printings of the same card never share a cache entry. +This is exactly what allows the printing selector and exact-card lookups to display the picture of the precise printing +a card was added as. + +The base key holds the full-size image. When a widget asks for a scaled version, the scaled pixmap is stored under an +additional key of the form `_x`, with the size adjusted for the device pixel ratio of the screen, +so each widget size is only ever scaled once. + +The cache key is also used for bookkeeping outside of the pixmap cache itself: + +- CardPictureLoaderWorker keeps a set of keys that are currently being loaded so the same card is never queued twice. +- CardPictureLoader tracks, per key, the last time loading failed. A failed load stores a NULL pixmap under the key; as + long as that marker is present, subsequent requests for the card show the "failed" card back and are only re-queued + after the retry interval of 300 seconds has passed. +- When the CardInfo of a loaded card is destroyed, its cache entries and failure markers are removed. # The Redirect Cache -TODO +Many picture URLs - in particular the Gatherer and Scryfall URLs from the default set of templates - redirect to a CDN +or to a different host. To avoid following the same redirect for every single card, CardPictureLoaderWorker remembers +redirects and applies them without an extra network round trip. + +The redirect cache is a hash map from original URL to redirect URL plus timestamp. It is persisted to a `cache.ini` +file (Qt's INI format, under the `redirects` array) inside the redirect cache directory +(`SettingsCache::instance().paths().getRedirectCachePath()`, i.e. `/redirects/`). The cache is loaded when the worker +starts, pruned of entries older than the configured TTL, and written back to disk when the application quits. + +Entries are added whenever a network reply reports a redirection (see below) and are consulted before any request is +made: both CardPictureLoaderWorker::queueRequest() and CardPictureLoaderWorker::makeRequest() check for a cached +redirect first and jump straight to the final URL. + +The TTL is the "Redirect Cache TTL" setting on the "Storage" settings page and defaults to 30 days. Lowering it makes +Cockatrice re-resolve redirects sooner, which can help when a download URL changed its redirect target. + +Because Cockatrice tracks redirects itself, the QNetworkAccessManager is configured with Qt's `ManualRedirectPolicy`. +Redirects found in a reply are handled manually: + +- A recursive redirect (a URL redirecting to itself) is treated as a failed load. +- Otherwise the redirect is recorded in the redirect cache and the request is re-issued against the target URL. +- A successful reply with one of the redirect status codes 301, 302, 303, 305, 307 or 308 is handled the same way. + +Clearing the network cache (CardPictureLoader::clearNetworkCache()) also clears the redirect cache. # Local Image Loading -TODO +Before any network request is issued, CardPictureLoaderWorker hands the ExactCard to CardPictureLoaderLocal, which +tries to find a matching picture on disk. If a local picture is found, it is used and no network request is made. + +CardPictureLoaderLocal searches three locations: + +- The **CUSTOM folder** (`/CUSTOM/`). Every file in it is indexed recursively by its base name + (both `baseName` and `completeBaseName`, so a file named `ExampleCard.jpg` is indexed as `ExampleCard`). The index is rebuilt + every 10 seconds, so new files are picked up without restarting the + client (changing the configured pictures directory only reassigns the search paths; the next timer tick rebuilds the index). +- The **set-named subfolders** of the pictures directory: `//` and + `/downloadedPics//`. +- The **root of the `downloadedPics` folder** (`/downloadedPics/`). The export naming schemes without a + set-folder part write their files straight into `downloadedPics/`, so this is where flat-scheme downloads and the + local overrides described below are matched. + +For each candidate folder, the loader generates file-name variants from the card's corrected name, set code, collector +number and provider ID using the import naming schemes (Card Name + Provider ID, Card Name + Set + Collector, +Set + Collector + Card Name, Card Name + Set, Card Name), each tried with both `_` and `-` as separator. A file is +accepted when its name without the extension *equals* the variant exactly - the extension itself is free - and the +first variant that yields a readable image wins. For example, the file `Example Card_EXM_43.png` in the `EXM` set +folder matches the card with corrected name `Example Card`, set code `EXM` and collector number `43`. + +\attention The file-name variants use the *corrected* card name, so split cards are stored under their joined name: the +"Example // Card" card is matched by a file named `ExampleCard.*`. + +The naming schemes are also documented in the user-facing page @ref custom_card_pictures, which additionally covers +the CUSTOM folder workflow, `picurl` and download URL templates. + +When the filesystem cache method is selected on the "Storage" settings page, downloaded images are additionally written +into `/downloadedPics/` using the configured export naming scheme (as `.png` files). The two export +schemes with a set-folder part (`Set Folder / Name + Provider ID` and `Set Folder / Name + Set Name + Collector`) write +into `downloadedPics//`; the three flat schemes write directly into `downloadedPics/`. Automatic cache writes +never overwrite an existing file, so a provider outage can permanently leave an outdated image in that folder until it +is deleted manually - the user-facing troubleshooting guide @ref fixing_card_pictures covers how to do this. Explicit +image overrides (see below) are the exception and always overwrite. + +# Local Image Overrides + +Beyond the generic on-disk lookup above, individual printings can be given explicit artwork that wins over every other +source without touching the CUSTOM folder or any download URL. This is the "Image Overrides" submenu of the context menu +that opens when you right-click a card in the deck editor's printing selector. + +- **Load Custom Image...** asks for a picture file and installs it for the card through + CardPictureLoader::saveCardImageToLocalStorage() with `allowOverwrite == true`. +- **One entry per alternate printing** (labeled ` `): selecting one hands the card to + CardPictureLoader::installPrintingOverride(), which resolves that printing's artwork - enqueueing a load and waiting + for the `CardInfo::pixmapUpdated` signal if it is not cached yet - and persists it for the card. +- **Clear Custom Image** calls CardPictureLoader::deleteAllLocalOverrides() to remove every stored override image of the + card, after which normal resolution resumes. The entry is only enabled while CardPictureLoader::hasLocalOverrides() + reports at least one stored file. + +Overrides are stored as `.png` files in `downloadedPics/` under the export naming scheme configured on the "Storage" +settings page - which is exactly why the local matcher also looks into the `downloadedPics/` root (see above). They are +written with `allowOverwrite == true`, so an override always replaces whatever the filesystem cache previously saved for +that spelling; only *automatic* cache writes are prevented from clobbering it. Overriding a card with its own current +printing is a no-op (the UI omits it from the menu), and an override whose artwork fails to resolve surfaces the +"failed" card back instead of a silent no-op while any override already on disk is left in place and re-displayed. # URL Generation and Resolution -TODO \ No newline at end of file +When no local image is available and downloading is enabled, the network loader starts working through a list of +candidate URLs. This list is managed by CardPictureToLoad and is built in two steps. + +First, CardPictureToLoad::extractSetsSorted() collects all sets the card has printings in and sorts them by set +priority. Unless the user disabled per-printing art ("Override all card art with personal set preference (Pre-ProviderID +change behavior)"), the set that +matches the requested printing's provider ID is moved to the front, so the exact printing is always attempted first. + +For each set, CardPictureToLoad::populateSetUrls() builds an ordered URL list: + +1. A custom URL defined for that printing via the `picurl` property in the card database, if present. +2. The configured download URL templates, in priority order (Deck Editor → "URL Download Priority"). + +URL templates are transformed into concrete URLs by CardPictureToLoad::transformUrl(), which substitutes reference +points. `!name!`, `!setcode!` and friends substitute card and printing data, while the `!set:!` and +`!prop:!` reference points resolve a property of the printing or of the card respectively. The canonical list +of all reference points with examples, including the `_fill_with_` and `_substr_` modifiers, lives in +@ref custom_card_pictures. + +The `!set:...!` and `!prop:...!` reference points also support two modifiers: + +- `_fill_with_` pads the value with the given text, right-aligned, e.g. `!set:num_fill_with_000!` turns collector + number `1` into `001`. If the value is longer than the fill text, the template is invalidated. +- `_substr__` extracts a substring, e.g. `!set:num_substr_2_2!` takes two characters starting at the + third. If the substring would extend past the end of the value, the template is invalidated. + +Substituted values are percent-encoded. If a template asks for a property the card or printing does not have (or one of +the modifiers invalidates it), the template yields no URL and is skipped; the next template is tried instead. + +\attention Custom URLs should start with `http://` or `https://`. The scheme is not validated before the URL is handed +to QNetworkAccessManager, so a template without an absolute scheme may silently fail to download; prefer HTTPS where the +provider allows it. + +The resolution order is: for the current set, try each URL in the list; when all URLs for a set are exhausted, move to +the next set; when every set is exhausted, the load fails. A failed load is reported through the NULL-pixmap mechanism +described in the PixmapCacheKeys and ProviderIDs section above. + +Several mechanisms influence the resolution process: + +- **Rate limiting.** The worker allows roughly 10 requests per second globally. A server that answers with HTTP 429 + gets its per-host allowance halved; the first 429 for a host is waited out (honoring the `Retry-After` header if + present) and the same URL retried, while a second 429 makes the loader fall through to the other configured sources. + When all sources are exhausted the request is deferred with some random jitter and retried once the back-off expires. +- **Redirects.** Replies with a redirect status (301, 302, 303, 305, 307, 308) are followed and recorded in the + redirect cache as described in the Redirect Cache section above. +- **Blacklisted images.** Gatherer returns the card back image for cards it does not know. A few known MD5 hashes of + that image are blacklisted, so such a "successful" download is treated as not found instead of being shown. +- **WebP.** Images detected as WebP (RIFF/WEBP header) are decoded through QMovie instead of QImageReader. +- **Downloads disabled.** When "Download card pictures on the fly" is disabled and the network cache method is active, + requests use Qt's `AlwaysCache` policy so that only previously cached images are served. + +A user-facing reference for writing download URL templates, including more worked examples, is available at +@ref custom_card_pictures. diff --git a/doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md b/doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md new file mode 100644 index 000000000..3f1079661 --- /dev/null +++ b/doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md @@ -0,0 +1,135 @@ +@page custom_card_pictures Custom Card Pictures + +There are four ways to make Cockatrice use custom artwork for your cards: + +- Placing image files in the **CUSTOM pictures folder**. +- Providing a **custom card database** that points each printing at a picture URL via the `picurl` property. +- Writing your **own download URL templates**. +- Setting an **image override** for a single card from inside the deck editor. + +Each of these is described below. If pictures are missing or wrong, see @ref fixing_card_pictures instead. + +# Custom Pictures Folder (CUSTOM) + +Any image file placed in the CUSTOM folder is used as the card picture, and no download is attempted for cards that +match a file there. + +- The folder is `/CUSTOM/`. The pictures directory is configured on the 'General' settings tab, + under 'Directories' → 'Pictures directory'. +- Any image format Qt can decode is accepted (PNG, JPG/JPEG, WebP, GIF, BMP, ...); the file extension is not filtered, + so even an extension-less file is picked up if the decoder recognizes its content. +- Files are indexed by their name, so you can organize them into subfolders freely. +- New or changed files are picked up automatically within a few seconds — no client restart is required. + +The file name must match the card using one of the naming schemes below. Both `_` and `-` are accepted as separators, +and the file extension is ignored when matching: + +| Scheme | Example file name | +| --------------------------- | ------------------------------------------------------- | +| Card Name | `Example Card.png` | +| Card Name + Set | `Example Card_DDL.png` | +| Card Name + Set + Collector | `Example Card_DDL_43.png` | +| Set + Collector + Card Name | `DDL_43_Example Card.png` | +| Card Name + Provider ID | `Example Card_0b23cdc8-d413-4fb1-8470-474221b10fe2.png` | + +The name used for matching is the *corrected* card name. Correction removes the split-card separator ` // ` and the +characters reserved in Windows file names (`* < > : " \ ?` and control characters), and turns `/` into a space, so the +"Example // Card" card is matched by a file named `ExampleCard.png`, not `Example // Card.png`. Most other punctuation +(commas, apostrophes, `!`, ...) is left untouched. + +\attention A file in the CUSTOM folder always wins over downloaded pictures, even if it is the wrong image. Delete the +file if you want to see the downloaded artwork again. + +The naming conventions are the same as those recognized in the set-named subfolders and in `downloadedPics`, and are +documented for developers in @ref loading_card_pictures. + +# Custom Card Database (picurl) + +If you maintain your own card database (see the +[Custom Cards & Sets](https://github.com/Cockatrice/Cockatrice/wiki/Custom-Cards-&-Sets) wiki), each printing's `` +tag can carry a `picurl` attribute containing a full URL for that printing's picture: + +```xml + +``` + +Cockatrice tries this URL **before** the configured download URL templates, so it is the most direct way to provide +custom artwork for a specific printing. + +- The URL should start with `http://` or `https://`; the scheme is not validated, so make sure it is absolute or the + download may silently fail. +- When you change a `picurl` for a card whose picture was already downloaded and cached, delete the stored images + (Storage tab → 'Delete Saved Images' / 'Delete Cached Images') so Cockatrice fetches the new URL. + +# Custom Download URL Templates + +The built-in download URLs are templates: Cockatrice replaces reference points in the URL with information about the +card and its printing. You can write your own templates in 'Cockatrice → Settings' (Ctrl + Shift + P by default), on +the 'Deck Editor' tab, in the 'URL Download Priority' section. + +The following reference points are available: + +| Reference point | Description | Example | +| ------------------------------- | ------------------------------- | -------------- | +| `!name!` | Card name | `Example Card` | +| `!name_lower!` | Card name, lower case | `example card` | +| `!corrected_name!` | Corrected card name | `ExampleCard` (instead of "Example // Card") | +| `!corrected_name_lower!` | Corrected card name, lower case | `examplecard` | +| `!sflang!` | Scryfall language code for the current client language; defaults to English when the language has no localized images | `en`, `zhs` | +| `!setcode!` / `!setcode_lower!` | Set code | `EXM` / `exm` | +| `!setname!` / `!setname_lower!` | Full set name | `Exemplary Set` / `exemplary set` | +| `!set:!` | A property of this printing, e.g. `muid` (Gatherer multiverse ID), `uuid` (Scryfall UUID), `num` (collector number), `rarity` | `373549` | +| `!prop:!` | A property of the card, e.g. `side` (front/back), `colors`, `cmc`, `coloridentity`, `type`, `pt`, and the format legality statuses | `front` | + +The `!set:...!` and `!prop:...!` reference points support two modifiers: + +- `_fill_with_` pads the value with the given text, right-aligned, e.g. `!set:num_fill_with_000!` turns collector + number `1` into `001`. If the value is longer than the fill text, the template is skipped. +- `_substr__` extracts a substring, e.g. `!set:num_substr_2_2!` takes two characters starting at the + third. If the substring would extend past the end of the value, the template is skipped. + +Substituted values are URL-encoded. A template that asks for a property the card or printing does not have is skipped, +and the next template in the list is tried instead. + +\attention Custom URLs should start with `http://` or `https://`. As with `picurl`, the scheme is not validated before +the URL is handed to QNetworkAccessManager, so use an absolute URL or the download may silently fail. + +Some working examples: + +```text +https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg +https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side! +https://api.scryfall.com/cards/multiverse/!set:muid!?format=image +https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card +https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card +``` + +See the [Custom Picture Download URLs](https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs) +wiki for more examples and ideas. + +\attention Keep in mind that templates using `!name!` or `!set:muid!` resolve by name or multiverse ID, not by the +exact printing. Only the Scryfall `!set:uuid!` templates always return the exact printing requested. See +@ref fixing_card_pictures for more on this. + +# Image Overrides + +The quickest way to give one card custom art is an image override: right-click the card in the deck editor's printing +selector and open the **Image Overrides** submenu of the context menu. + +- **Load Custom Image...** — choose a picture file (the dialog suggests PNG, JPG/JPEG and WebP); it becomes that card's + artwork immediately. +- **One entry per alternate printing** of the card, labeled ` ` (hovering an entry previews that + printing's artwork). Selecting one makes the card use that exact printing's picture, so e.g. a basic land can be shown + with any of its artworks. +- **Clear Custom Image** — removes the stored override and returns the card to normal resolution. It is only available + while the card has a stored override. + +Overrides are stored as `.png` files in `/downloadedPics/`, under the "Naming scheme" configured on +the Storage settings page, and are matched the same way as downloaded images. Because local files are checked before any +URL is requested, an override always wins over downloaded artwork and `picurl` for that card. The override exists only on +the machine it was created on - it is not part of the deck file - so a card with a stored override shows normally on +another computer. + +\attention If you also keep a matching file in the CUSTOM folder, that file is matched before the override. When you +change an override, use **Clear Custom Image** so the stored `.png` is replaced; manually deleting the file in +`downloadedPics/` has the same effect. diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index 468a28f8d..b55d00fcd 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -11,6 +11,10 @@ - @subpage beta_release +## Card Pictures + +- @subpage custom_card_pictures + ## Troubleshooting - @subpage fixing_card_pictures diff --git a/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md b/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md index 78ba5586b..7f761c020 100644 --- a/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md +++ b/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md @@ -28,7 +28,8 @@ valid URLs. If you suspect the list has been modified or corrupted, press 'Reset defaults. For information on how to add your own custom URL templates, see the 'How to add a custom URL' link in the same -settings section. +settings section, or @ref custom_card_pictures for a full reference of the URL reference points, the CUSTOM +pictures folder, and custom card databases. # Check Your Local Picture Folder @@ -41,8 +42,12 @@ Cockatrice checks the following locations, in order: - The custom pictures folder (recursively indexed by file name). - `//` - `/downloadedPics//` +- `/downloadedPics/` (for export naming schemes without a set folder) -The following import naming schemes are recognized (using both `_` and `-` as separators): +A file only matches when its name without the extension equals one of the recognized scheme patterns exactly. + +The following import naming schemes are recognized (using both `_` and `-` as separators). The canonical table with +concrete example file names is on @ref custom_card_pictures: | Scheme | Pattern | | --------------------------- | -------------------------- | @@ -56,6 +61,10 @@ If a picture you downloaded or placed manually is wrong, stale, or corrupted, de attention to the `downloadedPics` subfolder: this is where the filesystem caching method writes downloaded images, and after a provider outage it can permanently contain the wrong printing until you delete it manually. +If a card persistently shows artwork you assigned yourself, you may have an **image override** set for it. Right-click +the card in the deck editor's printing selector and use 'Image Overrides' → 'Clear Custom Image' to remove it (or +delete the stored `.png` in `downloadedPics/`). See @ref custom_card_pictures for details. + See @ref loading_card_pictures for details on how local images are loaded. # Clear Caches