Methodology & Accuracy
Every tool on ThumbnailPro is powered by small, tested functions, and every claim in the site's copy should be checkable. This page documents how the tools actually work — including the parts other thumbnail sites gloss over: what happens for sizes YouTube never generated, why the downloads work at all, and where official rules end and conventions begin.
This page is the reference; the guides take the same material one question at a time. YouTube thumbnail sizes explained covers the 5 stored sizes, Why maxresdefault is missing the placeholder check, How the one-click download works the CORS mechanism, Safe areas explained and Aspect-ratio math the size guide and the calculator, On-device background removal the local models, and How to read the thumbnail tester the difference between a typical and a spec. Where a guide and this page overlap, this page is the canonical statement.
URL parsing
Every YouTube video is identified by an 11-character ID drawn from letters, digits, hyphen, and
underscore. The parser extracts that ID from the link forms people actually paste — standard
watch?v= URLs, youtu.be share links (with their tracking query strings),
Shorts, embed, /live/, and legacy /v/ paths, or a bare ID — and validates
it against the exact character set and length. It rejects, with specific explanations rather than
a generic error: playlist links (no single video), channel links (/channel/, /@handle, /c/, /user/), non-YouTube domains including lookalikes such as youtube.com.evil.example, and non-http schemes like javascript: that can smuggle a fake hostname past naive
checks. All of this is covered by an automated test matrix.
Thumbnail URLs and the placeholder-detection check
From a video ID, the 5 stored sizes live at predictable addresses under
https://img.youtube.com/vi/{id}/: default (120×90), mqdefault (320×180),
hqdefault (480×360), sddefault
(640×480), and maxresdefault (1280×720). The two largest exist only for some uploads. When a size is missing, YouTube answers with
HTTP 404 — but the response body is a gray 120×90 placeholder image,
which a plain <img> tag happily renders, and which careless tools happily serve
as a "download". ThumbnailPro checks both signals: the HTTP status of the fetch, and the decoded pixel
width of the returned image (asking for a 1280px
maxresdefault and decoding 120px means placeholder). Only sizes that
pass both checks get a download button; a missing size is labeled "not available for this
video", and a size whose bytes arrived with a success status but would not decode is labeled
"This size could not be decoded — try the direct link." and gets no button either. The
classification logic is a pure function with its own tests.
Why one-click downloads work (the CORS reality)
A web page can only read image bytes from another domain if that domain's server explicitly
allows it (CORS). We verified empirically that YouTube's image server sends
Access-Control-Allow-Origin: * on thumbnail responses, which is why this site can fetch
the image in your browser, verify it is not a placeholder, and hand you a real one-click download.
Your browser talks directly to YouTube's servers — the same requests it makes when you browse YouTube
— and this site's servers never see your link. If something in your environment blocks those fetches
(an aggressive extension, a corporate proxy that strips CORS headers), the tool degrades honestly:
it shows "Open image" links to the original YouTube URLs with a save instruction, rather than pretending
a download happened.
Preview sizes, safe areas: typicals, not specs
The tester renders thumbnails at true display sizes — 320×180 for a desktop home-grid card, 168×94 for a suggested-sidebar row, 246×138 for a search row, 160×90 for a mobile list row. These are honest current typicals of how video platforms size those surfaces; platforms shift them a few pixels with every redesign, so they are labeled as representative, never exact. The same goes for the safe-area checker: YouTube publishes no overlay specification, so its zones (duration badge bottom-right, hover controls top-right, progress bar along the bottom) model the convention at typical proportions and say so on their labels. Official figures — 16:9, 640px minimum width, the upload size limits — are a different category: those are cited on the size guide to Google's help documentation, verified against it on July 25, 2026.
The maker: a pure engine driving a canvas
The thumbnail maker is split in two. A pure, heavily tested engine owns every decision: the versioned document model (layers, z-order, immutable updates, JSON serialization with corrupt-data rejection), the transform geometry (rotated-rect hit tests, scale-from-handle math, snap guides), undo/redo history semantics, the deterministic layout templates, and the export planner. The browser layer only draws that document onto a canvas and forwards pointer and keyboard events. Serialization is round-trip tested, and a corrupted autosave is rejected with a fresh start — never rendered.
Background removal: a local model, verifiably local
The maker's subject cutout runs Google's open-source MediaPipe selfie-segmentation model (@mediapipe/tasks-vision 0.10.35, Apache-2.0) entirely in your browser, and the standalone
background remover's "Click the subject" mode adds Google's
MediaPipe MagicTouch interactive-segmentation model (also Apache-2.0), executed by the same
local runtime. This site hosts the WASM runtime and both .tflite models itself (249 KB
and 6.2 MB) under
/vendor/mediapipe/ — no CDN, no third-party request, and a provenance note with the source
URLs, versions, checksums, and full license text ships alongside the files. To verify the privacy
claim yourself: open your browser's DevTools network panel, import a photo, toggle the cutout, and
observe that the only requests are your own browser fetching the model files from this site — no image
bytes leave the page. Running WebAssembly required exactly one addition to this site's Content-Security-Policy:
'wasm-unsafe-eval' in script-src
(verified empirically against a server sending the production headers). If the model fails to load,
the toggle disables with a message and the rest of the editor keeps working.
The background remover's "Best quality" tier runs a third model the same local-first way:
BiRefNet_lite (Bilateral Reference for High-Resolution Dichotomous Image Segmentation, tiny
variant — upstream ZhengPeng7/BiRefNet_lite, MIT), as a 512×512 ONNX export with
4-bit-quantized weights, executed by onnxruntime-web 1.29.0 (MIT). The 512×512 input
is not a stylistic choice but a measured constraint: the full 1024×1024 export needs roughly 6 GB
of working memory at inference, which exceeds the ~4 GB WebAssembly heap every browser gives
a page — so the previous 1024 artifact failed for everyone, and the shipped one was verified to actually
run and to reproduce the reference model's masks. It executes on onnxruntime-web's WASM CPU provider
inside a worker thread (the WebGPU provider was tested and produced silently wrong output for this
model on the pinned runtime version, so it is deliberately not used — a quality tier that quietly
returns an empty mask would be worse than a slightly slower one that is right). Both the model and
the runtime are self-hosted under /vendor/birefnet/ and /vendor/onnxruntime/, each with a provenance note (source URLs, repository revisions, checksums, the full
derivation and verification chain — including mask-parity measurements against the official
full-resolution export — and license texts) shipped alongside the files, and a test suite pins
every vendored byte size and the runtime version so the notes cannot silently drift from what is
served. The same DevTools check applies: the only network requests are model and runtime files
from this site, and no image bytes ever leave the page.
Maker templates, fonts, and export honesty
The style templates are deterministic data — generated gradients, outlined text, arrows, badges, and labeled photo slots — pinned by tests that assert each is structurally valid and distinct. They are not AI, and their descriptions state what each layout does rather than promising click-through. The three fonts (Archivo Black, Inter, Baloo 2) are SIL Open Font License releases, self-hosted as pinned-version subsets with license texts committed in the repository. Exports respect a user-chosen size cap (2 MB by default — YouTube's mobile limit, per the size guide's citation of Google's help documentation, verified July 25, 2026): JPG encoding binary-searches quality until the file fits (the same tested planner approach used across the Sumvia network), an unreachable cap is reported instead of ignored, and every export is re-decoded and dimension-checked before the download is offered, with the real byte size displayed.
Trademark and copyright stance
The preview mockups are deliberately generic — neutral grays, no YouTube logo, no cloned iconography — because YouTube's interface and branding are YouTube's property, and imitating them is neither necessary nor honest. Thumbnails themselves are the creators' artwork: the downloader exists for recovering your own work, backups, and research, and the site says so wherever downloads happen. ThumbnailPro is not affiliated with or endorsed by YouTube or Google.
Privacy: uploads never leave the browser
Images you load into the tester or the safe-area checker are read with local JavaScript, displayed from in-memory object URLs, and discarded when you close the page. There is no upload endpoint on this site. Links you paste into the downloader are parsed locally; the only network requests are your own browser fetching images from YouTube. You can watch all of this in your browser's network inspector — that is the level of verifiability we aim for.
Worked examples can't drift
The figures quoted in page copy — the derived thumbnail URL on the downloader page, the 1280→720 and 1000→562.5 examples on the aspect calculator — are computed at build time by the same engines that power the interactive tools. If the logic changed, the copy would change with it or the build would fail, so the documentation cannot silently disagree with the tools.
Worked figures
The illustrations below each make that claim concrete: every value in them was produced on this
page at build time by the engine named in the heading, and none was typed in. The tool pages
print the engines' answers to the questions a visitor brings; the tables here are the ones a
reference needs — the complete placeholder decision, every rejection the parser can produce, the
rules it applies before deciding, the inverse of the rounding example — rather than the typical
case, and where a sentence names a count, a code, or a verdict, the build asserts it. The video
ID
jNQXAC9IVRw is the illustrative one the URL engine's own documentation uses; the derivation
is identical for any valid ID.
thumbnailSet('jNQXAC9IVRw')
| Key | Pixels | Generated for | URL |
|---|---|---|---|
default | 120×90 | effectively every public video | https://img.youtube.com/vi/jNQXAC9IVRw/default.jpg |
mqdefault | 320×180 | effectively every public video | https://img.youtube.com/vi/jNQXAC9IVRw/mqdefault.jpg |
hqdefault | 480×360 | effectively every public video | https://img.youtube.com/vi/jNQXAC9IVRw/hqdefault.jpg |
sddefault | 640×480 | some uploads only | https://img.youtube.com/vi/jNQXAC9IVRw/sddefault.jpg |
maxresdefault | 1280×720 | some uploads only | https://img.youtube.com/vi/jNQXAC9IVRw/maxresdefault.jpg |
A download of the largest size would be saved as youtube-jNQXAC9IVRw-maxresdefault-1280x720.jpg — the ID, the size
key, and the pixel dimensions are all in the name, so files from different videos and sizes cannot
collide. Note that the smallest size, default, is
120×90 — the same dimensions as the gray placeholder, an equality the
build asserts rather than assumes — which is why the classification treats a small decode at
default as legitimate and only flags it as a placeholder for the larger sizes.
classifyThumbnail(size, probe) — the complete decision grid
The probe is what the browser learned about one thumbnail URL: whether the fetch's status was a success, and the pixel width the bytes decoded to — or nothing, if they would not decode. The size guide and the downloader page each show the verdict for the situations a visitor meets. This grid is exhaustive instead: every stored size against a failed status, a body that would not decode, and a successful decode at every width the engine knows — the placeholder's and each stored size's own. Each cell is the function's return value.
| Requested size | Status failed | Status ok, no decode | Status ok, 120 px decoded | Status ok, 320 px decoded | Status ok, 480 px decoded | Status ok, 640 px decoded | Status ok, 1280 px decoded |
|---|---|---|---|---|---|---|---|
default (120×90)
| missing | undecodable | available | available | available | available | available |
mqdefault (320×180)
| missing | undecodable | missing | available | available | available | available |
hqdefault (480×360)
| missing | undecodable | missing | available | available | available | available |
sddefault (640×480)
| missing | undecodable | missing | available | available | available | available |
maxresdefault (1280×720)
| missing | undecodable | missing | available | available | available | available |
The grid reads as three rules applied in order. The status check runs first and alone: a failed
status is missing for every size, including default,
and the decoded width is never consulted. Second, with a success status but no decodable image,
every size is undecodable — a separate word from missing, kept so that a broken
file is never reported as a size YouTube did not generate; the downloader draws no preview or button
for such a card. Third, with a success status and a decoded width, the rule is "narrower than requested
and no wider than the placeholder". Of the 25 decode cells, exactly
4 read missing, all in the 120-px
column, one for each of the 4 sizes wider than the placeholder; the same column
reads available for default, because 120
is that size's real width, which is why the rule compares against the requested size rather than a
fixed number. Every other decode cell is available — including decodes narrower
than requested but wider than the placeholder, and decodes wider than requested. The function detects
the placeholder; it does not audit dimensions, and the grid shows the whole of that boundary. Only
available earns a download button; every other verdict produces a labeled card and
no file.
parseYouTubeInput(text) — every rejection the engine can produce
A rejected paste comes back as a Result carrying two things: a code drawn from a closed
list, so the interface can tell the rejection classes apart, and a plain-text message it can print
exactly as received. The list of codes is fixed in the type system, which means a new class of rejection
cannot appear without a new code, and every message is written once, in the engine — the field under
the downloader shows it word for word. The downloader page prints a sampler of the links people paste;
the table here holds one row for each message the parser can return: 18 inputs producing
17 distinct messages under
6 codes, and the code in every row was asserted when this page was built. 4 of those rows — an empty /shorts/, /embed/, /live/, or
/v/ path — come from one template in the engine with the path name filled in; each is
listed on its own because each is a distinct string the field under the downloader can show.
| Code | Input | What was pasted | Message, word for word |
|---|---|---|---|
empty-input | | Nothing, or spaces only | Paste a YouTube video link or an 11-character video ID. |
not-a-youtube-url | the video at youtu.be/jNQXAC9IVRw | A sentence with a link inside it | That does not look like a YouTube link or video ID — check for stray spaces. |
not-a-youtube-url | https://youtube.com:video/watch?v=jNQXAC9IVRw | A colon where a slash belongs | That could not be read as a web address or a video ID. |
not-a-youtube-url | data://www.youtube.com/watch?v=jNQXAC9IVRw | A data: scheme carrying a YouTube hostname | Only http(s) YouTube links are supported. |
not-a-youtube-url | https://www.youtube.com@evil.example/watch?v=jNQXAC9IVRw | youtube.com before an @ — the real host is after it | That is not a YouTube link. This tool reads youtube.com and youtu.be URLs (or a bare video ID). |
not-a-youtube-url | jNQXAC9IVR | A bare ID one character short | That is not a YouTube link. This tool reads youtube.com and youtu.be URLs (or a bare video ID). |
channel-url | https://www.youtube.com/user/examplechannel/videos | Legacy /user/ channel path | That is a channel link, not a video. Open one of the channel’s videos and paste that URL. |
playlist-url | https://www.youtube.com/playlist?list=PLexampleplaylist | Playlist page | That is a playlist link, not a single video. Open a video in the playlist and paste its URL. |
playlist-url | https://www.youtube.com/watch?list=PLexampleplaylist | Watch page with list= but no v= | That watch link points at a playlist without a specific video (no v= parameter). Open the video itself and paste its URL. |
playlist-url | https://www.youtube.com/embed/videoseries?list=PLexampleplaylist | Embed of a playlist (videoseries) | That embed link plays a playlist (videoseries), not a single video. |
unsupported-url | youtu.be | youtu.be with nothing after the host | That youtu.be link has no video ID after the slash — copy the full share link. |
unsupported-url | https://www.youtube.com/watch?V=jNQXAC9IVRw | Watch page with the parameter in capitals (V=) | That watch link is missing its v= video parameter — copy the URL from the video page. |
unsupported-url | https://www.youtube.com/shorts/ | A /shorts/ path with nothing after it | That /shorts/ link has no video ID after it. |
unsupported-url | https://www.youtube.com/embed/ | An /embed/ path with nothing after it | That /embed/ link has no video ID after it. |
unsupported-url | https://www.youtube.com/live/ | A /live/ path with nothing after it | That /live/ link has no video ID after it. |
unsupported-url | https://www.youtube.com/v/ | A legacy /v/ path with nothing after it | That /v/ link has no video ID after it. |
unsupported-url | https://www.youtube.com/feed/subscriptions | A YouTube page that is not a video | That YouTube link is not a video page this tool recognizes — paste a watch, youtu.be, Shorts, embed, or live URL. |
invalid-video-id | https://www.youtube.com/watch?v=jNQXAC9IV.w | An ID with a character outside the set | That link does not contain a valid video ID — YouTube IDs are exactly 11 letters, digits, hyphens, or underscores. |
Three rows show where the order of the checks decides the answer. The parser tries the text as a
bare ID first and as an address second; a bare ID that is one character short fails the pattern
and is therefore read as an address, so it comes back as not-a-youtube-url — its "hostname"
is not on the list — rather than as a malformed ID. The message still points at the fix. An address
with youtube.com before an @ is handled by the browser's own URL parser
before the hostname check runs, and that parser treats everything before the
@ as a username: the host it reports is the part after it, which is why the row reads
not-a-youtube-url and the lookalike never reaches the path checks. The capital
V= row is one of the places the parser is case-sensitive: the first path segment is lowercased
before it is matched and the hostname is lowercased by the URL parser, but query-string keys are compared
exactly — as is the embed playlist marker videoseries — so
V= is not v= and the link comes back as
unsupported-url with the message that names the missing parameter. The rejection classes
themselves are pinned by the automated suite described at the end of this page.
parseYouTubeInput(text) — what is normalized before the checks
The accepted-forms table on the downloader page covers the links people paste. The rows here are the awkward variants of those links, and each resolves to the same ID — the build checks that it does:
| Variant | Input | ID extracted |
|---|---|---|
| Spaces before and after the link | https://www.youtube.com/watch?v=jNQXAC9IVRw | jNQXAC9IVRw |
| Host in capitals | https://WWW.YOUTUBE.COM/watch?v=jNQXAC9IVRw | jNQXAC9IVRw |
| Path in capitals | https://www.youtube.com/WATCH?v=jNQXAC9IVRw | jNQXAC9IVRw |
| Plain http scheme | http://youtu.be/jNQXAC9IVRw | jNQXAC9IVRw |
| Protocol-relative share link | //youtu.be/jNQXAC9IVRw | jNQXAC9IVRw |
| A fragment after the ID | https://www.youtube.com/watch?v=jNQXAC9IVRw#t=30 | jNQXAC9IVRw |
| Playlist context and a video | https://www.youtube.com/watch?list=PLexampleplaylist&v=jNQXAC9IVRw | jNQXAC9IVRw |
| A segment after the ID | https://www.youtube.com/live/jNQXAC9IVRw/extra | jNQXAC9IVRw |
| The music subdomain | https://music.youtube.com/watch?v=jNQXAC9IVRw | jNQXAC9IVRw |
Read together with the rejection table, these rows fix the parser's rules in one place.
Whitespace at the ends is trimmed; whitespace inside is fatal. The hostname and the first path
segment are matched without regard to case; a query key is not. A missing scheme is taken as
https, a protocol-relative // likewise, and plain http is accepted
— the scheme check exists to exclude javascript: and data:, not to
insist on a secure link. A fragment, extra parameters, and extra path segments after the ID are
ignored, because the ID is all that is needed; and list= alongside v= is
a video that happens to sit in a playlist, whereas list= alone is a playlist, which is
the whole difference between this table's row and the rejection table's. None of this consults the
network: the parser says whether the text names a video ID, not whether that video exists, and it
finishes before any image request is made.
heightForWidth(width, 16:9) and its inverse, with the rounding flag
| Width | Exact | Rounded pixel | isExact |
|---|---|---|---|
| 1280 | 720 | 720 | true |
| 1000 | 562.5 | 563 | false |
The flag is what the calculator reads to decide whether to say "rounded" next to a result: the
first row is whole, so nothing is added; the second is not, so both the exact value and the
nearest whole pixel are shown. A width divisible by 16 always lands on a whole pixel at
16:9.
The flag also records something the rounded number cannot: rounding is one-way. Feed the whole
result back into widthForHeight and the round trip closes — 720 tall at
16:9 gives 1280, isExact true. Feed the rounded result back and it does not:
563 tall gives 1000.89, reported as
1001 with isExact false, which is not the 1000 that produced it.
describeRatio says the same thing from the other side: 1000×563 simplifies to 1000:563, and is16x9 is false, because the test is integer arithmetic — width × 9 must equal height × 16 — and a rounded pixel has already broken the equality. That is why no tool on this site stores a
rounded pair as if it were the ratio: the calculator shows the exact value beside the pixel, and the
tester and the size checker report such a pair as not 16:9, with the crop it implies, rather than
calling it close enough. The crop table at the end of this section puts a figure on that pair.
overlayZones(1280, 720) — typical zones, not a spec
Zone x y Width Height Duration badge (typical) 998 619 282 101 Hover controls (typical) 1075 0 205 216 Watched-progress bar (typical) 0 684 1280 36
Coordinates are pixels from the image's top-left corner, so a zone whose x plus width equals
1280 hugs the right edge, and one whose y plus height equals 720 hugs the bottom.
The rectangles are fixed fractions of the frame, rounded to whole pixels, so they scale with the image.
They model the convention at typical proportions — YouTube publishes no overlay specification — which
is why every label carries the word "typical".
coverCropReport(width, height) for common export sizes
A 16:9 slot in the tester is a cover crop: the image is scaled until it fills the slot and
whatever overflows is trimmed. The report says which axis overflows and by how much.
Image Simplified ratio Fits 16:9 Cropped axis Cropped 1280×720 (maker preset)
16:9 yes — nothing 1080×1920 (maker preset)
9:16 no height 68.4% 1080×1080 (maker preset)
1:1 no height 43.8% 640×480 (stored sddefault)
4:3 no height 25% 1680×720 (21:9 banner at the maxresdefault height)
7:3 no width 23.8% 1000×563 (rounded 16:9 result from the table above)
1000:563 no height 0.1%
An image that is exactly 16:9 fits with nothing lost — the 1280×720 row. An image taller than 16:9 is scaled to the slot's width, so its extra height is cut
from the top and bottom: the vertical preset and the square both lose height, by very different amounts. An image wider than 16:9 is scaled to the slot's height and
loses width instead — the 21:9 banner gives up 23.8% of its width from the sides. The sddefault row is the stored
4:3 frame: it is a size YouTube serves back, and it still loses 25% of its height in a 16:9 slot, which is why the sizes guide keeps "served
back" apart from whether a size fits the player. The last row is the rounded calculator result from
the aspect table:
1000×563 is not exactly 16:9, so the report flags a crop,
but the amount is 0.1% — which is how a rounding flag and a crop report
can both be right about the same image. The report is geometry only. It states which pixels a
slot would trim; it says nothing about how anyone would respond to the result, and the tester's
page says so.
Tested against real behavior
The automated suite pins the URL-parser matrix (every accepted form, every rejection class,
hostile inputs), the thumbnail-set derivation, the placeholder classification, the aspect
arithmetic against hand-computed values, and the safe-area geometry. If you believe a result is
wrong — especially a size labeled available that was not, or the reverse — that is exactly the
report we want: see the contact page. Confirmed issues are fixed in the
engine and locked in with a new test.
Last reviewed: .