<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jack</title>
    <description>The latest articles on DEV Community by jack (@jack_e2a413ec5d0bba216).</description>
    <link>https://dev.to/jack_e2a413ec5d0bba216</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137922%2F418719d4-7c90-44de-8fe1-5751972dd457.jpg</url>
      <title>DEV Community: jack</title>
      <link>https://dev.to/jack_e2a413ec5d0bba216</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jack_e2a413ec5d0bba216"/>
    <language>en</language>
    <item>
      <title>I checked 2,016 AI images letter by letter. Here's how I kept the test fair</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Fri, 30 Oct 2026 21:53:44 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/i-checked-2016-ai-images-letter-by-letter-heres-how-i-kept-the-test-fair-3215</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/i-checked-2016-ai-images-letter-by-letter-heres-how-i-kept-the-test-fair-3215</guid>
      <description>&lt;p&gt;GPT Image printed VENON on a death metal T-shirt. The prompt said VENOM. The last M had turned into an N, hidden in the thorns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl5awr0dxjq4vdv0wu9t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl5awr0dxjq4vdv0wu9t.jpg" alt="GPT Image death metal logo reading VENON instead of VENOM" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our first check missed it. It was read off a contact sheet at thumbnail size and marked correct. A zoomed re-read caught it later. That one image shaped most of how we run this test, so this post is about the method, not the leaderboard.&lt;/p&gt;

&lt;p&gt;Some context. My team runs AIFont, a site that publishes text-effect prompts for image models. Every prompt goes to five models in seven languages, and every image gets checked against the word it was supposed to write. After 59 styles that came to 2,057 first-attempt images, 2,016 of which we could score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results, briefly
&lt;/h2&gt;

&lt;p&gt;Correct out of judged, first attempt only:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Script&lt;/th&gt;
&lt;th&gt;GPT Image 2.5&lt;/th&gt;
&lt;th&gt;Nano Banana 2.1&lt;/th&gt;
&lt;th&gt;Grok Imagine 2.0&lt;/th&gt;
&lt;th&gt;Seedream 5.0 Pro&lt;/th&gt;
&lt;th&gt;Qwen-Image 3.0 Pro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latin (en, de, es, pt)&lt;/td&gt;
&lt;td&gt;235/236&lt;/td&gt;
&lt;td&gt;235/235&lt;/td&gt;
&lt;td&gt;229/235&lt;/td&gt;
&lt;td&gt;230/235&lt;/td&gt;
&lt;td&gt;213/235&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chinese&lt;/td&gt;
&lt;td&gt;59/59&lt;/td&gt;
&lt;td&gt;58/59&lt;/td&gt;
&lt;td&gt;58/59&lt;/td&gt;
&lt;td&gt;56/59&lt;/td&gt;
&lt;td&gt;56/59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japanese&lt;/td&gt;
&lt;td&gt;59/59&lt;/td&gt;
&lt;td&gt;58/59&lt;/td&gt;
&lt;td&gt;52/59&lt;/td&gt;
&lt;td&gt;51/59&lt;/td&gt;
&lt;td&gt;45/59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arabic&lt;/td&gt;
&lt;td&gt;48/50&lt;/td&gt;
&lt;td&gt;51/51&lt;/td&gt;
&lt;td&gt;35/49&lt;/td&gt;
&lt;td&gt;27/47&lt;/td&gt;
&lt;td&gt;6/53&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I give counts, not percentages, on purpose. With about 60 images per cell, one image moves a percentage by almost two points. GPT Image and Nano Banana are one image apart overall. That's a tie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same prompt, first image only
&lt;/h2&gt;

&lt;p&gt;Two rules do most of the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every model gets the identical prompt.&lt;/strong&gt; One prompt per style per language, written in that language, sent unchanged to all five. No per-model tuning. If a model needs a special prompt to spell right, that's a result, not something to fix before measuring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only the first image counts.&lt;/strong&gt; We do rerun wrong images, because the website needs a usable picture. But a retry only exists because the first try failed, and sometimes we draw extra samples after a correct one. Both lean toward correct. Count them and every model looks better than it is, and the weaker models get more second chances.&lt;/p&gt;

&lt;p&gt;There was a less obvious trap. New styles get drafted on one model first, and the prompt changes a few times before all five run. Count every prompt version and that drafting model gets extra rows the others never saw. So scoring uses only the prompt version each style uses now. A prompt version is the MD5 of the full text sent, so "same prompt" is something you can check in the data, not something you have to take from me.&lt;/p&gt;

&lt;p&gt;Settings were fixed per model: 2K, square, each vendor's API, October 22–29, 2026. GPT Image at xhigh quality, Grok Imagine at medium (its highest). The other three have no quality setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger: append-only, latest review wins
&lt;/h2&gt;

&lt;p&gt;Every generation writes a row, failed API calls included. Reviews go in a second table. Nothing in either table gets edited. Trimmed down, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- one row per generation, success or failure&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gen_attempts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;font_id&lt;/span&gt;       &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;-- style slug&lt;/span&gt;
  &lt;span class="n"&gt;locale&lt;/span&gt;        &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;word&lt;/span&gt;          &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- the text the prompt asks for&lt;/span&gt;
  &lt;span class="n"&gt;prompt_hash&lt;/span&gt;   &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;-- md5 of the full prompt sent&lt;/span&gt;
  &lt;span class="n"&gt;attempt_kind&lt;/span&gt;  &lt;span class="nb"&gt;tinyint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- 1 first, 2 retry, 3 extra sample&lt;/span&gt;
  &lt;span class="n"&gt;model_id&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;-- upstream model version at generation time&lt;/span&gt;
  &lt;span class="n"&gt;ok&lt;/span&gt;            &lt;span class="nb"&gt;tinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;-- did the API return an image&lt;/span&gt;
  &lt;span class="n"&gt;archive_url&lt;/span&gt;   &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- our copy of the original, never deleted&lt;/span&gt;
  &lt;span class="n"&gt;gen_time&lt;/span&gt;      &lt;span class="nb"&gt;datetime&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- one row per review; changing your mind = a new row&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gen_reviews&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempt_id&lt;/span&gt;    &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;verdict&lt;/span&gt;       &lt;span class="nb"&gt;tinyint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- 1 correct, 2 wrong, 3 unsure&lt;/span&gt;
  &lt;span class="n"&gt;error_types&lt;/span&gt;   &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- fixed vocabulary&lt;/span&gt;
  &lt;span class="n"&gt;rendered&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- what the image actually says&lt;/span&gt;
  &lt;span class="n"&gt;zoomed&lt;/span&gt;        &lt;span class="nb"&gt;tinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;reviewer&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;is_native&lt;/span&gt;     &lt;span class="nb"&gt;tinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;supersedes_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- the review this one replaces&lt;/span&gt;
  &lt;span class="n"&gt;review_time&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three choices I'd make again.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;model_id&lt;/code&gt; is read from the service config at the moment of generation. No hand-kept mapping table. If a vendor quietly swaps the model behind the same name, the data splits on its own.&lt;/p&gt;

&lt;p&gt;A re-review is a new row pointing at the one it replaces. VENON's wrong "correct" is still in there, followed by the correction. You can see who changed their mind and when.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;attempt_kind&lt;/code&gt; follows the latest reviews. If a first image flips from correct to wrong, the second image in that cell stops being an "extra sample" and becomes a "retry". It's the one field the tooling is allowed to recompute. And the content build refuses to publish a style if any of its images has no review yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI reviews everything, people review where the AI might be wrong
&lt;/h2&gt;

&lt;p&gt;Reading 2,000 images carefully by hand isn't realistic. I'd rather a person read 500 carefully than 2,000 tired. So there are two layers.&lt;/p&gt;

&lt;p&gt;An AI reviewer read every image, zoomed in for Chinese, Japanese and Arabic. Then a person went through the images where the AI was most likely to be wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every image the AI marked wrong or unsure&lt;/li&gt;
&lt;li&gt;Arabic, where AI reading isn't reliable&lt;/li&gt;
&lt;li&gt;Chinese and Japanese images the AI had only seen on a contact sheet&lt;/li&gt;
&lt;li&gt;a random sample of 50 from everything else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample of 50 matched the AI on all 50, so the rest keep the AI's verdict and say so (&lt;code&gt;human_reviewed: false&lt;/code&gt;). In the scored set, 489 images had a person look at them.&lt;/p&gt;

&lt;p&gt;What we skipped: cases a glance settles. A paragraph of extra lettering across the image, or a word that differs only in case, which a rule decides (more below). Human time there buys nothing.&lt;/p&gt;

&lt;p&gt;The direction of the AI's mistakes surprised me. It was too strict, not too lenient. About a third of the non-Arabic images it marked wrong were overturned by the human pass. The human pass wasn't perfect either: re-reading the disagreements changed 11 more verdicts. One read isn't enough, for a model or a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong only if you can say what it reads as
&lt;/h2&gt;

&lt;p&gt;Most of those overturned calls were the same kind: a letter that looks odd. The ぶ in しぶや drawn like a 3 with two dots. An extra slanted stroke inside 自. A capital F with a flourish that looks a bit like a J. A native reader reads all of them as the intended word without stopping.&lt;/p&gt;

&lt;p&gt;So the rule became: &lt;strong&gt;a word is wrong only if you can say what it reads as instead.&lt;/strong&gt; VENON. Konra. Eiwig. 爱の兰. If all you can say is "the shape is weird", it's correct. That's why &lt;code&gt;rendered&lt;/code&gt; is required for every spelling error. If you can't fill it in, you don't have an error.&lt;/p&gt;

&lt;p&gt;Three calls follow from that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traditional Chinese counts as correct in a Simplified prompt.&lt;/strong&gt; A reader sees 义氣 and reads 义气. Mixed forms are normal on real signs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case doesn't count&lt;/strong&gt; unless the prompt asked for it. POTION for Potion is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extra text counts as wrong only when it competes with the word&lt;/strong&gt;: similar size, right next to it, reads like part of the title. A shop sign in the background is background.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think this is the right rule for any text-rendering test, and I expect people to disagree. A stricter rule, every stroke matching a reference glyph, is easier to automate. It also punishes exactly the stylization people ask these models for. Nobody prompts for a blackletter logo and wants Helvetica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arabic: why we don't claim 100%
&lt;/h2&gt;

&lt;p&gt;Nobody on my team reads Arabic natively. The AI doesn't read it reliably either. In the first human pass, the AI and the person disagreed on 17% of the Arabic images they both read.&lt;/p&gt;

&lt;p&gt;When they disagreed, we didn't pick a winner. The record becomes &lt;code&gt;unsure&lt;/code&gt;, gets &lt;code&gt;disputed: true&lt;/code&gt;, keeps both calls, and stays out of the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"شارع"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grok Imagine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text_verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unsure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviews"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reviewer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text_verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"incorrect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"native_speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zoomed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reviewer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maintainer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text_verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"correct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"native_speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zoomed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disputed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;41 images in the scored set are like this. It changes how you should read the Arabic row. Nano Banana's 51/51 comes from 58 images; 7 are disputed. GPT Image has 8.&lt;/p&gt;

&lt;p&gt;I'm confident in the ranking. 48 of 50 against 6 of 53 is a far bigger gap than any reviewing error. I'm not confident in the exact numbers, and they will move when native readers check them. Writing "100% human-verified" on this would be easy, and it would be false.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffw97kgyd4vpjcsb6kl52.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffw97kgyd4vpjcsb6kl52.jpg" alt="Seedream Arabic graffiti tag with an extra letter, reading نمبر instead of نمر" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That one isn't disputed. The prompt asked for نمر (tiger). Seedream added a ب and a vowel mark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five numbers hide the error types
&lt;/h2&gt;

&lt;p&gt;Overall: 99%, 100%, 93%, 91%, 79%. The ranking is right. It's still not enough to pick a model.&lt;/p&gt;

&lt;p&gt;Qwen-Image made 86 errors. In 40 of them it spelled the word correctly and then filled the image with text nobody asked for, often a scrambled copy of the prompt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5u1mpymzxqlf9opbd48.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5u1mpymzxqlf9opbd48.jpg" alt="Qwen-Image writes Reino, then eight lines of garbled Spanish" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Grok Imagine and Seedream fail on detail. Of Grok's 14 Arabic errors, 6 are a missing, extra or moved dot and 5 an extra letter. Of Seedream's 20, 9 are dots and 7 extra letters. In Japanese it's dakuten in the wrong place: Seedream turned ばくはつ into ばくばつ.&lt;/p&gt;

&lt;p&gt;The error type predicts what a second run does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;First image wrong&lt;/th&gt;
&lt;th&gt;Fixed by one retry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT Image&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nano Banana&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok Imagine&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seedream&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen-Image&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Detail errors are noise: about half disappear on a second try. Qwen's extra text is a habit and survives retries. So "Seedream 91%, Qwen 79%" looks like worse and a bit more worse, but the fixes are different. With Seedream, run it again. With Qwen, rewrite the prompt; rerunning just burns money.&lt;/p&gt;

&lt;p&gt;My take: a text-rendering benchmark that reports only an accuracy number is half a benchmark. That's why &lt;code&gt;error_types&lt;/code&gt; in the ledger is a fixed list (&lt;code&gt;extra_letter&lt;/code&gt;, &lt;code&gt;dots_diacritics&lt;/code&gt;, &lt;code&gt;extra_text&lt;/code&gt;, and so on), not a free-text note. Free-text notes don't add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data is public
&lt;/h2&gt;

&lt;p&gt;All 2,495 images in the ledger are in the dataset, including retries and test runs, one JSON record each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;978&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"death-metal-font"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VENOM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"035d561d24e21967dda5e64918b7d79a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GPT Image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-image-2.5-sunburst"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cos.aifont.ai/research/attempts/death-metal-font/death-metal-font-en-sunburst-s1-90094.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text_verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"incorrect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"wrong_letter"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rendered_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VENON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"human_reviewed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disputed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prompts sit in a separate file keyed by &lt;code&gt;prompt_hash&lt;/code&gt;. Images are linked, not shipped. License is CC BY 4.0, archived on Zenodo with DOI &lt;a href="https://doi.org/10.5281/zenodo.23815210" rel="noopener noreferrer"&gt;10.5281/zenodo.23815210&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you recompute anything, keep &lt;code&gt;attempt == "first"&lt;/code&gt;, &lt;code&gt;purpose == "published"&lt;/code&gt;, the current prompt version per style, and drop &lt;code&gt;disputed&lt;/code&gt;. &lt;code&gt;summary.json&lt;/code&gt; is already filtered that way.&lt;/p&gt;

&lt;p&gt;Corrections are the part I care about most. If you read Arabic, Japanese or any language in there and a verdict is wrong, open a "Wrong verdict" issue with the record id and what the image actually says. An accepted correction goes in as a new review under your GitHub handle, same append-only rule, and the next release recomputes the verdict. Arabic readers first.&lt;/p&gt;

&lt;p&gt;If you run a comparison like this without a native reader for some of the languages, say so in the results. We do.&lt;/p&gt;

&lt;p&gt;The full per-language results and more examples are in &lt;a href="https://aifont.ai/blog/best-image-generation-model-for-text" rel="noopener noreferrer"&gt;our report on the best image model for text&lt;/a&gt;, and the data is on &lt;a href="https://github.com/Funovate-Technology/aifont-text-rendering" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I tested 8 free background removers on the same 8 photos</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:32:20 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/i-tested-8-free-background-removers-on-the-same-8-photos-5el9</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/i-tested-8-free-background-removers-on-the-same-8-photos-5el9</guid>
      <description>&lt;p&gt;To find the best free background remover, I tested eight of them on the same eight photos on 19 October 2026: PixVibe, Backgroundless, Clipdrop, Pixelcut, Photoroom, Adobe Express, Canva and remove.bg. I only used what each tool gives you for free, scored every result, and checked where each tool sends your photo.&lt;/p&gt;

&lt;p&gt;I make PixVibe, one of the eight. I'm saying that first because it matters for everything below. PixVibe also came out joint last on cutout quality, and that is in the results too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Score /100&lt;/th&gt;
&lt;th&gt;Cutout quality /5&lt;/th&gt;
&lt;th&gt;Free result size&lt;/th&gt;
&lt;th&gt;Free batch&lt;/th&gt;
&lt;th&gt;Where your photo goes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;PixVibe&lt;/td&gt;
&lt;td&gt;88.6&lt;/td&gt;
&lt;td&gt;4.13&lt;/td&gt;
&lt;td&gt;Original (2496 × 1664)&lt;/td&gt;
&lt;td&gt;20 photos, free account&lt;/td&gt;
&lt;td&gt;Stays on your device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Backgroundless&lt;/td&gt;
&lt;td&gt;88.6&lt;/td&gt;
&lt;td&gt;4.13&lt;/td&gt;
&lt;td&gt;Original (2496 × 1664)&lt;/td&gt;
&lt;td&gt;40 photos, no account&lt;/td&gt;
&lt;td&gt;Stays on your device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Clipdrop&lt;/td&gt;
&lt;td&gt;73.1&lt;/td&gt;
&lt;td&gt;4.63&lt;/td&gt;
&lt;td&gt;1024 × 682&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Uploaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Pixelcut&lt;/td&gt;
&lt;td&gt;72.9&lt;/td&gt;
&lt;td&gt;4.38&lt;/td&gt;
&lt;td&gt;Original with a free account, 1080 × 720 without&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Uploaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Photoroom&lt;/td&gt;
&lt;td&gt;70.9&lt;/td&gt;
&lt;td&gt;4.38&lt;/td&gt;
&lt;td&gt;2400 × 1600&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Uploaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Adobe Express&lt;/td&gt;
&lt;td&gt;69.9&lt;/td&gt;
&lt;td&gt;4.38&lt;/td&gt;
&lt;td&gt;Original (2496 × 1664)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Saved to your Adobe cloud storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;69.9&lt;/td&gt;
&lt;td&gt;4.38&lt;/td&gt;
&lt;td&gt;Original (2496 × 1664)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Uploaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;remove.bg&lt;/td&gt;
&lt;td&gt;68.9&lt;/td&gt;
&lt;td&gt;4.38&lt;/td&gt;
&lt;td&gt;612 × 408&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Uploaded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The short version: Clipdrop cut out the cleanest edges. PixVibe and Backgroundless had the lowest cutout quality of the eight, and they still finished first because they are the only two that process the photo in your browser, give the full-size result for free and let you remove backgrounds from a batch of photos for free.&lt;/p&gt;

&lt;p&gt;PixVibe runs its AI model on the user's own device. The photo never reaches a server, so PixVibe has no server cost per image, and that is how it can offer the full size and batches for free. The trade-off today is quality: in this test, the server models cut hair, glass and spokes more cleanly than the models running in a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;p&gt;Eight photos, each built around one thing background removers get wrong. I generated them with &lt;a href="https://fontvibe.ai" rel="noopener noreferrer"&gt;FontVibe&lt;/a&gt; (an AI image generator, mostly used for text designs) so that every one of them can be published and downloaded: no stock licence, no real person's face. Each photo is 2496 × 1664 pixels. Download any of them and run your own test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Photo&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/01-curly-hair.jpg" rel="noopener noreferrer"&gt;Curly hair&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;loose strands blowing across a busy street&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/02-golden-dog.jpg" rel="noopener noreferrer"&gt;Golden retriever&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;long fur on autumn leaves of almost the same color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/03-sneakers.jpg" rel="noopener noreferrer"&gt;Sneakers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a product with a soft shadow on a table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/04-wine-glass.jpg" rel="noopener noreferrer"&gt;Wine glass&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;clear glass with a street visible through it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/05-bicycle.jpg" rel="noopener noreferrer"&gt;Bicycle&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;thin spokes with the background showing between them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/06-group.jpg" rel="noopener noreferrer"&gt;Three friends&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;several people and hair against a park&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/07-black-cat.jpg" rel="noopener noreferrer"&gt;Black cat&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a black cat on a dark grey sofa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pixvibe.ai/blog/best-free-background-remover/test-images/08-fontvibe-3d-text.jpg" rel="noopener noreferrer"&gt;3D text logo&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;letters with holes, a glow behind them and a reflective floor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free plans only.&lt;/strong&gt; No trials, no paid credits. Where a tool needs an account, I used a free one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One run per photo per tool,&lt;/strong&gt; with the default settings. PixVibe was scored on its best free mode, Ultra; its default mode, Standard, is covered below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same day, same machine:&lt;/strong&gt; 19 October 2026, Chrome on a Mac mini, home broadband.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result size&lt;/strong&gt; is the file a free user can actually download. I confirmed it with a downloaded file for six tools. For PixVibe I read the size of the result file the page produced, and for Pixelcut I checked the 1080 × 720 size in its download menu without an account and the original size with a free account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch&lt;/strong&gt; means more than one photo in, results out, without paying. I tried two photos at once on every tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I scored
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I scored&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cutout quality&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;Average of the eight photos (0 to 5 each), times 13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where your photo goes&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Processed on your device 15. Uploaded and deleted within a stated 30 days or less 9. Uploaded with no such promise 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free batch&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;20 or more photos per batch 8. Fewer than 20: 4. None: 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free result size&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Original 3. Long side 1500 px or more 2. 1000 px or more 1. Smaller 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No sign-up&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A single photo works without an account 3, otherwise 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No watermark&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;No watermark on the free download 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial use&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The terms clearly allow it for free users 3. Unclear or contradictory 1. Not allowed 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each cutout got 0 to 5: 5 means I could not find a flaw when zoomed in, 4 a small flaw you would not notice in normal use, 3 a visible flaw you would fix by hand, 2 a clear mistake such as a missing piece of the subject or a large patch of background, 1 unusable, 0 no result.&lt;/p&gt;

&lt;p&gt;I judged most cutouts from the full result on a magenta background, where anything left behind shows up. Two exceptions: Adobe Express and Pixelcut draw their result in an editor I could not export from without downloading, so I judged them from screenshots of that preview, about 750 and 600 pixels wide. remove.bg and Clipdrop were judged at the size they give for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which background removers upload your photo
&lt;/h2&gt;

&lt;p&gt;I watched the network requests each page made while it processed a photo. Six of the eight sent the photo to their own servers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What the page did while processing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PixVibe&lt;/td&gt;
&lt;td&gt;No image left the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgroundless&lt;/td&gt;
&lt;td&gt;No image left the browser; only analytics requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remove.bg&lt;/td&gt;
&lt;td&gt;Sent a request to &lt;code&gt;www.remove.bg/images&lt;/code&gt; and fetched the result from &lt;code&gt;o.remove.bg/downloads/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photoroom&lt;/td&gt;
&lt;td&gt;Sent a request to &lt;code&gt;sdk.photoroom.com/v1/segment&lt;/code&gt; about 10 seconds after the photo was chosen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixelcut&lt;/td&gt;
&lt;td&gt;Sent a request to &lt;code&gt;api2.pixelcut.app/image/matte/v1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipdrop&lt;/td&gt;
&lt;td&gt;Sent a request to &lt;code&gt;api.clipdrop.co/remove-background/v2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;Showed the result from &lt;code&gt;media-transformation.canva.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe Express&lt;/td&gt;
&lt;td&gt;Created a project in the account for every photo and read it from Adobe's cloud storage (&lt;code&gt;platform-cs&lt;/code&gt; on adobe.io)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of the six promises to delete a free user's photos within 30 days. remove.bg says it will "delete them shortly after" (&lt;a href="https://www.remove.bg/privacy" rel="noopener noreferrer"&gt;remove.bg Privacy Policy&lt;/a&gt;). Clipdrop keeps photos from users who have not signed up for two years (&lt;a href="https://clipdrop.co/privacy" rel="noopener noreferrer"&gt;Clipdrop Privacy Policy&lt;/a&gt;). Backgroundless states that images are never uploaded (&lt;a href="https://www.backgroundless.io/privacy" rel="noopener noreferrer"&gt;Backgroundless Privacy Policy&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Photo by photo
&lt;/h2&gt;

&lt;p&gt;Each image below shows the same crop of the same photo from all eight tools. Magenta is what the tool removed. Adobe Express and Pixelcut appear on their own checkerboard because those crops come from screenshots of their editors. The remove.bg and Clipdrop crops are enlarged from their free sizes, so they look softer.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Curly hair
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4ues4dj59iq3bj8uk8d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4ues4dj59iq3bj8uk8d.webp" alt="The same crop of curly hair cut out by PixVibe, Backgroundless, Pixelcut, Clipdrop, Photoroom, Adobe Express, Canva and remove.bg" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the curly hair photo, Clipdrop, Pixelcut, Adobe Express, Canva and remove.bg kept the finest strands blowing away from the head. PixVibe, Backgroundless and Photoroom lost some of the outermost strands. Every tool kept the face and the main shape of the hair.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Golden retriever on autumn leaves
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7615pp7ez6cr0bk24gwd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7615pp7ez6cr0bk24gwd.webp" alt="The same crop of a golden retriever's head cut out by the eight background removers" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the golden retriever photo, all eight tools kept the soft fur around the head. Two mistakes showed lower down, outside this crop: PixVibe left a few leaf fragments under the paws, and Photoroom cut off part of the right front paw.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Sneakers on a table
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ual7cwofmho4c5452on.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ual7cwofmho4c5452on.webp" alt="The same crop of a white sneaker cut out by the eight background removers" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the sneaker photo, all eight tools removed the table and the soft shadow cleanly, and every tool scored 5. Of the eight test photos, this is the closest to an ordinary product shot.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Wine glass
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjgubfq56sz7smt4sm8g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjgubfq56sz7smt4sm8g.webp" alt="The same crop of a wine glass: most tools keep the street seen through the glass as solid, Clipdrop removes it" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The wine glass photo separated the tools most. Clipdrop made the glass fully transparent, keeping the wine and the rim. Photoroom made it partly transparent. The other six kept the street seen through the glass as if it were printed on it. Adobe Express also kept the potted plant next to the glass as part of the subject, the only large mistake in the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Bicycle
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5evd8q9q5sekrooneyp6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5evd8q9q5sekrooneyp6.webp" alt="The same crop of a bicycle wheel: spokes kept or broken by each background remover" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the bicycle photo, Clipdrop, Photoroom and Adobe Express kept every spoke and cleared the gaps between them. PixVibe, Canva and remove.bg broke some spokes. Pixelcut left small fragments of background near the saddle, handlebar and pedal. Backgroundless lost many spokes and left smears where they had been.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Three friends in a park
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckheovx6jlw5vdyblsfn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckheovx6jlw5vdyblsfn.webp" alt="The same crop of three people's heads cut out by the eight background removers" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the photo of three friends in a park, all eight tools separated the people from the park and from each other, with clean hair edges. Every tool scored 5.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Black cat on a dark sofa
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftastrj44lrer29okqgx1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftastrj44lrer29okqgx1.webp" alt="The same crop of a black cat on a dark sofa cut out by the eight background removers" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the black cat photo, all eight tools kept the whole cat and all eight left a strip of sofa along the bottom edge. A dark subject on a dark background was the one case in this test that no tool solved.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. 3D text logo
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kveyghazcq863erter7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kveyghazcq863erter7.webp" alt="The same crop of colorful 3D letters cut out by the eight background removers" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the 3D text logo, all eight tools removed the background inside the holes of the letters, the glow behind them and the reflective floor. PixVibe's default Standard mode did not: it kept the glow and the background inside the "a". Its Ultra mode removed both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each tool, briefly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PixVibe (88.6)
&lt;/h3&gt;

&lt;p&gt;Free, processed on your device, full-size results, 20 photos per batch with a free account, and a single photo needs no account. Cutout quality 4.13 in Ultra mode. Ultra needs a browser with WebGPU and downloads about 223 MB of model data the first time. The default Standard mode scored 3.63: it failed the black cat and the 3D text. The terms allow commercial use of the results (&lt;a href="https://pixvibe.ai/terms" rel="noopener noreferrer"&gt;PixVibe Terms&lt;/a&gt;). Try it on the &lt;a href="https://pixvibe.ai/tools/remove-background" rel="noopener noreferrer"&gt;free background remover&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backgroundless (88.6)
&lt;/h3&gt;

&lt;p&gt;Free, processed on your device, no account even for batches of 40 photos. Each result downloads one PNG at a time for free; a ZIP of the batch is a paid feature (&lt;a href="https://www.backgroundless.io/faq" rel="noopener noreferrer"&gt;Backgroundless FAQ&lt;/a&gt;). Cutout quality 4.13, the same as PixVibe, with a weaker result on the bicycle and a stronger one on the dog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clipdrop (73.1)
&lt;/h3&gt;

&lt;p&gt;The cleanest cutouts in the test, 4.63, and the only tool that made the wine glass fully transparent. The free plan needs an account and scales every photo down before cutting it: my 2496 × 1664 photos came back at 1024 × 682, with an upgrade offered to keep the original. Free users get 20 removals per 24 hours (&lt;a href="https://clipdrop.co/pricing" rel="noopener noreferrer"&gt;Clipdrop pricing&lt;/a&gt;) and process one photo at a time; the page says "Free users can only process images one by one". Clipdrop's FAQ states its terms do not limit commercial use (&lt;a href="https://clipdrop.co/" rel="noopener noreferrer"&gt;Clipdrop FAQ&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Pixelcut (72.9)
&lt;/h3&gt;

&lt;p&gt;Cutout quality 4.38. Without an account the download is 1080 × 720; with a free account it is the original size. With a free account, the batch editor processed my two photos, then asked for Pro at "Download all". Its pricing page lists a commercial licence only on the paid plans (&lt;a href="https://www.pixelcut.ai/pricing" rel="noopener noreferrer"&gt;Pixelcut pricing&lt;/a&gt;), while a Pixelcut article says free results can be used commercially, so I scored commercial use as unclear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Photoroom (70.9)
&lt;/h3&gt;

&lt;p&gt;Cutout quality 4.38, with no account needed for a single photo and a 2400 × 1600 download without a watermark. It made the wine glass partly transparent, second only to Clipdrop, and it was the only tool to cut off part of the dog's paw. A free account can open Photoroom's batch editor, but the download button says Pro, and Photoroom's help centre states that batch export is limited to Pro, Max and Ultra (&lt;a href="https://help.photoroom.com/en/articles/11784338-what-is-the-batch-feature" rel="noopener noreferrer"&gt;Photoroom Help: What is the Batch feature&lt;/a&gt;). Free accounts cannot be used commercially (&lt;a href="https://help.photoroom.com/en/articles/12523455-free-accounts-and-commercial-use" rel="noopener noreferrer"&gt;Photoroom Help: Free accounts and commercial use&lt;/a&gt;). If you are looking for a Photoroom alternative for commercial work on a free plan, PixVibe, Backgroundless and Clipdrop allow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adobe Express (69.9)
&lt;/h3&gt;

&lt;p&gt;Needs an Adobe account. The free download was the full 2496 × 1664 with no watermark, and the result screen offers ready-made backgrounds. It takes one photo at a time; dropping two photos did nothing. Each photo became a project in the account. Adobe's pages do not agree with each other: its AI page says images generated in Adobe Express are safe for commercial use (&lt;a href="https://www.adobe.com/express/ai" rel="noopener noreferrer"&gt;Adobe Express AI&lt;/a&gt;), while its pricing FAQ lists remove background among premium features (&lt;a href="https://www.adobe.com/express/pricing" rel="noopener noreferrer"&gt;Adobe Express pricing&lt;/a&gt;), even though the tool worked on my free account. I scored commercial use as unclear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canva (69.9)
&lt;/h3&gt;

&lt;p&gt;Worked on a free Canva account from Canva's background remover page, with a full-size download and no watermark. Canva's own pages disagree on whether free accounts get it: the help centre says it is "included with paid plans" (&lt;a href="https://www.canva.com/help/background-remover/" rel="noopener noreferrer"&gt;Canva Help: Background Remover&lt;/a&gt;), and the feature page mentions a limited number of free downloads (&lt;a href="https://www.canva.com/features/background-remover/" rel="noopener noreferrer"&gt;Canva Background Remover&lt;/a&gt;). Selecting two uploads and choosing BG Remover opened a Canva Pro upgrade window. Its cutouts matched remove.bg almost exactly.&lt;/p&gt;

&lt;h3&gt;
  
  
  remove.bg (68.9)
&lt;/h3&gt;

&lt;p&gt;Cutout quality 4.38 with no account, but the free download is a 612 × 408 preview; the 2496 × 1664 original needs payment. The website takes one photo at a time, and its bulk page sends you to Canva Pro (&lt;a href="https://www.remove.bg/f/batch-editing" rel="noopener noreferrer"&gt;remove.bg Bulk Editing&lt;/a&gt;). The free service is for non-commercial use only (&lt;a href="https://www.remove.bg/tos" rel="noopener noreferrer"&gt;remove.bg Terms of Service&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  remove.bg closes on December 1: a remove bg alternative
&lt;/h2&gt;

&lt;p&gt;remove.bg's standalone website will no longer be available from 1 December 2026 at 9:00am CET, and its background removal is moving to Canva (&lt;a href="https://www.remove.bg/faq" rel="noopener noreferrer"&gt;remove.bg FAQ&lt;/a&gt;). In this test, Canva produced almost the same cutouts as remove.bg, at full size, but it needs an account. If you used remove.bg without an account, PixVibe, Backgroundless, Photoroom and Pixelcut also work without one, and all four gave a larger free download than remove.bg's 612 × 408 preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk background remover: who lets you batch for free
&lt;/h2&gt;

&lt;p&gt;I gave every tool two photos at once. Two of the eight processed and returned both for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backgroundless:&lt;/strong&gt; up to 40 photos per batch, no account. Download each result separately; the ZIP is paid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PixVibe:&lt;/strong&gt; up to 20 photos per batch with a free account, downloaded together as a ZIP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other six do not give you a free batch. remove.bg, Adobe Express and Clipdrop take one photo at a time. Pixelcut processed my two photos with a free account and asked for Pro at "Download all". Photoroom let a free account load photos into its batch editor, and its download button asks for Pro. Canva showed a Pro upgrade window as soon as I chose BG Remover for two photos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free plans compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Account for one photo&lt;/th&gt;
&lt;th&gt;Free download&lt;/th&gt;
&lt;th&gt;Watermark&lt;/th&gt;
&lt;th&gt;Commercial use on the free plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PixVibe&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Original size&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Allowed by the terms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgroundless&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Original size&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Allowed by the FAQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipdrop&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;1024 × 682&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Allowed by the FAQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixelcut&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Original with a free account, 1080 × 720 without&lt;/td&gt;
&lt;td&gt;None stated on its page&lt;/td&gt;
&lt;td&gt;Pages disagree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photoroom&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;2400 × 1600&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe Express&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Original size&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Pages disagree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Original size&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Pages disagree on free access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remove.bg&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;612 × 408&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The six free downloads I made had no watermark, PixVibe does not add one, and Pixelcut's background remover page states that its free download has none. Sizes are for my 2496 × 1664 test photos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: every score
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Photo&lt;/th&gt;
&lt;th&gt;PixVibe&lt;/th&gt;
&lt;th&gt;Backgroundless&lt;/th&gt;
&lt;th&gt;Clipdrop&lt;/th&gt;
&lt;th&gt;Pixelcut&lt;/th&gt;
&lt;th&gt;Photoroom&lt;/th&gt;
&lt;th&gt;Adobe Express&lt;/th&gt;
&lt;th&gt;Canva&lt;/th&gt;
&lt;th&gt;remove.bg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Curly hair&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden retriever&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sneakers&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wine glass&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bicycle&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three friends&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Black cat&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D text logo&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.63&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.38&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Quality 65&lt;/th&gt;
&lt;th&gt;Photo location 15&lt;/th&gt;
&lt;th&gt;Batch 8&lt;/th&gt;
&lt;th&gt;Size 3&lt;/th&gt;
&lt;th&gt;No sign-up 3&lt;/th&gt;
&lt;th&gt;No watermark 3&lt;/th&gt;
&lt;th&gt;Commercial 3&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PixVibe&lt;/td&gt;
&lt;td&gt;53.6&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;88.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgroundless&lt;/td&gt;
&lt;td&gt;53.6&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;88.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipdrop&lt;/td&gt;
&lt;td&gt;60.1&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;73.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixelcut&lt;/td&gt;
&lt;td&gt;56.9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;72.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photoroom&lt;/td&gt;
&lt;td&gt;56.9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;70.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe Express&lt;/td&gt;
&lt;td&gt;56.9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;69.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;56.9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;69.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remove.bg&lt;/td&gt;
&lt;td&gt;56.9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;68.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://pixvibe.ai/blog/best-free-background-remover" rel="noopener noreferrer"&gt;pixvibe.ai&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>design</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I gave 7 AI design tools the same 5 jobs on their free plans</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Fri, 16 Oct 2026 17:55:55 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/i-gave-7-ai-design-tools-the-same-5-jobs-on-their-free-plans-14jm</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/i-gave-7-ai-design-tools-the-same-5-jobs-on-their-free-plans-14jm</guid>
      <description>&lt;p&gt;Most "best AI font generator" lists I read were written from product pages. Very few show what the tools actually produced. So I picked five ordinary design jobs, typed the exact same prompt into each tool, took the first result, and scored it. Every image in this post is the raw output, and every prompt is printed so you can run it yourself.&lt;/p&gt;

&lt;p&gt;One thing to clear up before the results. When people search "AI font generator", most of them don't want a .ttf file. They want a word to look a certain way on a finished thing: a thumbnail, an invitation, a shirt. That's a text effect on a design, and that's what all seven of these tools make. If you need an installable font, skip to the section on font files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrdmy82qtdacujewah9c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrdmy82qtdacujewah9c.jpg" alt="Job 5 compared: the same gig poster prompt in seven tools" width="799" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Average score&lt;/th&gt;
&lt;th&gt;Text errors&lt;/th&gt;
&lt;th&gt;Free plan, as I saw it on Oct 12, 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;FontVibe&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;30 credits a day, up to 200 a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Leonardo&lt;/td&gt;
&lt;td&gt;8.7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;150 tokens a day (8 per image in my test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Recraft&lt;/td&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;30 credits a day, 100 a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Adobe Firefly&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3 images a day on the default model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Microsoft Designer&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;15 credits a month (1 per image)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5 designs used 77% of the month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Ideogram&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;30 credits (3 per low quality image)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Text errors" means a broken letter, a covered word or garbled text in the final image. Scores are out of 10.&lt;/p&gt;

&lt;p&gt;The short version: the gap at the top is small. Leonardo finished 0.1 behind FontVibe, and on two of the five jobs it scored higher. Where the tools really split was whether they did the specific lettering effect the prompt asked for. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;p&gt;Five jobs, from simple to hard. The difficulty is about the whole design: how much information it has to carry and how many layout rules it has to follow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bakery Instagram post&lt;/strong&gt; (simple)
&lt;code&gt;Square Instagram post for my bakery announcing "Fresh Croissants Every Saturday", warm and cozy feel, golden flaky lettering, with a basket of croissants&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube thumbnail&lt;/strong&gt; (simple)
&lt;code&gt;YouTube thumbnail for my survival challenge video, title "100 DAYS ALONE" in huge cracked-ice letters, snowy forest cabin in the background, leave the right third empty for my face&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kids' birthday invitation&lt;/strong&gt; (medium)
&lt;code&gt;Birthday party invitation for my daughter, "Mia's 7th Birthday" in glossy balloon letters, "Saturday, Nov 14 · 2 PM", "Sunny Park Pavilion", "RSVP by Nov 7", bright and playful, portrait card&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;T-shirt chest print&lt;/strong&gt; (medium)
&lt;code&gt;Front chest print for my streetwear brand, "NIGHTSHIFT" in wildstyle graffiti with drips, small "AFTER DARK CLUB · EST. 2026" underneath, a crescent moon graphic, isolated on plain white so I can send it to the printer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gig poster&lt;/strong&gt; (hard)
&lt;code&gt;A3 gig poster for my band "BURNING ORCHARD", band name in molten fire lettering as the main focus, "with special guests THE LOW TIDES", "SAT 14 NOV · DOORS 8 PM", "THE CELLAR · 22 MILL STREET", "TICKETS £12 ON THE DOOR", gritty rock atmosphere with burning trees&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free plan features only. Default model, default settings. I only set the aspect ratio closest to the job.&lt;/li&gt;
&lt;li&gt;One image per job. If a tool always returns more than one, I scored the best of that batch and said so.&lt;/li&gt;
&lt;li&gt;I didn't pick fonts or styles by hand in any tool, including mine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The scoring rubric
&lt;/h3&gt;

&lt;p&gt;Every image got a score from 1 to 10 on four things. I wrote down the reason for each deduction before giving the number.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I scored&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;What I looked at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Legibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;First a check for hard errors: garbled letters, cut off text, text covered by other elements, broken shapes. No hard errors means 7 to 10, depending on how easily the main line reads. Any hard error means 1 to 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Design execution and style match&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;Did it make the look the prompt asked for (pastry, ice, balloons, graffiti, fire), and how well: texture, colour, detail, and whether the whole image hangs together.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layout and completeness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;Can you read the main message at a glance, is every line of information from the prompt there, is the order clear, and did it follow the format rules (square, empty right third, white background, A3).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Appeal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Would a normal person be unable to make this themselves, and does it look like a designer made it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What each score band means:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Legibility&lt;/th&gt;
&lt;th&gt;Design execution&lt;/th&gt;
&lt;th&gt;Layout and completeness&lt;/th&gt;
&lt;th&gt;Appeal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9–10&lt;/td&gt;
&lt;td&gt;No errors, reads instantly even small&lt;/td&gt;
&lt;td&gt;Every part of the requested look is there and well made&lt;/td&gt;
&lt;td&gt;Clear hierarchy, all information present, careful layout&lt;/td&gt;
&lt;td&gt;Designer quality, memorable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7–8&lt;/td&gt;
&lt;td&gt;No errors, takes a second look&lt;/td&gt;
&lt;td&gt;The look is mostly there, small flaws&lt;/td&gt;
&lt;td&gt;Clear hierarchy, all information present, ordinary layout&lt;/td&gt;
&lt;td&gt;Looks professional but like a generic template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–6&lt;/td&gt;
&lt;td&gt;Not used (legibility is pass or fail)&lt;/td&gt;
&lt;td&gt;Only half done, or flat&lt;/td&gt;
&lt;td&gt;Unclear hierarchy, or one minor line missing&lt;/td&gt;
&lt;td&gt;Looks amateur&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1–4&lt;/td&gt;
&lt;td&gt;Any hard error&lt;/td&gt;
&lt;td&gt;Requested look missing, or cheap looking&lt;/td&gt;
&lt;td&gt;Key information missing or unreadable&lt;/td&gt;
&lt;td&gt;Not usable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The total is calculated, not judged: &lt;strong&gt;0.20 × legibility + 0.35 × design execution + 0.25 × layout + 0.20 × appeal&lt;/strong&gt;, rounded to one decimal. This is the same rubric we use internally to rate designs made on FontVibe, written before this test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every score
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Bakery&lt;/th&gt;
&lt;th&gt;Thumbnail&lt;/th&gt;
&lt;th&gt;Invitation&lt;/th&gt;
&lt;th&gt;T-shirt&lt;/th&gt;
&lt;th&gt;Poster&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FontVibe&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leonardo&lt;/td&gt;
&lt;td&gt;8.7&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;7.7&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recraft&lt;/td&gt;
&lt;td&gt;7.9&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe Firefly&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Designer&lt;/td&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideogram&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;6.1&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tested on October 12 and 13, 2026. These tools change every few weeks, and one try per job means luck plays a part. Treat the scores as a snapshot, not a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job by job
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Job 1: Bakery Instagram post
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpync64hxtayipz389d9v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpync64hxtayipz389d9v.jpg" alt="Job 1 compared: bakery Instagram post in seven tools" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The prompt asked for "golden flaky lettering". Only FontVibe made letters that look like baked pastry. Microsoft Designer got close. Leonardo used a script with a light pastry texture and added a shop name and a tagline on its own. Ideogram, Canva and Firefly all went with plain gold type, which looks fine but ignores the brief. Recraft's agent made four images; one spelled Saturday as "Savrdary", and the agent spotted it and dropped it itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 2: YouTube thumbnail
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnifbdxwibuakpgp55eii.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnifbdxwibuakpgp55eii.jpg" alt="Job 2 compared: YouTube thumbnail in seven tools" width="799" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This job has one layout rule: keep the right third empty for a face. FontVibe, Leonardo and Recraft followed it and made convincing ice. Firefly made the best looking ice of the group but put the cabin in the right third. Canva and Designer kept the space clear but used white distressed letters with no ice at all. Ideogram's ice was good, but the L in ALONE nearly disappears into the background, so at thumbnail size it reads "A ONE".&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 3: Kids' birthday invitation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx2t3t8g6q7xp0vln5kkj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx2t3t8g6q7xp0vln5kkj.jpg" alt="Job 3 compared: birthday invitation in seven tools" width="799" height="659"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Leonardo won this one. It put the date, place and RSVP on a card with small icons, which is what a parent would actually print. Designer was close behind. FontVibe's balloon letters were strong, but the date sits at the top and the venue at the bottom, split by a big balloon "7". Firefly wrote the date as "SATOV, 14" and invented an address, a phone number and an email. Canva used glitter type rather than balloon letters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 4: T-shirt chest print
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5d3mi6b620w615vwletr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5d3mi6b620w615vwletr.jpg" alt="Job 4 compared: T-shirt print in seven tools" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hard part here is "isolated on plain white so I can send it to the printer". Recraft was the only tool that returned a vector file, which a print shop can use as is. Canva and FontVibe made proper wildstyle lettering with drips and the moon. Firefly's lettering was excellent, but it drew the design on a T-shirt mockup, so it isn't a print file. Designer's wildstyle is so tangled that NIGHTSHIFT is hard to read. Ideogram's drips run over the "AFTER DARK CLUB" line.&lt;/p&gt;

&lt;p&gt;A note on FontVibe here, since I make it. My first request was routed to its single-image template mode, which produced a graffiti wall and missed the white background and the moon. I then asked in the same chat for design concepts and scored that result. The template mode isn't a design flow, so I didn't count it, but you should know it happened. We're changing the routing so a request like this goes to design concepts first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 5: Gig poster
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrdmy82qtdacujewah9c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrdmy82qtdacujewah9c.jpg" alt="Job 5 compared: gig poster in seven tools" width="799" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hardest job, and the closest race. Leonardo and Firefly both nailed molten fire type, burning trees and six lines of information in a clear order. FontVibe was close; its secondary lines are all one size, so the special guests line doesn't stand out. Ideogram's layout was one of the best, but it printed a garbled word, "SSCIDU", above the date. Canva's band name is a red outline face, not fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each tool, briefly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. FontVibe (8.8)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dchmgldpbh8ir1naugx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dchmgldpbh8ir1naugx.jpg" alt="FontVibe's five results, free plan features, Oct 12, 2026" width="798" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FontVibe reads the brief and shows you three design concepts first: layout, lettering, colours and the lines of text it plans to use. You pick one, fill in anything missing (like a shop name), and it renders. It was the only tool that did the requested lettering effect on all five jobs, with no text errors.&lt;/p&gt;

&lt;p&gt;Where it lost points: the invitation layout split the details, the poster's secondary text had no hierarchy, and the T-shirt request first went to the wrong mode. It's also not the fastest: about two minutes from prompt to image, including the concept step. A design cost me 33 credits (6 for concepts, 27 to render); with the cheapest image model it's 23. The free plan gives you 30 credits a day, up to 200 a month. Commercial use is allowed on every plan, including free.&lt;/p&gt;

&lt;p&gt;Good for: anyone who cares that the lettering looks exactly like what they asked for.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Leonardo (8.7)
&lt;/h3&gt;

&lt;p&gt;Leonardo's "Auto" model picked GPT Image 2 for all five jobs, and the free plan gives you that. Images came back in 30 to 40 seconds for 8 tokens each, so 150 daily tokens go a long way. It won the invitation and tied for the poster. It missed on the T-shirt, where it drew a simple black tag instead of wildstyle. Free images are public; private generation is a paid feature. The first try at the invitation was blocked by its content filter (I suspect the word "daughter"), with tokens refunded; the same prompt went through on retry.&lt;/p&gt;

&lt;p&gt;Good for: high quality one-off images if you're happy with less control over the lettering style.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Recraft (8.3)
&lt;/h3&gt;

&lt;p&gt;Recraft's chat agent runs one or two rounds on its own, checks spelling and layout, and tells you which image it thinks is best and why. It flagged two misspelled images on its own and told me not to use them. It's also the only tool that gave me a vector file for the shirt. The trade off is cost: each job used 6 to 13 credits because the agent decides how many rounds to run, and the free plan can't choose the image count. Recraft's own pricing page says free plan images are owned by Recraft, shown publicly and not licensed for commercial use.&lt;/p&gt;

&lt;p&gt;Good for: logos and print work where a vector file matters, if you're on a paid plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  4 (tie). Adobe Firefly (8.0)
&lt;/h3&gt;

&lt;p&gt;Firefly's home page defaults to Google's Gemini 3.1 model (Nano Banana 2), not Adobe's own Firefly model; you have to switch in the menu if you want the one Adobe calls commercially safe. I left the default. It produced the best ice and one of the best posters, but it added information nobody asked for and broke the invitation date. The free plan gave me three images a day on this model. Adobe doesn't publish the number.&lt;/p&gt;

&lt;p&gt;Good for: polished, photographic scenes. Check every line of text.&lt;/p&gt;

&lt;h3&gt;
  
  
  4 (tie). Microsoft Designer (8.0)
&lt;/h3&gt;

&lt;p&gt;The simplest interface here: describe, pick a colour palette, pick one of three sizes. It was also the slowest, at 1.5 to 2 minutes per image, and it only offers square, 2:3 and 3:2, so there's no true 16:9 thumbnail. Its invitation was the second best of the group; its T-shirt lettering was hard to read. 15 free credits a month, one per image.&lt;/p&gt;

&lt;p&gt;Good for: occasional use when you don't want to learn anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Canva (7.8)
&lt;/h3&gt;

&lt;p&gt;Canva AI returns an editable Canva design, not a flat image, which is a real advantage if you want to change a word later. Layout and information were solid on every job, with no text errors. But it used ordinary fonts from its library and ignored the lettering effect in four of five jobs (pastry, ice, balloons, fire). It did make good wildstyle for the shirt. On the free plan, five designs used 77% of my monthly AI allowance.&lt;/p&gt;

&lt;p&gt;Good for: people who already design in Canva and will edit the result by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Ideogram (7.5)
&lt;/h3&gt;

&lt;p&gt;Ideogram was the fastest, at 20 to 30 seconds, and its layouts often looked the most professional. But three of five images had text problems: a vanishing letter, drips over a line of text, and a garbled word. Its "Magic Prompt" is on by default and adds details on its own, which helped the bakery post and changed the lettering I asked for. The account showed 30 credits on the free plan, with low quality images at 3 credits each.&lt;/p&gt;

&lt;p&gt;Good for: fast drafts where you'll check and fix the text yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you need an actual font file
&lt;/h2&gt;

&lt;p&gt;None of the seven tools above make an installable font. If that's what you need, I tried &lt;strong&gt;Mixfont&lt;/strong&gt;, which does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswmqrdmlz42k2cqfka7s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswmqrdmlz42k2cqfka7s.jpg" alt="A specimen set in the Mixfont font, rendered from the TTF file it exported" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I asked for a bold, bubbly display font for the same birthday invitation. Two minutes and 20 of my 50 free credits later, it gave me a TTF file I could install. The letters are consistent across upper case, lower case and numbers, and a long word like "Extraordinary" sets cleanly. The standard tier has 72 glyphs, so some symbols are missing (there was no &amp;amp; or £). An extended set with 319 glyphs costs 50 credits.&lt;/p&gt;

&lt;p&gt;A font file and a text effect solve different problems. A font is for typing many words over and over. A text effect is for one word that has to look a particular way on one design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a template tool gives you
&lt;/h2&gt;

&lt;p&gt;For comparison, I also tried TextStudio, a long-running template based text effect site that doesn't use AI. You pick an effect and type your words. The free "Frosty" template turned "100 DAYS ALONE" into a clean icy line of text on a snowy background. That's all it does: one line, one style, no cabin, no layout. The fire template was a premium one and covered in "PREMIUM" watermarks, and the pages carry a lot of ads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free plans compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Free allowance (Oct 12, 2026)&lt;/th&gt;
&lt;th&gt;Time per image&lt;/th&gt;
&lt;th&gt;Notes from the test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FontVibe&lt;/td&gt;
&lt;td&gt;20 credits without an account; with one, 30 a day, up to 200 a month. A design costs 23 to 33&lt;/td&gt;
&lt;td&gt;About 2 min including concepts&lt;/td&gt;
&lt;td&gt;Commercial use allowed on all plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leonardo&lt;/td&gt;
&lt;td&gt;150 tokens a day; 8 per image on Auto&lt;/td&gt;
&lt;td&gt;30 to 40 s&lt;/td&gt;
&lt;td&gt;Free images are public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recraft&lt;/td&gt;
&lt;td&gt;30 credits a day, 100 a month, plus 30 bonus&lt;/td&gt;
&lt;td&gt;30 to 60 s&lt;/td&gt;
&lt;td&gt;Free images owned by Recraft, public, not for commercial use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe Firefly&lt;/td&gt;
&lt;td&gt;Daily free images; 3 a day on Gemini 3.1 in my test&lt;/td&gt;
&lt;td&gt;20 to 30 s&lt;/td&gt;
&lt;td&gt;Daily limit not published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Designer&lt;/td&gt;
&lt;td&gt;15 credits a month; 1 per image&lt;/td&gt;
&lt;td&gt;1.5 to 2 min&lt;/td&gt;
&lt;td&gt;Three sizes only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;Officially up to 20 AI uses a month; 5 designs used 77%&lt;/td&gt;
&lt;td&gt;30 to 40 s&lt;/td&gt;
&lt;td&gt;Output is an editable design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideogram&lt;/td&gt;
&lt;td&gt;30 credits on the account; 3 per low quality image&lt;/td&gt;
&lt;td&gt;20 to 30 s&lt;/td&gt;
&lt;td&gt;Magic Prompt on by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixfont&lt;/td&gt;
&lt;td&gt;50 credits; 20 per standard font&lt;/td&gt;
&lt;td&gt;About 2 min&lt;/td&gt;
&lt;td&gt;Exports a TTF file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I'm taking back to FontVibe
&lt;/h2&gt;

&lt;p&gt;Two things from this test go straight onto our list. A request with print rules, like "white background" and "a moon graphic", should go to design concepts, not the single-image template. And our secondary text needs clearer hierarchy on information heavy designs like posters and invitations. If you want to check our results against your own job, &lt;a href="https://fontvibe.ai/generate" rel="noopener noreferrer"&gt;FontVibe's design mode is here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: the full score sheet
&lt;/h2&gt;

&lt;p&gt;Legibility / design execution / layout / appeal, then the weighted total.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Bakery&lt;/th&gt;
&lt;th&gt;Thumbnail&lt;/th&gt;
&lt;th&gt;Invitation&lt;/th&gt;
&lt;th&gt;T-shirt&lt;/th&gt;
&lt;th&gt;Poster&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FontVibe&lt;/td&gt;
&lt;td&gt;9/9/9/9 = &lt;strong&gt;9.0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/9/9 = &lt;strong&gt;9.0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/8/8 = &lt;strong&gt;8.6&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7/9/9/9 = &lt;strong&gt;8.6&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/8/9 = &lt;strong&gt;8.8&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leonardo&lt;/td&gt;
&lt;td&gt;9/8/9/9 = &lt;strong&gt;8.7&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/9/9 = &lt;strong&gt;9.0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/9/9 = &lt;strong&gt;9.0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7/7/9/8 = &lt;strong&gt;7.7&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/9/9 = &lt;strong&gt;9.0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recraft&lt;/td&gt;
&lt;td&gt;9/7/8/8 = &lt;strong&gt;7.9&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/8/9/8 = &lt;strong&gt;8.5&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/8/8 = &lt;strong&gt;8.6&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7/8/9/8 = &lt;strong&gt;8.1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/8/9/8 = &lt;strong&gt;8.5&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe Firefly&lt;/td&gt;
&lt;td&gt;9/6/8/8 = &lt;strong&gt;7.5&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/6/8 = &lt;strong&gt;8.1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;3/9/7/8 = &lt;strong&gt;7.1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7/9/7/9 = &lt;strong&gt;8.1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/9/9 = &lt;strong&gt;9.0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Designer&lt;/td&gt;
&lt;td&gt;9/8/8/8 = &lt;strong&gt;8.2&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/5/9/7 = &lt;strong&gt;7.2&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/9/9/8 = &lt;strong&gt;8.8&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;3/8/9/7 = &lt;strong&gt;7.1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/8/9/8 = &lt;strong&gt;8.5&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;9/6/8/8 = &lt;strong&gt;7.5&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/5/8/8 = &lt;strong&gt;7.2&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/6/9/8 = &lt;strong&gt;7.8&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7/9/9/9 = &lt;strong&gt;8.6&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/6/9/9 = &lt;strong&gt;8.0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideogram&lt;/td&gt;
&lt;td&gt;9/6/9/9 = &lt;strong&gt;8.0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;3/8/9/8 = &lt;strong&gt;7.3&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;9/8/8/7 = &lt;strong&gt;8.0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;3/6/8/7 = &lt;strong&gt;6.1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;4/9/9/9 = &lt;strong&gt;8.0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The raw images, input screenshots and my written deductions for every score are kept on file. If you run one of these prompts and get something very different, email me at &lt;a href="mailto:hello@fontvibe.ai"&gt;hello@fontvibe.ai&lt;/a&gt; and I'll add it to this post.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://fontvibe.ai/blog/best-ai-text-effect-generators" rel="noopener noreferrer"&gt;fontvibe.ai&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>design</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI cost modelling: measure your own token counts, the published table was off by 2.3x</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:32:50 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/ai-cost-modelling-measure-your-own-token-counts-the-published-table-was-off-by-23x-2dop</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/ai-cost-modelling-measure-your-own-token-counts-the-published-table-was-off-by-23x-2dop</guid>
      <description>&lt;p&gt;We resell image generation, so we have to know what a generation costs us before we can price it. That turned out to be a lot harder than reading the pricing page, and the two times it bit us were both cases where a number looked authoritative and was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ratios taken from the published table were off by 2.3x on one tier
&lt;/h2&gt;

&lt;p&gt;The provider charges per token. Tokens per image depend on resolution and on a quality setting, and the pricing page does not list tokens per image. It points you at a calculator in the docs, built from a table of token counts for a 1024×1024 image.&lt;/p&gt;

&lt;p&gt;So we took our quality multipliers from that table. Low costs about 0.26 of Medium, High costs about 3.94 of Medium, price accordingly, move on.&lt;/p&gt;

&lt;p&gt;To be fair to the provider, that table is not wrong. It describes one configuration, correctly. The mistake was ours: we treated ratios from a single 1024×1024 case as if they were the model's quality multipliers everywhere.&lt;/p&gt;

&lt;p&gt;Then we measured. Nine generations, three resolutions, three quality settings, reading the token counts back out of our own usage records:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;derived from the published table&lt;/th&gt;
&lt;th&gt;measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;low ÷ medium&lt;/td&gt;
&lt;td&gt;0.258&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.112&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high ÷ medium&lt;/td&gt;
&lt;td&gt;3.94&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;High was close enough. Low cost us less than half of what we had assumed, so we had been pricing our cheapest tier as if it were 2.3 times more expensive than it actually is.&lt;/p&gt;

&lt;p&gt;The thing that convinced me the measurement was real and not noise was that the ratios held across resolutions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;low  ÷ medium:  0.112, 0.112, 0.111
high ÷ medium:  3.998, 3.999, 3.999
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different resolutions, same ratio to three decimal places. That is a parameter, not a sample. If those numbers had come back 0.11, 0.19, 0.14 I would have assumed measurement error and kept the published figure.&lt;/p&gt;

&lt;p&gt;That is the check I would suggest to anybody doing this. A cost ratio that is stable across conditions you varied is a real multiplier you can build a price on. One that moves around is either noise or evidence that your model has the wrong shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new model version at identical prices can still cost a quarter as much
&lt;/h2&gt;

&lt;p&gt;The second one is stranger, and we caught it before it shipped rather than after.&lt;/p&gt;

&lt;p&gt;The provider released a new generation of the same image model. Same price per token on every axis: image input, cached input, text input, output, all identical to the previous generation. Reading the pricing page, there is no change at all.&lt;/p&gt;

&lt;p&gt;But the quality tiers went from three named levels to five, and a third-party benchmark on a fixed 2048×1152 image reported:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;old model, &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1,413&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new model, &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;367&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same tier name, same price per token, about a quarter of the tokens.&lt;/p&gt;

&lt;p&gt;I could not reproduce that directly, because at the time we had not integrated the new model. What made me believe it was that it agreed with a number we had measured ourselves for a completely different reason. Our own internal figure was high ÷ medium = 4.00 on the old model. 1,413 ÷ 3.85 lands on 367. So the third-party claim reduces to "the new model's &lt;code&gt;high&lt;/code&gt; is roughly the old model's &lt;code&gt;medium&lt;/code&gt;", and that is a statement our own independent measurement supports.&lt;/p&gt;

&lt;p&gt;Worth being explicit about that reasoning, because it is the only reason I trusted a number I had not produced. A third-party benchmark you cannot reproduce is worth something if it agrees with a ratio you measured yourself for unrelated reasons. It is worth nothing on its own.&lt;/p&gt;

&lt;p&gt;The provider did document this, sort of. Their model page says the five quality levels are not direct equivalents of the previous model's levels, and recommends evaluating with your own prompts. That sentence reads like standard hedging. It is actually a spec change with a number attached, and the number is not in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters more if you resell
&lt;/h2&gt;

&lt;p&gt;If you are the end user of the API this is a pleasant surprise. You get the same work for less money, or you move up a tier.&lt;/p&gt;

&lt;p&gt;If you resell, it is a trap, and the trap is shaped like a one-line config change.&lt;/p&gt;

&lt;p&gt;Our users pick a quality tier and pay accordingly. High costs them four times what Medium costs. That multiple exists because High really did consume about four times the compute.&lt;/p&gt;

&lt;p&gt;Swap the model ID to the new generation and nothing else, and the same button still sends &lt;code&gt;quality=high&lt;/code&gt;, still charges four times, and now comes back with roughly what Medium used to produce. Nobody sees an error. The image still looks fine. The only thing that changed is that the customer is paying a 4× premium for something that is no longer 4× the work.&lt;/p&gt;

&lt;p&gt;That is not a performance regression you can file and get to later. The thing you sold is not the thing you delivered. We ended up leaving the old model on its own row, untouched, and adding the new ones as separate options with their own tiers, measured separately, specifically so that nobody could make that change by editing one field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smaller one that nearly got us
&lt;/h2&gt;

&lt;p&gt;Unrelated to models, same category of mistake.&lt;/p&gt;

&lt;p&gt;We had a cost baseline that turned out to be wrong, $0.0021 where it should have been $0.00233, and three derived figures computed from it sitting in the same document. Fixing the baseline was a two-character edit. The three derived numbers stayed exactly where they were, looking correct, being wrong. They needed to go 4.7 / 2.4 / 2.2 to 4.3 / 2.1 / 1.9 and they were not going to do that on their own.&lt;/p&gt;

&lt;p&gt;Derived numbers do not update themselves. They just keep being confidently wrong in a table somebody will read next quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we do now
&lt;/h2&gt;

&lt;p&gt;We do not build a cost model on a vendor's published example table. It is illustrative, it is often derived from one configuration, and in our case the ratios taken from it were off by 2.3× on a tier we actually sell. Our own usage records are the only source that describes what we are actually being charged for.&lt;/p&gt;

&lt;p&gt;Measure ratios, not absolutes, and vary something while you do it. A multiplier that holds steady across three resolutions is something you can price against. An absolute cost per image is out of date the next time anything changes.&lt;/p&gt;

&lt;p&gt;Treat "the tiers are not direct equivalents" as a breaking change, not a disclaimer. Vendors write that sentence in the voice of a footnote.&lt;/p&gt;

&lt;p&gt;And if you resell, put a measurement step between "new model is available" and "new model is live", because the config change that skips it is one line and reads as harmless.&lt;/p&gt;

&lt;p&gt;We do this for &lt;a href="https://fontvibe.ai" rel="noopener noreferrer"&gt;fontvibe.ai&lt;/a&gt;, which is where the numbers came from. The reasoning is not specific to images or to any one provider.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>api</category>
      <category>llm</category>
    </item>
    <item>
      <title>Image prompting lessons: stop describing the image, hand the model a reference sheet</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:10:01 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/image-prompting-lessons-stop-describing-the-image-hand-the-model-a-reference-sheet-l1k</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/image-prompting-lessons-stop-describing-the-image-hand-the-model-a-reference-sheet-l1k</guid>
      <description>&lt;p&gt;Thirteen rounds, one afternoon, one word. The word was ONLINE and I wanted it drawn in a particular chunky cartoon style. I have the whole log because I was writing down what changed each round, which turned out to be the only reason I can now say anything useful about it.&lt;/p&gt;

&lt;p&gt;Short version up front, since it isn't a twist: no amount of rewording got me there. I ended up drawing a reference sheet by hand and pointing at it.&lt;/p&gt;

&lt;p&gt;I picked letters for this on purpose. Most image work gets judged on taste and those arguments go nowhere, but "is that an R" has an answer, and so does "is that corner curved". You can be wrong in a way somebody can check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scorer was happy the entire time
&lt;/h2&gt;

&lt;p&gt;There was an aesthetic scorer in the loop. Every round it came back somewhere between 8.5 and 9.0. My own rating of the same images started around 3 and ended around 9.&lt;/p&gt;

&lt;p&gt;Round 1, scored 8.5:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpzqr31mh3pim8bq0ac9v.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpzqr31mh3pim8bq0ac9v.jpeg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had asked for small torn notches at the tips of the strokes. This has cracks across the entire body of every letter, plus a gloss I never mentioned.&lt;/p&gt;

&lt;p&gt;The scorer was not malfunctioning. It is a decent-looking image. It answers "is this good" and I was asking "is this the thing I described", and once your model is competent enough to produce non-garbage those two questions stop being related. If I had let the score gate anything I would have stopped at round 1 and shipped a picture of broken glass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounds 1 to 3 were aimed at something that was not there
&lt;/h2&gt;

&lt;p&gt;Here is the part I am least pleased about.&lt;/p&gt;

&lt;p&gt;I had looked at my reference at normal size, as a whole word, and read it as rounded letters with torn spikes poking out. So I spent three rounds describing torn spikes.&lt;/p&gt;

&lt;p&gt;There were no spikes. When I finally cropped out single letters and blew them up, the letterforms were polygons made of straight facet cuts, and the things I had read as spikes were just the corners where two straight edges met. Nothing in that reference was torn, ripped, or broken. Three rounds of careful prompt surgery, aimed at reproducing a defect in my own eyesight.&lt;/p&gt;

&lt;p&gt;Zoom in on one element before you write a single word about it. Whole-word at thumbnail size gives you the vibe and lies to you about the geometry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The word you picked is arguing with you
&lt;/h2&gt;

&lt;p&gt;This is the expensive one, and it happened three separate times with three different words.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;I wrote&lt;/th&gt;
&lt;th&gt;I meant&lt;/th&gt;
&lt;th&gt;I got&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chamfer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a flat angled corner&lt;/td&gt;
&lt;td&gt;a 3D bevel, side faces lit and everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;puzzle-piece-like&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;letters sitting tightly together&lt;/td&gt;
&lt;td&gt;jigsaw tabs. Actual ones.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paper cutout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;flat, no depth&lt;/td&gt;
&lt;td&gt;flat, with fold creases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu5gav59ryrx4qpa7r9wo.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu5gav59ryrx4qpa7r9wo.jpeg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is round 10. I got precisely what I typed and nothing I meant.&lt;/p&gt;

&lt;p&gt;The part that cost me rounds is that you can't fix this by pushing harder in the other direction. By round 8 the prompt carried "no 3D", "no bevel", "no side faces", "no internal lines", and the extrusion came back anyway, because &lt;code&gt;chamfer&lt;/code&gt; and &lt;code&gt;cut&lt;/code&gt; both point hard at "something was sliced here, show the marks". A pile of denials does not outvote the word itself. The word has to go.&lt;/p&gt;

&lt;p&gt;So when a constraint keeps losing, stop reinforcing the constraint. Go find what it is losing to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Say what it is, not how it got made
&lt;/h2&gt;

&lt;p&gt;Two phrasings that mean the same thing to a person:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;corners cut at irregular angles             # how it got made

the outer silhouette of every letter is an
irregular polygon, straight edges meeting
at sharp corners of varying length          # what it is
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first kept giving me cut marks. Seams inside the letter body, creases, and the little lit faces you would see if something really had been sliced through. The second gave me the shape and stopped.&lt;/p&gt;

&lt;p&gt;My guess is that a verb names an event, and pictures of events come with the mess the event leaves behind. I would not push that too hard as a theory. As a habit it has held up: when junk keeps appearing inside the subject that you never asked for, go read your prompt for verbs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Words about the room change the letters
&lt;/h2&gt;

&lt;p&gt;The prompt is split into a subject half and a scene half. I had assumed that split meant something.&lt;/p&gt;

&lt;p&gt;Material vocabulary in the scene half, describing the room, leaks onto the subject every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;edge&lt;/code&gt;, &lt;code&gt;core&lt;/code&gt;, &lt;code&gt;depth&lt;/code&gt; used about the environment, and the flat letters grow 3D extrusion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;voxel&lt;/code&gt; or &lt;code&gt;cube&lt;/code&gt;, and the letter edges go blocky and stair-stepped&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plastic&lt;/code&gt;, &lt;code&gt;toy&lt;/code&gt;, &lt;code&gt;brick&lt;/code&gt;, and the letters soften and round off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those were about the text. There's no compartment in there for "this clause is scenery". I ended up banning material words from the scene half entirely, objects and space and light only, and the drift stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving up and drawing it
&lt;/h2&gt;

&lt;p&gt;By round 11 the shape of the problem was obvious. Every fix moved something else. Tighten the silhouette, gloss comes back. Kill the gloss, the shape goes wrong. Fix the shape, a metaphor drags in creases. I was not converging, I was circling.&lt;/p&gt;

&lt;p&gt;There is also an embarrassing middle stretch I have skipped over, where I landed a version I liked at round 5, showed it, and was told round 4 had been closer. Rounds 6 through 8 were me trying to get back to something I had already had.&lt;/p&gt;

&lt;p&gt;So I drew the thing instead: a full A-Z and 0-9 sheet in the target style, one image. The prompt shrank to a pointer, roughly "follow the letterform skeleton in the reference, make the surface cleaner than the reference."&lt;/p&gt;

&lt;p&gt;Eleven rounds of problems went away in one go. Not gradually. The 3D, the seams, the gloss, the jigsaw tabs, the creases, all of it, in a single generation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdczzltmvoji11ob7qdi.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdczzltmvoji11ob7qdi.jpeg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, drawing the sheet was work. It was less work than eleven rounds, and I still have the sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing the reference could not do
&lt;/h2&gt;

&lt;p&gt;It locked the shape, which also meant I could not change anything.&lt;/p&gt;

&lt;p&gt;The last round wanted that exact shape with a glossier finish. Asked for in one sentence, the gloss dragged the 3D back in, same as round 8. Split across two sentences it behaved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The 2D silhouette of every letter must stay identical to the reference.
No change to the letterform shape itself, no extrusion, no added bevel geometry.

Within that unchanged shape, push the surface material to a glossy,
high-shine polished enamel finish.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical information. Two sentences. I don't have an explanation for why that matters and I am slightly suspicious of it, but it has reproduced every time I h&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2pvy5qhvz6u2wy6urt6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2pvy5qhvz6u2wy6urt6.jpeg" alt=" " width="800" height="600"&gt;&lt;/a&gt;ave needed to hold one property and move another, so I keep doing it.&lt;/p&gt;

&lt;p&gt;If you take one thing from this: text is fine for a couple of constraints and it falls apart once you need five of them true at the same time. That is not a prompting skill issue, it's the channel. When you notice yourself circling, the question to ask is whether the thing you are describing could just be handed over instead.&lt;/p&gt;

&lt;p&gt;This came out of building typeface generators, which live at &lt;a href="https://fontvibe.ai" rel="noopener noreferrer"&gt;fontvibe.ai&lt;/a&gt;. Nothing above needs them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>promptengineering</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Unicode normalization is not a deduplication key</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:29:47 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/unicode-normalization-is-not-a-deduplication-key-4c6d</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/unicode-normalization-is-not-a-deduplication-key-4c6d</guid>
      <description>&lt;p&gt;I had 84,000 kaomoji to deduplicate and a key that looked obviously correct:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key = "".join(unicodedata.normalize("NFKC", text).split())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Strip the whitespace, normalize the Unicode, group by the result. Every&lt;br&gt;
deduplication tutorial I have ever read says to normalize first. It took me one&lt;br&gt;
afternoon of reviewing the output to realize that this key had quietly decided&lt;br&gt;
&lt;code&gt;(╥_╥)&lt;/code&gt; and &lt;code&gt;(╥﹏╥)&lt;/code&gt; were the same record.&lt;/p&gt;

&lt;p&gt;They are two different faces. The wavy mouth is a harder cry than the flat one.&lt;/p&gt;
&lt;h2&gt;
  
  
  NFKC folds four characters that matter here
&lt;/h2&gt;

&lt;p&gt;NFKC is &lt;em&gt;compatibility&lt;/em&gt; normalization. The K stands for compatibility, and the&lt;br&gt;
point of it is to fold characters that exist in Unicode only for round-tripping&lt;br&gt;
with older encodings: the fullwidth Latin letters, the halfwidth katakana, the&lt;br&gt;
circled numbers, the ligatures. It maps them onto their "normal" equivalents so&lt;br&gt;
that &lt;code&gt;ｈｅｌｌｏ&lt;/code&gt; and &lt;code&gt;hello&lt;/code&gt; compare equal.&lt;/p&gt;

&lt;p&gt;That is correct when the text is prose and you want &lt;code&gt;Ｅ－ｍａｉｌ&lt;/code&gt; to match&lt;br&gt;
&lt;code&gt;E-mail&lt;/code&gt;. It stops being correct once the characters are the content themselves.&lt;br&gt;
Here is what it does to four characters that carry meaning in a kaomoji:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codepoint&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;NFKC result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;U+FE4F&lt;/td&gt;
&lt;td&gt;WAVY LOW LINE &lt;code&gt;﹏&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_&lt;/code&gt; (U+005F)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+FF3C&lt;/td&gt;
&lt;td&gt;FULLWIDTH REVERSE SOLIDUS &lt;code&gt;＼&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;\&lt;/code&gt; (U+005C)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+FF64&lt;/td&gt;
&lt;td&gt;HALFWIDTH IDEOGRAPHIC COMMA &lt;code&gt;､&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;、&lt;/code&gt; (U+3001)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+3000&lt;/td&gt;
&lt;td&gt;IDEOGRAPHIC SPACE &lt;code&gt;　&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&lt;/code&gt; (U+0020)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first one is the crying face. &lt;code&gt;﹏&lt;/code&gt; is a wavy mouth and &lt;code&gt;_&lt;/code&gt; is a flat one, and&lt;br&gt;
NFKC folds them together because one of them was originally a typesetting variant&lt;br&gt;
of the other. The second one is a pair of arms. &lt;code&gt;＼(^o^)／&lt;/code&gt; is a wide celebratory&lt;br&gt;
shrug, &lt;code&gt;\(^o^)/&lt;/code&gt; is a narrow one, and after NFKC they are the same string.&lt;/p&gt;

&lt;p&gt;The third one is a case where NFKC is &lt;em&gt;right&lt;/em&gt;. A fullwidth and a halfwidth&lt;br&gt;
ideographic comma really are the same punctuation mark, and &lt;code&gt;(≖、≖╬)&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;(≖､≖╬)&lt;/code&gt; really are one entry. That is what makes this hard. The normalization&lt;br&gt;
is not simply wrong. It is right about maybe a third of what it touches, and you&lt;br&gt;
can't tell which third without looking.&lt;/p&gt;
&lt;h2&gt;
  
  
  233 proposed merges, or 6,944
&lt;/h2&gt;

&lt;p&gt;This is easy to measure on a published dataset, so you do not have to take my word&lt;br&gt;
for it. The file below is the merged corpus, 82,109 entries, already deduplicated&lt;br&gt;
on exact string match. Every collision found here is a &lt;em&gt;proposed&lt;/em&gt; merge between&lt;br&gt;
two entries that a human previously decided were different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ud&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyfn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/kaomoji.jsonl.gz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;keyfn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;strip_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;with_nfkc&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip_only&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# (233, 500)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with_nfkc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;# (6944, 15038)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stripping whitespace alone proposes 233 merges. Adding NFKC proposes 6,944,&lt;br&gt;
covering 15,038 of the 82,109 entries. Auto-merging on that key deletes about&lt;br&gt;
8,000 records, and you never find out which ones, because a merge leaves no&lt;br&gt;
trace.&lt;/p&gt;

&lt;p&gt;Some of what it finds is genuinely useful. These are real groups from that run:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(╥_╥)              (╥﹏╥)
(・∀・)ノ            (･∀･)ノ
(╯°□°)╯︵ ┻━┻       (╯°□°）╯︵ ┻━┻
(T_T)              (T＿T)          (Ｔ＿Ｔ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The second row is a real duplicate. Halfwidth versus fullwidth katakana middle&lt;br&gt;
dot is not something anyone chose, it is an artifact of which IME the author was&lt;br&gt;
typing in. The third row is a table flip where one of the two parentheses came&lt;br&gt;
out fullwidth, which is also an accident. The first row is two different faces.&lt;br&gt;
The fourth row is three faces if you care about stroke weight and one face if you&lt;br&gt;
don't.&lt;/p&gt;

&lt;p&gt;There is no rule that separates those four rows. I tried several: character&lt;br&gt;
class, width consistency within a single entry, whether the fold lands in the&lt;br&gt;
mouth region. All of them got the first row wrong in one direction or the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whitespace carries pose information
&lt;/h2&gt;

&lt;p&gt;Stripping whitespace has the same problem on a smaller scale, because whitespace&lt;br&gt;
is load-bearing in this data:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ʕ·ᴥ·ʔ          bear, facing you
ʕ·ᴥ·　ʔ         bear, looking left
ʕ　·ᴥ·ʔ         bear, looking right

( •_•)&amp;gt;⌐■-■    before the sunglasses
(•_•)&amp;gt;⌐■-■     also before the sunglasses, typed by someone else
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The first three are three entries. The last two are one. Whether a given space is&lt;br&gt;
a pose or a typo is not recoverable from the string, and I don't think it is&lt;br&gt;
recoverable at all without knowing what the thing depicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three keys, one of which is allowed to merge
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;key_exact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;                       &lt;span class="c1"&gt;# byte-identical; safe to auto-merge
&lt;/span&gt;&lt;span class="n"&gt;key_loose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;      &lt;span class="c1"&gt;# whitespace only; PROPOSES merges
&lt;/span&gt;&lt;span class="n"&gt;key_nfkc&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# output field only
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;key_exact&lt;/code&gt; collisions merge automatically with no review. &lt;code&gt;key_loose&lt;/code&gt; collisions&lt;br&gt;
go to a human queue, and 233 groups is an afternoon. &lt;code&gt;key_nfkc&lt;/code&gt; is computed,&lt;br&gt;
stored as a field in the dataset, and never consulted by the merge pipeline.&lt;br&gt;
Consumers who want aggressive folding for search or for their own matching can&lt;br&gt;
have it. It just doesn't get to delete anything.&lt;/p&gt;

&lt;p&gt;That last key is the part I got wrong twice. My first instinct after it burned me&lt;br&gt;
was to rip it out entirely, which would have been the opposite mistake, because&lt;br&gt;
the folding is genuinely useful for recall. If somebody searches for &lt;code&gt;(╥﹏╥)&lt;/code&gt; you&lt;br&gt;
want the flat-mouthed one in the results too. So I kept computing it and took&lt;br&gt;
away its ability to destroy a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the queue small enough that you will actually read it
&lt;/h2&gt;

&lt;p&gt;Normalization tells you which records might be the same. Whether they are the&lt;br&gt;
same is a different question, and I am&lt;br&gt;
not sure that one has an algorithmic answer.&lt;/p&gt;

&lt;p&gt;So normalize to find candidates, keep the raw string as the only identity, and&lt;br&gt;
put a human in front of anything that would destroy a record. If the human queue&lt;br&gt;
is too big to review, the candidate key is too aggressive and needs tightening.&lt;br&gt;
Nobody reviews 6,944 groups. They trust the key and move on, which is exactly how&lt;br&gt;
the records disappear without anyone noticing.&lt;/p&gt;




&lt;p&gt;The corpus is on GitHub with the raw JSONL and the verification scripts, and on&lt;br&gt;
npm and PyPI as &lt;code&gt;kaomoji-dataset&lt;/code&gt; if you just want the data:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i kaomoji-dataset
pip install kaomoji-dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/Funovate/fontvibe-kaomoji" rel="noopener noreferrer"&gt;https://github.com/Funovate/fontvibe-kaomoji&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to look at the entries rather than the code, the browsable version is&lt;br&gt;
at &lt;a href="https://fontvibe.ai/tools/kaomoji" rel="noopener noreferrer"&gt;fontvibe.ai/tools/kaomoji&lt;/a&gt;. Data is CC BY&lt;br&gt;
4.0.&lt;/p&gt;

</description>
      <category>unicode</category>
      <category>datascience</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>I merged the seven public kaomoji collections. They barely overlap.</title>
      <dc:creator>jack</dc:creator>
      <pubDate>Tue, 22 Sep 2026 16:02:59 +0000</pubDate>
      <link>https://dev.to/jack_e2a413ec5d0bba216/i-merged-the-seven-public-kaomoji-collections-they-barely-overlap-3jag</link>
      <guid>https://dev.to/jack_e2a413ec5d0bba216/i-merged-the-seven-public-kaomoji-collections-they-barely-overlap-3jag</guid>
      <description>&lt;p&gt;I wanted a complete list of kaomoji, the &lt;code&gt;( ͡° ͜ʖ ͡°)&lt;/code&gt; and &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt; family. There isn't one.&lt;br&gt;
So I merged the public collections instead, and the merge turned up something I&lt;br&gt;
wasn't expecting: they hardly overlap at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Seven collections, 21 pairs, 0.3% median overlap
&lt;/h2&gt;

&lt;p&gt;The ones I found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/fdw/rofimoji" rel="noopener noreferrer"&gt;fdw/rofimoji&lt;/a&gt;, the biggest English-side set&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Allaman/emoji.nvim" rel="noopener noreferrer"&gt;Allaman/emoji.nvim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aoguai/rime_kaomoji_dict" rel="noopener noreferrer"&gt;aoguai/rime_kaomoji_dict&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mtripg6666tdr/Kaomoji_proj" rel="noopener noreferrer"&gt;mtripg6666tdr/Kaomoji_proj&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the Japanese IME dictionaries&lt;/li&gt;
&lt;li&gt;kaosute&lt;/li&gt;
&lt;li&gt;an older list of my own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across the 21 pairwise combinations the median Jaccard overlap is 0.3%. Four&lt;br&gt;
pairs share not a single entry. One pair clears 5%, rofimoji and emoji.nvim,&lt;br&gt;
which are both English-side Linux pickers, so that one is no surprise.&lt;/p&gt;

&lt;p&gt;The IME dictionaries are where most kaomoji actually live, and they share almost&lt;br&gt;
nothing with any of the English sets.&lt;/p&gt;

&lt;p&gt;Each entry in the merged file keeps the sources it came from, so you can check&lt;br&gt;
this yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;

&lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/kaomoji.jsonl.gz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;combinations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# 0.003  0.079  4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the collections is close to complete, and after seeing those numbers I&lt;br&gt;
understand why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage checked at run time
&lt;/h2&gt;

&lt;p&gt;Saying "this has everything" is not worth much on its own, so&lt;br&gt;
&lt;code&gt;scripts/verify_coverage.py&lt;/code&gt; downloads each upstream file when you run it and&lt;br&gt;
diffs it against the merged set. Nothing is precomputed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Upstream&lt;/th&gt;
&lt;th&gt;Entries&lt;/th&gt;
&lt;th&gt;Covered&lt;/th&gt;
&lt;th&gt;Missing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rofimoji&lt;/td&gt;
&lt;td&gt;1,562&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;emoji.nvim&lt;/td&gt;
&lt;td&gt;2,016&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rime_kaomoji_dict&lt;/td&gt;
&lt;td&gt;959&lt;/td&gt;
&lt;td&gt;99.4%&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kaomoji_proj&lt;/td&gt;
&lt;td&gt;2,166&lt;/td&gt;
&lt;td&gt;97.8%&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It prints all 57 misses, so you can look at them and disagree with me about what&lt;br&gt;
counts as a kaomoji. Three rounds of that took rofimoji from 93.0% to 100.0%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Labels, and where they fall down
&lt;/h2&gt;

&lt;p&gt;Deduplicated it comes to 82,109 entries. Around 70k carry emotion, intent and&lt;br&gt;
subject labels from a controlled vocabulary in en, ja, zh, es, pt and de, so&lt;br&gt;
嬉しい, 开心, feliz and happy return the same entries instead of whichever English&lt;br&gt;
word the original author happened to use.&lt;/p&gt;

&lt;p&gt;Three things worth knowing before you use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The labels are coarse. 69,679 labelled entries share only 605 distinct label
combinations, because they were applied in bundles rather than judged one at a
time. Fine for pulling up a mood, not for finding one specific face.&lt;/li&gt;
&lt;li&gt;About 15% is left unlabelled rather than guessed at.&lt;/li&gt;
&lt;li&gt;Most of the corpus is 顔文字＋セリフ, a face plus a line of Japanese dialogue,
which is not much use if you cannot read it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is why the English-language picker at&lt;br&gt;
&lt;a href="https://fontvibe.ai/tools/kaomoji" rel="noopener noreferrer"&gt;fontvibe.ai/tools/kaomoji&lt;/a&gt; shows 11,020, the&lt;br&gt;
subset that renders safely outside Japanese contexts, while the full 82,109 sits&lt;br&gt;
in the dataset and on the Japanese page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i kaomoji-dataset
pip &lt;span class="nb"&gt;install &lt;/span&gt;kaomoji-dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both read a gzip off disk, so they are Node and Python only for now. They will&lt;br&gt;
not bundle for a browser. The repo has the raw JSONL and a CSV as well:&lt;br&gt;
&lt;a href="https://github.com/Funovate/fontvibe-kaomoji" rel="noopener noreferrer"&gt;github.com/Funovate/fontvibe-kaomoji&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;106 of the entries are not collected from anywhere. I drew them, for feelings&lt;br&gt;
that had no kaomoji yet: 躺平, 社死, saudade, mamihlapinatapai. Those are CC0.&lt;/p&gt;

&lt;p&gt;If you know of a collection I missed, say so and I will run the coverage script&lt;br&gt;
against it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>python</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
