QR codes and subtitle files look unrelated, but they share one trait: both are tiny interchange formats where a single wrong character makes the other device give up. A Wi-Fi QR code whose password contains an unescaped semicolon does nothing when scanned. An SRT file with a dot where the comma should be can make a player drop the cue entirely.
This guide skips the tour of the interface and focuses on where things actually break: how to write the payload, how capacity and error correction trade off, how to choose size and quiet zone; and for subtitles, how the three formats differ in timestamp syntax, what is lost in conversion, and how to find and fix timing problems. Everything runs in the browser, so Wi-Fi passwords, unreleased video and subtitle drafts are not uploaded anywhere.
1. Decide what goes inside the QR code before styling it
A QR code only encodes a string as black and white modules. After the scanner decodes the string, the phone decides what to do with it, such as opening a page, joining Wi-Fi or saving a contact, based on conventions in the text itself. So the order is: get the text right first, then adjust size, color and error correction. The ToolGarden generator accepts any text and does not build Wi-Fi or contact payloads for you, so verifying the text is your job.
| Purpose | Payload | Common mistake |
|---|---|---|
| Web page | https://example.com/promo?id=42 | Leaving out https://, so the scan shows plain text instead of a link |
| Wi-Fi | WIFI:T:WPA;S:NetworkName;P:password;; | Not backslash-escaping ; , : \ " in the name or password; open networks should use T:nopass |
| Phone | tel:+15551234567 | Plain digits, which some cameras show as text only |
| mailto:hi@example.com?subject=Hello | Spaces and non-ASCII characters in the subject are not URL-encoded | |
| Contact | BEGIN:VCARD … END:VCARD | Line breaks swallowed; every field must sit on its own line |
Wi-Fi escaping, the trap most people fall into
The Wi-Fi payload separates fields with semicolons, so a backslash, semicolon, comma, colon or double quote inside the network name or password must be preceded by a backslash. If the SSID or password happens to consist only of hex characters (for example 1234abcd), some parsers treat it as a hex string, and the usual workaround is to wrap it in double quotes. Hidden networks add an H:true field.
# Ordinary WPA network
WIFI:T:WPA;S:Meeting-Room;P:abc12345;;
# Password contains a semicolon and a backslash: the real password is pa;ss\word
WIFI:T:WPA;S:Meeting-Room;P:pa\;ss\\word;;
# Open network (no password)
WIFI:T:nopass;S:Guest;;
# Hidden network
WIFI:T:WPA;S:Hidden-Net;P:abc12345;H:true;;2. Capacity, error correction and size: making it scan for real
QR codes come in 40 versions. Version N has a side of 17 + 4N modules (version 1 is 21×21, version 40 is 177×177). Longer content needs a higher version, which means more modules, and at a fixed print size each module gets smaller. Error correction levels L, M, Q and H can recover roughly 7%, 15%, 25% and 30% of damaged data, at the price of more modules for the same content. Byte-mode capacities for a few common versions (in bytes) let you estimate where your content lands.
| Version (side) | L | M | Q | H |
|---|---|---|---|---|
| 1 (21×21) | 17 | 14 | 11 | 7 |
| 3 (29×29) | 53 | 42 | 32 | 24 |
| 5 (37×37) | 106 | 84 | 62 | 44 |
| 7 (45×45) | 154 | 122 | 86 | 64 |
| 10 (57×57) | 271 | 213 | 151 | 119 |
Capacity is counted in UTF-8 bytes, so each CJK character costs 3 bytes and pushes the version up faster than the same number of Latin letters. The result panel shows the input byte count, so you can look it up in the table directly.
Worked example: a long tracking URL versus a short link
Say you want a poster QR code for https://example.com/spring-sale?utm_source=poster&utm_medium=qr&utm_campaign=2026spring, which is 87 bytes. At the default M level, version 5 holds 84 bytes, which is not enough, so the code moves up to version 6 (41×41). A 28-byte short link, https://example.com/s/spring, does not fit in version 2 at M (26 bytes), so it lands on version 3 (29×29). The module count is exactly half (841 versus 1681). The generator defaults to 320 px output with a 2-module quiet zone: the 41×41 code plus quiet zone is 45 modules across, about 7 px per module, while the 29×29 code is 33 modules across, about 9.7 px per module. On the same poster area, the short link gives larger modules that are easier to focus on from a distance.
Size, quiet zone and scanning distance
- Size: the generator accepts 160 to 1024 px, default 320. 240 px or more is usually enough on screen; for print, export 1024 px and scale down in your layout software, which is much sharper than enlarging a small image.
- Quiet zone: the specification calls for a blank border four modules wide, while the generator defaults to 2 (adjustable from 0 to 8). For print, or for a code on a dark background, raise it to 4. A quiet zone invaded by artwork, borders or neighboring text is the most common reason a code scans on screen but not on paper.
- Contrast: the generator draws near-black modules (#111111) on white, which is already maximum contrast. Do not invert it onto a dark background; some scanners handle inverted codes unreliably.
- Distance: a common rule of thumb is a scanning distance of about ten times the code width. A 2 cm code is comfortable at around 20 cm; for signage, work backwards from viewing distance.
What a center logo costs you
When you upload a logo, the generator switches error correction to H automatically. The white rounded box behind the logo is 28% of the code side, about 7.8% of the area (0.28² ≈ 0.078), and the logo itself is 22% of the side. The hidden data is covered by the roughly 30% redundancy of level H, so do not expect to enlarge the logo further. More importantly, level H raises the version for the same content: a 40-byte URL is version 3 (29×29) at M but version 5 (37×37) at H, so modules shrink and scanning distance drops. If the code is going on a small label, skip the logo.
3. Decoding and verifying: let a second device read it before you print
The QR Code Decoder accepts PNG, JPG, WebP, GIF and BMP images (up to 12 MB and 24 million pixels each) or a live camera feed. Both are decoded locally with jsQR; images and video frames are not sent anywhere, and inverted patterns are tried as well. It helps in three ways.
- Pre-print verification: drop in the PNG you are about to send to the printer and check that the decoded text matches what you typed character for character, especially Wi-Fi escaping and URL parameters.
- Inspecting unknown codes safely: read the text first and decide whether to visit it, instead of letting the phone camera jump straight to a link. Shortened links and long tracking URLs become obvious.
- Locating “it won’t scan”: drop in a photo taken on site. If the tool cannot decode it either, the problem is usually the image (glare, tilt, tiny modules, invaded quiet zone). If the tool decodes it but the phone does not, suspect the payload (Wi-Fi escaping, a scheme the OS does not handle).
| Symptom | Most likely cause | How to check |
|---|---|---|
| Scans on screen, not on paper | Quiet zone invaded, low print resolution, ink bleed | Export 1024 px, set quiet zone to 4, photograph the proof and decode it |
| Scan returns garbled text | Content truncated or mis-encoded | Compare the decoded text with the original; check the byte count against version capacity |
| Wi-Fi code does nothing | Header is not WIFI:, or special characters are unescaped | Decode it and inspect each field for semicolons and backslashes |
| Scans sometimes after adding a logo | Logo area uses up the H-level redundancy | Use a smaller logo, or drop it and shorten the content |
| Fails far away, works close up | Modules too small | Shorten the content (use a short link) or print larger |
4. Subtitles: how SRT, VTT and LRC differ
All three are plain text. They differ in how timestamps are written and what extra information they can carry. Knowing those differences explains what is lost in conversion and why a player might reject a file.
| Item | SRT | WebVTT | LRC |
|---|---|---|---|
| Timestamp | 00:01:02,500 (comma before milliseconds) | 00:01:02.500 (dot); hours may be omitted: 01:02.500 | [01:02.50] (min:sec.centiseconds) |
| End time | Yes, start --> end | Yes, same as SRT | Usually start only; ends when the next line starts |
| File header | None; each cue starts with an index | Must begin with WEBVTT | May carry tags such as [ti:] and [ar:] |
| Extras | Practically none (HTML tags often sneak in) | Position and alignment settings, NOTE comments, STYLE blocks | Line-level sync; some extensions add per-word timing |
| Typical use | Desktop players, editors, video-platform uploads | The track element of an HTML video | Music-player lyrics |
What the tool actually does (verified in the source)
- Format detection checks the file extension first (.vtt, .srt, .lrc), then whether the content starts with WEBVTT (BOM tolerated), then whether it contains -->, in which case it is treated as SRT; otherwise it is LRC.
- SRT export always renumbers cues from 1 and sorts them by start time. VTT export adds the WEBVTT header. LRC export writes each line as [mm:ss.cc]<mm:ss.cc>text, with both start and end, and collapses multi-line text into one line.
- When importing LRC, tag lines such as [ti:], [ar:], [al:], [by:] and [offset:] are skipped, and [offset:] is not applied to the timestamps.
- An LRC line with several time tags (a repeated chorus) is expanded into several cues.
- The editor can load local audio or video. Click a row to jump to that time; play to a position and press Set start or Set end to write the current playback time into the selected cue; or edit the time cells by hand. If the end is not later than the start, it is set to start plus one second.
Example 1: LRC to SRT, and the fixed three seconds
LRC has no end times. The tool gives every line a default duration of 3 seconds and cuts it short only if the next line starts earlier.
# Input LRC
[ti:Demo]
[00:12.00]Hello world
[00:15.50]Second line
[00:21.00]Third line
# Output SRT
1
00:00:12,000 --> 00:00:15,000
Hello world
2
00:00:15,500 --> 00:00:18,500
Second line
3
00:00:21,000 --> 00:00:24,000
Third lineNotice that the second line disappears at 18.5 s while the third appears at 21 s, leaving a 2.5 s gap. If you want a line to stay until the next one appears, set each end time to the next start time in the editor. Lyrics with long instrumental breaks hit this the most.
Example 2: SRT to VTT for a web player
# Input SRT
1
00:00:01,000 --> 00:00:03,500
Welcome
# Output VTT
WEBVTT
00:00:01.000 --> 00:00:03.500
Welcome<video controls src="demo.mp4">
<track kind="subtitles" src="demo.vtt" srclang="en" label="English" default>
</video>A track file must be same-origin with the page, or served with CORS headers that allow it, and should have the MIME type text/vtt. Opening the HTML directly from disk often fails to load subtitles because of file:// restrictions, which is not a problem with the subtitle file itself.
5. Common subtitle failures and how to diagnose them
| Symptom | Cause | Fix |
|---|---|---|
| “No valid subtitle lines” after import | Not really SRT/VTT/LRC, or timing lines are malformed | Check for --> ; for LRC, check the [mm:ss.xx] form |
| Some VTT cues are missing | Timestamps like 01:02.500 omit hours, while the tool expects hh:mm:ss | Prefix them to 00:01:02.500 and import again |
| Non-ASCII text is garbled | The file is GBK, Big5 or another non-UTF-8 encoding, and the tool reads UTF-8 | Re-save as UTF-8 in a text editor first |
| Position/alignment lost after converting to VTT | Cue settings, NOTE and STYLE blocks are not preserved | Re-add them by hand afterwards, or check you really need them |
| LRC output is uniformly early or late | The source has an [offset:] tag, which is skipped | Fold the offset into each timestamp manually |
| Lyrics flash and vanish | LRC has no end time, so each line shows for 3 s by default | Lengthen the end times in the editor |
Shifting the whole timeline when there is no one-click button
The editor lets you set each cue’s start and end, but it has no “delay everything by 2.5 s” button. If the whole track is off by a constant, it is quicker to shift the SRT text once with the snippet below, pasted into the browser developer console, and then load the result back into the editor for a final check.
const shiftMs = 2500; // positive delays, negative advances
const pad = (n, w) => String(n).padStart(w, '0');
function shiftSrt(srt) {
return srt.replace(/(\d{2}):(\d{2}):(\d{2}),(\d{3})/g, (_, h, m, s, ms) => {
const total = Math.max(0, ((+h * 60 + +m) * 60 + +s) * 1000 + +ms + shiftMs);
return pad(Math.floor(total / 3600000), 2) + ':' +
pad(Math.floor(total % 3600000 / 60000), 2) + ':' +
pad(Math.floor(total % 60000 / 1000), 2) + ',' + pad(total % 1000, 3);
});
}6. When not to use this approach
- Dynamic codes that need scan analytics or an editable destination: a static QR code cannot be changed once printed. Those needs call for a redirect service, with its privacy and longevity trade-offs.
- Payments and login credentials: a QR code is encoding, not encryption.
- Very long text: near the version 40 limit the modules are so dense that reliable scanning is unrealistic. Use a link or a file download instead.
- Styled subtitles: ASS/SSA fonts, colors and karaoke effects, as well as burned-in hard subtitles, are outside what SRT, VTT and LRC can express.
- Speech transcription: the subtitle tool edits, times and converts; it does not recognize speech itself.
- Word-by-word karaoke highlighting: word-level timestamps after the line timestamp are not parsed.