Paste a hex dump from a packet capture, a
database column or a \x-escaped log line and read it
as text. The bytes can be separated by spaces, colons or 0x
prefixes. It also works the other way, and it reports bytes
that are not valid UTF-8 instead of turning them into
question marks without a word.
Going the other way, the Separator menu writes the bytes as plain 4865, spaced or colon separated pairs, \x escapes or 0x prefixed values.
48 65 6c 6c 6f 2c 20 5a 6f c3 ab 21 20 e2 98 95 0a
0x46 0x61 0x63 0x74 0x75 0x72 0x61 0x20 0x23 0x34 0x32 0x0a
\xf0\x9f\x98\x80 Hello, Zoë! ☕
Factura #42
😀
The line breaks in the hex are only separators and are
ignored, like the spaces. The line breaks in the output come
from the 0a bytes at the end of each group,
because a newline in text is one more byte.
| Bytes | Text | Why |
|---|---|---|
| 48 65 6c 6c 6f | Hello | Plain ASCII, one byte per character |
| c3 ab | ë | Two bytes: 110xxxxx 10xxxxxx |
| e2 98 95 | ☕ | Three bytes, most of the BMP |
| f0 9f 98 80 | 😀 | Four bytes, above U+FFFF |
| e9 | é in Latin-1, invalid in UTF-8 | A lone lead byte with nothing after it |
The first byte tells you how long the character is: below
80 is a single ASCII byte, c2 to
df starts a two-byte sequence, e0
to ef three, f0 to f4
four. Every continuation byte is between 80 and
bf. A continuation byte where a lead byte should
be, or a lead byte followed by ASCII, is what makes the
decoder print U+FFFD.
If UTF-8 shows replacement characters, try Latin-1. The
note under the tool counts the invalid bytes. A lone
e9 is é in Latin-1 (and in Windows-1252, which
is identical for letters) but invalid in UTF-8, so data from
an old MySQL latin1 table or a Windows export usually
decodes cleanly that way. If UTF-8 gives you
é instead of é, the bytes were encoded twice:
a program read UTF-8 as Latin-1 and saved it again.
Latin-1 here means ISO-8859-1: bytes 80 to 9F come out as C1 control characters, not the curly quotes and euro sign Windows-1252 puts there. If the hex is not text at all (an image header, a compressed blob), neither encoding will make it readable, and the byte count is the useful number.
// Hex -> text. Strip 0x and \x prefixes and any separators first.
const hex = "0x5a, 0x6f, 0xc3, 0xab 20 e2 98 95";
const clean = hex.replace(/0x|\\x|[^0-9a-f]/gi, "");
const bytes = Uint8Array.from(clean.match(/../g), (h) => parseInt(h, 16));
console.log(new TextDecoder().decode(bytes)); // Zoë ☕
// fatal: true throws on invalid UTF-8 instead of printing U+FFFD
try {
new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xe9]));
} catch (e) {
console.log("not UTF-8, try latin1:", new TextDecoder("latin1").decode(new Uint8Array([0xe9])));
}
// Text -> hex, space separated
const out = Array.from(new TextEncoder().encode("Zoë ☕"),
(b) => b.toString(16).padStart(2, "0")).join(" ");
console.log(out); // 5a 6f c3 ab 20 e2 98 95
// Node.js
console.log(Buffer.from("Zoë").toString("hex")); // 5a6fc3ab
console.log(Buffer.from("5a6fc3ab", "hex").toString("utf8")); // Zoë In browsers the label latin1 decodes as windows-1252, which only differs from ISO-8859-1 in bytes 80 to 9F.
<?php
echo bin2hex("Zoë ☕"), PHP_EOL; // 5a6fc3ab20e29895
echo hex2bin("5a6fc3ab"), PHP_EOL; // Zoë
// hex2bin() rejects separators and odd lengths, so clean up first
$hex = preg_replace('/0x|\\\\x|[^0-9a-f]/i', '', '0x5a, 0x6f, \xc3:ab');
echo hex2bin($hex), PHP_EOL; // Zoë
// Spaced, uppercase output
echo strtoupper(implode(' ', str_split(bin2hex("Zoë"), 2))), PHP_EOL; // 5A 6F C3 AB
// Check the bytes are UTF-8 before trusting them
$bytes = hex2bin("e9");
var_dump(mb_check_encoding($bytes, 'UTF-8')); // bool(false)
echo mb_convert_encoding($bytes, 'UTF-8', 'ISO-8859-1'), PHP_EOL; // é # Text -> hex (bytes.hex with a separator needs Python 3.8+)
print("Zoë ☕".encode("utf-8").hex(" ")) # 5a 6f c3 ab 20 e2 98 95
# Hex -> text. fromhex() skips spaces but not 0x, colons or commas.
raw = "0x5a:0x6f:0xc3:0xab"
clean = raw.replace("0x", "").replace(":", "")
print(bytes.fromhex(clean).decode("utf-8")) # Zoë
# Invalid UTF-8: choose between an exception, U+FFFD, or another codec
data = bytes.fromhex("e9")
print(data.decode("utf-8", errors="replace")) # U+FFFD
print(data.decode("latin-1")) # é Hex is a way of writing bytes, two digits per byte. What those bytes mean depends on the encoding, and UTF-8 (RFC 3629) is the one to assume unless you know otherwise. Its structure is visible in the hex: bytes below 80 are ASCII, C2 to F4 start a multi-byte character, and 80 to BF only ever continue one.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
Plain pairs (48656c), space or colon separated bytes, 0x48 prefixes, \x48 escapes, comma-separated C arrays and %48 URL style, mixed with line breaks. Separators are ignored; only the hex digits count.
The bytes are not valid UTF-8, and each bad sequence is replaced with U+FFFD. The note under the tool counts them. Data from older systems is often Latin-1; switch the Encoding dropdown to try it.
The counter above the output shows characters and bytes. In UTF-8 an ASCII character is one byte, accented Latin letters two, most other scripts three, and emoji four.
No. This treats hex as a sequence of bytes that encode text. To convert a single number such as 0xFF to 255, use the number base converter.
Yes. Upload File reads the file in your browser and shows its bytes as hex, up to 5 MB. The file is not sent anywhere.
Set Direction to Text to hex and Separator to \x (\x48\x65). The output is one escape per byte with nothing between them, so Hello becomes \x48\x65\x6c\x6c\x6f, ready to paste inside a string literal in C, Python, PHP or a shell printf. Tick Uppercase if you want capital hex digits. Pasting such a line back in with Hex to text decodes it again.
Choose 0x (0x48 0x65) to prefix every byte and separate them with spaces, or Comma (0x48, 0x65) to get a list you can paste straight into a C, Go or Java array initializer. Both are presentation only: the Hex to text direction strips the prefixes and commas again, so either form round-trips without editing.
Pick None (4865) in Separator. That gives a continuous string of two digits per byte, the same form that Node's Buffer.toString("hex"), PHP's bin2hex() and Python's bytes.hex() produce, and the form most APIs expect for keys and digests. Colon (48:65) matches the style used for MAC addresses and OpenSSL certificate fingerprints.
Text to binary shows the same bytes in binary, decimal or octal. To turn bytes into something that survives a text channel use the Base64 encoder, and for single numbers rather than strings the number base converter is quicker.