Javid
·14 min read

RGB to CMYK: The Formula and Why Print Needs More

SelfDevKit color tools converting an RGB color to CMYK alongside HEX, HSL, and OKLCH values

To convert RGB to CMYK, divide each channel by 255, set black to K = 1 - max(R, G, B), then compute each ink as C = (1 - R - K) / (1 - K), and the same for M with green and Y with blue. Multiply by 100 for percentages. rgb(59, 130, 246) becomes cmyk(76%, 47%, 0%, 4%).

That formula is what nearly every online RGB to CMYK converter runs, and it is exact in its own terms: you can reverse it and land within a rounding error of where you started. What it cannot do is tell you what a printing press will put on paper. To measure how far apart those two things are, the tests below run the same colors through the formula and through a real press profile (Coated FOGRA39, a widely used European profile for coated offset paper) and compare the predicted printed results. Some colors came out fine. A mid grey missed by a margin anyone would see.

This post covers the formula, hex to CMYK and CMYK to RGB, tested code in JavaScript and Python, and the point where you need an ICC profile instead.

The RGB to CMYK formula, step by step

The RGB to CMYK formula pulls out the darkest shared component as black (K), then expresses the remaining color as cyan, magenta, and yellow relative to what is left. Here it is with #3B82F6, blue-500 in Tailwind CSS v3:

  1. Normalize. R' = 59/255 = 0.231, G' = 130/255 = 0.510, B' = 246/255 = 0.965.
  2. Black. K = 1 - max(0.231, 0.510, 0.965) = 0.035.
  3. Cyan. C = (1 - 0.231 - 0.035) / (1 - 0.035) = 0.761.
  4. Magenta. M = (1 - 0.510 - 0.035) / 0.965 = 0.472.
  5. Yellow. Y = (1 - 0.965 - 0.035) / 0.965 = 0.
  6. Round to percent. cmyk(76%, 47%, 0%, 4%).

One edge case breaks naive implementations. For pure black, K = 1 and every ink formula divides by zero. Special-case it as 0, 0, 0, 100.

The intuition: RGB is additive light, starting from black and adding red, green, and blue. CMYK is subtractive ink on white paper, where cyan absorbs red, magenta absorbs green, and yellow absorbs blue. So cyan is essentially "the absence of red," and K replaces the grey part that all three inks would otherwise have to build together.

Hex to CMYK and CMYK to RGB

Hex to CMYK is the same conversion with one extra step: a hex code is just RGB written in base 16, so parse #RRGGBB into three bytes and run the formula. #FF6600 is rgb(255, 102, 0), which gives cmyk(0%, 60%, 100%, 0%). If the parsing part is what trips you up (shorthand #F60, eight-digit alpha, stray # characters), the hex to RGB color guide covers those cases.

CMYK to RGB runs the formula backwards:

R = 255 × (1 - C) × (1 - K)
G = 255 × (1 - M) × (1 - K)
B = 255 × (1 - Y) × (1 - K)

This is also the conversion the W3C wrote into CSS Color Module Level 5 for device-cmyk(). The spec expresses it as red = 1 - min(1, cyan * (1 - black) + black), which is algebraically the same thing with a clamp. The spec only uses it as a fallback: if a stylesheet supplies a CMYK ICC profile, "the computed value of the device-cmyk() function must be the Lab value of the CMYK color." In practice neither path matters yet, because MDN reports that no browser supports device-cmyk() at all.

The round trip is not lossless

Whole-number CMYK percentages are coarser than 8-bit RGB, so a round trip through them usually moves the color slightly. Running all 16,777,216 RGB colors through the JavaScript functions below (RGB to whole-percent CMYK and back) gives:

Measurement Result
Colors that return exactly 2,136,545 (12.73%)
Colors that shift 87.27%
Largest error on any channel 2 (out of 255)
Distinct integer CMYK values produced 2,463,617

#3B82F6 comes back as #3B82F5. Nobody can see a difference of 1 in the blue channel, but a test that asserts cmykToRgb(rgbToCmyk(x)) === x will fail for most inputs. Compare with a tolerance of 2, or keep the CMYK values as floats until the last step.

RGB to CMYK in JavaScript and Python

Both snippets below were run as shown, and the comments are their real output.

function rgbToCmyk(r, g, b) {
  const [rp, gp, bp] = [r / 255, g / 255, b / 255];
  const k = 1 - Math.max(rp, gp, bp);
  if (k === 1) return { c: 0, m: 0, y: 0, k: 100 }; // pure black: avoid 0/0
  const pct = (v) => Math.round(v * 100);
  return {
    c: pct((1 - rp - k) / (1 - k)),
    m: pct((1 - gp - k) / (1 - k)),
    y: pct((1 - bp - k) / (1 - k)),
    k: pct(k),
  };
}

function hexToCmyk(hex) {
  const h = hex.replace(/^#/, '');
  const full = h.length === 3 ? [...h].map((ch) => ch + ch).join('') : h;
  if (!/^[0-9a-f]{6}$/i.test(full)) throw new Error(`Not a hex color: ${hex}`);
  const n = parseInt(full, 16);
  return rgbToCmyk((n >> 16) & 255, (n >> 8) & 255, n & 255);
}

function cmykToRgb(c, m, y, k) {
  const ch = (v) => Math.round(255 * (1 - v / 100) * (1 - k / 100));
  return { r: ch(c), g: ch(m), b: ch(y) };
}

rgbToCmyk(59, 130, 246); // { c: 76, m: 47, y: 0, k: 4 }
hexToCmyk('#f60');       // { c: 0, m: 60, y: 100, k: 0 }
cmykToRgb(76, 47, 0, 4); // { r: 59, g: 130, b: 245 }
def rgb_to_cmyk(r, g, b):
    rp, gp, bp = r / 255, g / 255, b / 255
    k = 1 - max(rp, gp, bp)
    if k == 1:
        return (0, 0, 0, 100)
    c, m, y = ((1 - x - k) / (1 - k) for x in (rp, gp, bp))
    return tuple(round(v * 100) for v in (c, m, y, k))

def cmyk_to_rgb(c, m, y, k):
    return tuple(round(255 * (1 - v / 100) * (1 - k / 100)) for v in (c, m, y))

rgb_to_cmyk(0, 166, 81)        # (100, 0, 51, 35)
cmyk_to_rgb(100, 0, 51, 35)    # (0, 166, 81)

If you already depend on the color npm package, Color('#3b82f6').cmyk().round().object() gives the same { c: 76, m: 47, y: 0, k: 4 }. Note that color 5.0.0 converts to CMYK but will not parse a cmyk(...) string; use Color.cmyk(76, 47, 0, 4) to go the other way.

What the formula doesn't know about printing

The naive RGB to CMYK formula assumes perfect inks on perfect paper. Real ink spreads into the paper (dot gain), real cyan is contaminated with a bit of magenta, and the press has a limit on how much total ink the sheet can hold. An ICC profile is a measured description of all that for one specific press, ink set, and paper. Conversion software uses it to pick the CMYK values that will actually reproduce your color.

To see the gap, the colors below were converted two ways using LittleCMS 2.19 (through Pillow 13) and the Coated FOGRA39 profile with the relative colorimetric intent. The profile then predicted what each set of CMYK numbers would look like printed, and ΔE2000, a standard color-difference metric, measured the distance from the original sRGB color. These are profile predictions, not measurements of real prints. As a rough guide, a ΔE around 1 is hard to see even side by side, and differences above 5 are obvious.

Screen color Formula CMYK ΔE printed FOGRA39 CMYK ΔE printed
#808080 grey 0, 0, 0, 50 10.1 49, 40, 40, 22 1.5
#00A651 green 100, 0, 51, 35 26.9 82, 1, 91, 0 0.0
#8B4513 brown 0, 50, 86, 45 11.0 29, 76, 100, 35 4.4
#1DA1F2 sky blue 88, 33, 0, 5 11.5 72, 27, 0, 0 4.0
#000000 black 0, 0, 0, 100 10.7 91, 79, 62, 98 6.4
#FF6600 orange 0, 60, 100, 0 6.9 0, 71, 100, 0 3.6
#F5F5DC beige 0, 0, 10, 4 4.1 5, 2, 20, 0 0.7
#E31837 red 0, 89, 76, 11 3.1 1, 100, 80, 0 0.5
#FFD700 gold 0, 16, 100, 0 1.3 0, 14, 96, 0 0.6

Three patterns stand out.

Greys print too dark. The formula turns every neutral into black ink only, so #808080 becomes K 50%. Dot gain makes a 50% tint print noticeably darker than 50%, and the result misses by 10 ΔE. The profile builds the same grey from all four inks at much lower coverage.

Black is not just K. 0, 0, 0, 100 prints as a dark charcoal, not the black on your screen. The profile asks for 91, 79, 62, 98, a "rich black" with 330% total ink, which is the coverage limit of this profile. Rich black is right for large dark areas and wrong for small body text, where any misregistration between the four plates leaves colored fringes. Printers typically want text in K only, so even the "correct" conversion needs a human decision.

Mid-dark colors drift most. #00A651 is comfortably inside the printable range; the profile reproduces it exactly. The formula's 100, 0, 51, 35 misses by 26.9 because it has no idea how much darker real ink gets when you stack cyan, yellow, and 35% black.

Some screen colors have no CMYK equivalent

Even a perfect conversion cannot print colors outside the press's gamut. Pure #00FF00 comes out of FOGRA39 as 66, 0, 100, 0, which looks like #66B338 when shown back on screen: an 18.3 ΔE miss that no CMYK value fixes. Pure blue (#0000FF) misses by 15.7, and #FF0000 prints closer to #E42325. The formula never warns you about this. It produces confident numbers for colors that cannot exist on paper.

Same RGB, same press, different CMYK

The profile is not a neutral fact either. ISO Coated v2 (ECI) targets the same FOGRA39 printing condition, and in this test both profiles mapped the same CMYK values to almost identical Lab colors. Yet they separate colors differently:

Screen color Coated FOGRA39 ISO Coated v2 (ECI)
#000000 91, 79, 62, 98 95, 95, 45, 95
#0000FF 95, 76, 0, 0 92, 100, 0, 0
#FF0000 0, 99, 100, 0 0, 90, 100, 0
#808080 49, 40, 40, 22 47, 38, 38, 24

Choices like how much grey to move into black ink, and how to compress out-of-gamut colors, are baked into each profile. So when someone asks "what is the CMYK of our brand blue," the honest answer is "for which press?"

Converting with an ICC profile in code

To convert RGB to CMYK for actual print output, run the color through a color management engine with the printer's ICC profile. In Python, Pillow wraps LittleCMS:

from PIL import Image, ImageCms

srgb = ImageCms.createProfile("sRGB")
press = ImageCms.getOpenProfile("CoatedFOGRA39.icc")

to_press = ImageCms.buildTransform(
    srgb, press, "RGB", "CMYK",
    renderingIntent=ImageCms.Intent.RELATIVE_COLORIMETRIC,
)

def rgb_to_press_cmyk(r, g, b):
    px = ImageCms.applyTransform(Image.new("RGB", (1, 1), (r, g, b)), to_press)
    return tuple(round(v * 100 / 255) for v in px.getpixel((0, 0)))

rgb_to_press_cmyk(128, 128, 128)  # (49, 40, 40, 22)
rgb_to_press_cmyk(0, 0, 0)        # (91, 79, 62, 98)

# Whole images: convert once, save as a CMYK TIFF
img = Image.new("RGB", (64, 64), (0, 166, 81))
ImageCms.applyTransform(img, to_press).save("out-cmyk.tif")

Three practical notes:

  • Get the profile from the printer. FOGRA39 is common in Europe and GRACoL/SWOP profiles in the US, but the printer knows what they actually run. The ECI downloads page lists PSO Coated v3 (FOGRA51) as its current coated offset profile and keeps ISO Coated v2 in an archive of older profiles, so ask before assuming FOGRA39.
  • Pick a rendering intent. Relative colorimetric keeps in-gamut colors exact and clips the rest, which suits logos and UI colors. Perceptual compresses the whole range to keep relationships between colors, which suits photos. In the test above, perceptual turned #FF0000 into 0, 96, 94, 0 instead of 0, 99, 100, 0.
  • Convert once, at the end. Keep artwork in RGB and convert to CMYK when exporting for a specific job. Every RGB to CMYK conversion throws away gamut you cannot get back.

When the formula is the right tool

The naive RGB to CMYK formula is fine whenever the numbers are a label rather than a print instruction:

  • Filling in the CMYK row of a brand spec sheet as a starting point, clearly marked as unproofed
  • Building a design token file or color picker that shows CMYK alongside HEX and HSL
  • Rough checks: "is this color mostly cyan?" or "does this grey have a color cast?"
  • Implementing the device-cmyk() fallback from the CSS spec

It is the wrong tool for anything going to a press: packaging, print ads, business cards from a commercial printer, or a brand color that must match across offset and digital print. For those, convert with the printer's profile or ask them to do it. If the brand color has to be exact everywhere, it probably should be a spot color (such as a Pantone ink) rather than a CMYK mix at all.

Converting RGB to CMYK in SelfDevKit

SelfDevKit color tools showing an RGB color converted to CMYK, HEX, HSL, and OKLCH values

SelfDevKit's Color Tools show the CMYK value of any color you paste in, alongside HEX, RGB, HSL, and OKLCH. Paste #3b82f6, rgb(59 130 246), or hsl(217, 91%, 60%) and you get cmyk(76%, 47%, 0%, 4%), ready to copy. The same panel generates color harmonies and checks WCAG contrast, so a palette review happens in one place.

To be clear about what it is: the CMYK shown is the formula conversion described above, rounded to whole percents. It is the right value for specs, tokens, and quick checks, not a substitute for a press profile. It also converts in one direction. It does not accept CMYK input, so for CMYK to RGB use the three-line formula or the code above.

It runs offline, which matters more than it sounds for color work. An unreleased brand palette or a client's identity system under NDA is exactly the kind of thing you should not be pasting into random web converters. More on that in why offline matters.

Common questions

Is RGB to CMYK conversion accurate?

The formula is mathematically consistent but not accurate for printing, because it ignores ink, paper, and press behavior. In the FOGRA39 test above, the profile predicted that formula values would miss the original color by up to 26.9 ΔE, while profile-based values stayed under 2 for five of the nine colors in the table.

What is the CMYK value of black?

The formula says 0, 0, 0, 100. For large areas a printer will usually want a rich black like 91, 79, 62, 98 (FOGRA39) or a simpler mix such as 60, 40, 40, 100 if their guidelines specify one. For small text, use K only.

Try it yourself

For the everyday case, a CMYK value for a spec sheet or a quick look at how a color breaks down into inks, SelfDevKit gives you the conversion next to every other format, offline, with 50+ other developer tools in the same app. If you are new, the getting started guide is a short tour.

Download SelfDevKit and keep your color work on your own machine.

Related Articles

Hex to RGB Color: Conversion Rules, Edge Cases, and Code That Works
DEVELOPER TOOLS

Hex to RGB Color: Conversion Rules, Edge Cases, and Code That Works

Convert hex to RGB color values correctly: the math, shorthand and alpha rules, parser code, and the platform gotchas that break conversions.

Read →
RGB Color Picker: How RGB Works, CSS Syntax, and Developer Workflows
DEVELOPER TOOLS

RGB Color Picker: How RGB Works, CSS Syntax, and Developer Workflows

Use an RGB color picker to find exact color values, convert formats, and write modern CSS with confidence.

Read →
Hex to RGB: Conversion Chart, Mental Math, and What Modern CSS Changed
DEVELOPER TOOLS

Hex to RGB: Conversion Chart, Mental Math, and What Modern CSS Changed

Hex to RGB charts for CSS colors and greys, mental math you can do without a tool, and why OKLCH palettes broke the exact conversion.

Read →
Hex Color Picker: The Developer Guide to Color Formats, Contrast, and Workflows
DEVELOPER TOOLS

Hex Color Picker: The Developer Guide to Color Formats, Contrast, and Workflows

Use a hex color picker to find exact color codes, convert between formats, and check WCAG contrast.

Read →