UUID, QR Code and Base64: Where Three Generators Stop Telling You They Failed
Three pages that make things: identifiers, codes, encodings. They are the simplest of the toolset in one respect — none of them parses your input into a different model — and the trickiest in another, because when they run out of room or out of options they do not say so. One of them will hand you a blank image and a filename.
Everything below was measured by driving the pages in a browser: 2,000 UUIDs checked against the RFC 4122 shape, capacities found by searching each error-correction level for the longest string that still renders, and a set of synthetic files handed to the Base64 reader including an empty one and one with no extension.
The UUID page is as random as your browser, and no more clever
UUID Generator builds each character independently:
var arr = new Uint8Array(1);
crypto.getRandomValues(arr);
uuid += hex[arr[0] & 0x0f];
Two things follow from that. The entropy source is the browser's cryptographically secure generator, not Math.random(), so these are safe to use as secrets — an API key, a nonce, an id you do not want guessable. And the masking is & 0x0f, sixteen possible values taken from 256, which divides evenly: there is no modulo bias, no digit is slightly more likely than another. It asks for one random byte per output character, so a single UUID costs 32 draws from the CSPRNG. That is slightly wasteful and completely harmless.
The two fixed positions are what make it a v4:
- position 14 is hard-coded
4— the version nibble; - position 19 is
(arr[0] & 0x3) | 0x8— the variant nibble, which by RFC 4122 must be8,9,aorb.
Measured over 2,000 generated UUIDs: {8: 515, 9: 497, a: 523, b: 465}, with zero failing the canonical xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx pattern. Four buckets, roughly 500 each — the variant bits are uniform, and so is everything else.
If you are validating these with a strict regex somewhere downstream, that [89ab] is the part people get wrong, and it is the part this page gets right.
The count box has two defaults
The number field is <input type="number" min="1" max="100" value="10">, but the attributes are only advisory — the spinner arrows respect them, typing does not. The script decides:
var count = parseInt(document.getElementById('uuidCount').value, 10) || 10;
if (count < 1) count = 1;
if (count > 100) count = 100;
Measured, pressing Generate with each value typed in:
| You type | Rows you get | Because |
|---|---|---|
101 |
100 | clamped down |
-5 |
1 | clamped up |
2.7 |
2 | parseInt stops at the dot |
0 |
10 | 0 is falsy, so \|\| 10 fires |
| (empty) | 10 | NaN is falsy |
abc |
10 | NaN again |
That middle row is the one that surprises people: zero does not mean zero, and neither does anything the browser cannot read as a number. There is no message telling you the field was invalid — you just get the default ten. If you wanted one UUID, -5 gets it; 0 does not.
Your format checkboxes are not live
Two options sit above the list: Uppercase and With hyphens. Both carry checked in the markup, so the first ten ids you see are upper-case and hyphenated — you have to untick something to get any other shape. Both are read inside generateUUIDs() and nowhere else. The count field, and both checkboxes, have no change or input listener — the page contains zero addEventListener calls, verified on the live file. So:
- generate 10 ids, untick Uppercase, and the list you are looking at stays upper-case;
- untick With hyphens and nothing re-renders;
- change the count from 10 to 5 and the 10 rows remain until you press Generate again.
This is not a bug you can see, which is why it is worth writing down: press Generate after touching an option. Toggling and then copying the visible rows gives you the old settings.
The two options themselves behave exactly as advertised, once Generate runs. Uppercase is uuid.toUpperCase() on the finished string, so 36 characters either way. Unticking With hyphens makes positions 8, 13, 18 and 23 of the loop emit an empty string while the loop still runs its full 36 — measured length 32, hex digits only, no spaces. Untick both and you get 32 lower-case characters, still a valid v4 in compact form: the hard-coded 4 lands as the 13th hex digit and the variant nibble as the 17th, which is exactly where RFC 4122 puts them.
Pressing Generate wipes the list first (list.innerHTML = ''), so there is no way to build a batch of 300 across three clicks; the previous screenful is gone. Uniqueness across clicks is not something to worry about — with 122 random bits, drawing duplicates is not a practical risk — but nothing on the page checks it either.
Copy is per row, via navigator.clipboard.writeText(), and the button text swaps to ✓ for 800 ms. There is a Copy All button above the list. One cosmetic catch worth knowing if you are on a phone: each row's copy button is styled opacity: 0 and only becomes visible on hover, so on a touch screen the buttons are invisible — they still exist and still occupy the row, you just cannot see where to tap.
The QR generator has no error path at all
QR Code Generator redraws on every keystroke (oninput="generateQR()"), and the two selects — size and error correction — also call it directly. generateQR() also runs once at the end of the script, but the textarea starts empty, so that first run only draws the Enter content to generate QR code placeholder — a code appears once you type something. The page does not fill in its own URL either: to get a code for the page you are on, copy the address into the box yourself. The controls:
- Size: Small 160 px, Medium 240 px (default), Large 320 px, Extra Large 400 px.
- Error correction: L 7%, M 15% (default), Q 25%, H 30%.
Size and capacity are unrelated, and this is the misunderstanding that gets people into trouble. The pixel choice only decides how big each module (dot) is drawn; how much text a code can hold is decided by the QR version and the error-correction level. The page will not tell you this, because it will not tell you anything at all when the text does not fit.
Measured by searching each level for the longest ASCII string that still renders:
| Level | Longest ASCII text |
|---|---|
| L | 2,950 characters |
| M | 2,328 characters |
| Q | 1,660 characters |
| H | 1,270 characters |
And the same search in Chinese at level M gives 776 characters — 776 × 3 = 2,328, exactly the ASCII ceiling. So the limit is counted in UTF-8 bytes, not characters. A URL-encoded link with Chinese in it is three times as expensive as it looks, and an emoji costs four.
Now the part that matters. When your text exceeds the level's ceiling, there is no message. The drawing library throws an internal error (TypeError: Cannot read properties of undefined (reading '3') at H, (reading '0') at L) and the page never hears about it. What you are left holding, measured:
- the container shrinks to 95 characters of markup — a
<canvas width="240" height="240">and an<img>with nosrc; - the canvas has 0 non-transparent pixels out of 57,600;
- the Download button remains enabled and works. It calls
canvas.toDataURL()on that empty canvas and savesqrcode.png— a structurally valid PNG whose data URL runs 2,786 characters, i.e. a 2,072-byte file of nothing but transparent pixels.
So the failure is not merely silent, it is satisfying: the download completes, the file opens, the image viewer shows a white square. If you ever receive a QR PNG that displays nothing, this is what happened to it.
The practical rules: if nothing appears, your text is too long for that level — switch to L, not to a bigger pixel size. And look at the preview before you download, because the button gives no signal that the canvas is empty. If you are choosing a size for a printed code, remember it only changes how large each module is drawn, so 320 or 400 px is the comfortable choice for print.
Base64 trusts your browser's guess about the file
Image to Base64 takes one file — the input is accept="image/*" with multiple off — and prints either a full data URI or raw Base64 (the first option, and therefore the default, is the full data URI). Unlike the UUID page's dead options, this select is live: addEventListener('change', updateOutput) re-reads the cached data URI, so you can upload once and flip between the two forms as many times as you like without re-picking the file.
The MIME type in the output comes from file.type, which is what your operating system and browser believe the file is. Nothing inspects the bytes. Measured with synthetic files:
| File | file.type |
What the page outputs |
|---|---|---|
a.png, 8 bytes of anything |
image/png |
data:image/png;base64,iVBORw0KGgo= |
note.txt, 5 bytes |
text/plain |
data:text/plain;base64,aGVsbG8= |
doc.pdf |
application/pdf |
data:application/pdf;base64,JVBERi0xLjQ= |
mystery (no extension) |
(empty) | data:application/octet-stream;base64,eHl6 |
a.svg |
image/svg+xml |
data:image/svg+xml;base64,PHN2Zy… |
empty.bin, 0 bytes |
application/octet-stream |
data:application/octet-stream;base64, |
Two consequences. The type is a label, not a check — a file named .png that contains a PDF gets a data:image/png: prefix, and the browser will then refuse to display it while the page cheerfully reports success. And a file with no recognised type still gets encoded, with application/octet-stream as the fallback, which is the correct guess but also means the output is no longer an "image" data URI.
The preview box sits under the output and is only useful for actual images. What the page does for anything else, measured: it adds the show class to the preview element and sets its src to the data URI, so the box looks armed. For the PNG stub, the text file, the PDF and the empty file, the image's natural size is 0×0 — nothing renders. Only the real SVG produced a picture (300×150). An empty file is a special case of its own: data:application/octet-stream;base64, with nothing after the comma, and the info line reading empty.bin | 0 B.
The size label has a quirk worth knowing before you encode something large. The formatter switches units at 1 KB and 1 MB and never again:
| Bytes | Shown as |
|---|---|
| 0 | 0 B |
| 1,023 | 1023 B |
| 1,024 | 1.0 KB |
| 1,536 | 1.5 KB |
| 1,048,575 | 1024.0 KB |
| 1,048,576 | 1.00 MB |
| 1,073,741,824 (1 GiB) | 1024.00 MB |
| 5,368,709,120 (5 GiB) | 5120.00 MB |
So there is no unit above megabytes, and one gigabyte announces itself as 1024 MB. The decimal places change too — one for KB, two for MB. It is a cosmetic thing, but it is also the only size feedback you get, and a large file here is a large string: Base64 costs about four thirds of the original, and the whole thing is held in memory as text in your tab. Nothing is uploaded — there is no request anywhere in the page's script — but a 20 MB image is a 27 MB string in a <textarea>, and the tab pays for it.
Clearing resets both boxes and empties the preview, and leaves your format choice alone: measured after Clear, the format select still said raw. Copying an empty box gives an alert — Please upload an image first. That is one of exactly two on-the-record complaints the three pages make: the other is the QR page's Please generate a QR code first, and it fires only when the textarea is empty, because that is the only state in which no canvas exists. Text that exceeds the level's capacity gets neither.
Which page for which job
| You need | Use | Watch out for |
|---|---|---|
| Identifiers that must not be guessable | uuid-generator | press Generate after changing an option |
| Exactly one UUID | uuid-generator with -5 |
0 and abc both give you ten |
| A code for a short link | qrcode-generator at M | nothing |
| A code for a long URL | qrcode-generator at L | the pixel size will not help |
| A QR to print | qrcode-generator, 320 or 400 px | check it renders before downloading |
| An image inlined in CSS or HTML | image base64, Full Data URI | the MIME type is a guess, not a check |
| Base64 for an API body | image base64, Raw Base64 | Clear keeps this setting, so it may already be set |
FAQ
Are these UUIDs safe to use as tokens?
Yes. They come from crypto.getRandomValues, the browser's cryptographically secure source, and each of the 30 random characters is masked from a full byte with no modulo bias. They are v4 in shape: measured 2,000 out of 2,000 matched the RFC pattern, and the variant nibble landed in all four legal values.
Why did the page ignore my untick? Because the options are only read when Generate runs. There is no listener on the checkboxes or the count field, so an existing list never re-renders. Both boxes start ticked, so the very first list is upper-case with hyphens; untick one and press Generate.
The page opens with no code. Is it broken?
No. The textarea starts empty, so the generateQR() call at the end of the script draws only the Enter content to generate QR code placeholder; you get a code once there is something to encode. It will not fill in the current page's URL either — copy it into the box yourself.
I asked for 0 UUIDs and got 10. Which is right?
Ten, by design. The script is parseInt(value, 10) || 10, and zero is falsy, so it takes the default. Use -5 if you want the minimum of one.
My QR downloaded a blank white square. Is the file corrupt? The file is fine and the code was never drawn. Your text exceeded the error-correction level's byte capacity, the drawing library threw, and the page caught nothing — the Download button exported the empty canvas. Switch to level L or shorten the text; a larger pixel size changes nothing.
How much text really fits? 2,950 ASCII characters at L, 2,328 at M, 1,660 at Q, 1,270 at H — all measured. Count UTF-8 bytes instead: Chinese cost exactly three times as much as ASCII in the same test (776 characters at M).
Why does my Base64 image not preview? Check what you uploaded. The page marks the preview as shown for any file, but only a real image has non-zero dimensions — a PNG stub, a text file and a PDF all reported 0×0 while still producing output text. The encoding itself succeeded.
Can I convert a PDF or a font with the Base64 page?
Yes, by dropping it on the zone — the accept="image/*" filter applies to the file picker, not to the drop handler, and the script does not care about the extension. It will just be labelled by whatever MIME type your browser infers, or application/octet-stream.
Does any of this leave my machine?
Your content does. The UUID page uses your browser's entropy pool, the QR page draws the code locally, and the Base64 page reads the file with FileReader in the same tab; none of the three sends your input, text or file anywhere, and none of the three makes a request from its own script. One honest exception: the QR page loads its drawing library from cdn.jsdelivr.net (qrcodejs@1.0.0, served without an integrity attribute), so opening that page is itself a network request to a third party — your text is not in it, but your IP address is. For the Base64 page the cost is memory rather than privacy: the entire encoding lives in the page as a string.