<?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: Eric Cheung</title>
    <description>The latest articles on DEV Community by Eric Cheung (@eric_cheung_1030).</description>
    <link>https://dev.to/eric_cheung_1030</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3903644%2F1541f806-6631-484c-b89e-e4d08b9e77c3.png</url>
      <title>DEV Community: Eric Cheung</title>
      <link>https://dev.to/eric_cheung_1030</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eric_cheung_1030"/>
    <language>en</language>
    <item>
      <title>I Built a Watermark Remover — Here’s What I Actually Learned</title>
      <dc:creator>Eric Cheung</dc:creator>
      <pubDate>Wed, 29 Apr 2026 06:52:49 +0000</pubDate>
      <link>https://dev.to/eric_cheung_1030/i-built-a-watermark-remover-heres-what-i-actually-learned-4o86</link>
      <guid>https://dev.to/eric_cheung_1030/i-built-a-watermark-remover-heres-what-i-actually-learned-4o86</guid>
      <description>&lt;p&gt;I'd generate an image with Gemini, like it, want to drop it into a draft or mockup — and there was the visible watermark sitting right on top of the export. Not a huge deal, but annoying enough that I'd break flow every time. Opening Photoshop or GIMP for one overlay felt absurd. Cropping usually ruined the composition.&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.amazonaws.com%2Fuploads%2Farticles%2F3xo54rdjgjgdidpqoo91.png" 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.amazonaws.com%2Fuploads%2Farticles%2F3xo54rdjgjgdidpqoo91.png" alt=" " width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I spent a weekend building something for exactly that: &lt;strong&gt;&lt;a href="https://geminiwatermarkremover.ai/" rel="noopener noreferrer"&gt;Gemini Watermark Remover&lt;/a&gt;&lt;/strong&gt; — upload an image, remove the visible mark in-browser, download a clean PNG.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it and what I got wrong before I got it right.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first decision: do one thing
&lt;/h2&gt;

&lt;p&gt;I started with a constraint: no editor, no layers, no timeline, no format conversion, no "enhance" button. Just this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Upload → Remove the mark → Download a clean PNG.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. Every time I felt the urge to add something — batch mode, adjustment sliders, export options — I came back to that constraint and cut it.&lt;/p&gt;

&lt;p&gt;The constraint wasn't laziness. It was a product decision. Tools that do everything require users to think. Tools that do one thing let users just get on with their work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the browser, and why it actually mattered
&lt;/h2&gt;

&lt;p&gt;The core processing runs in the browser. No server upload, no queue, no storage policy. The image never leaves the tab.&lt;/p&gt;

&lt;p&gt;For a photo editor this might be a trade-off. For a small utility like this, it's the right default. People use it for drafts, client concepts, internal assets — images that probably shouldn't hit a random server in the first place.&lt;/p&gt;

&lt;p&gt;The pipeline is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File input → Image decode → Canvas render → Mark detection / region processing → Preview → PNG export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation is mostly standard Canvas API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadImageFromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ImageBitmap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please upload a valid image file.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&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;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;drawToCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImageBitmap&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;exportAsPng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Export failed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing glamorous. But getting upload → preview → export to feel seamless is the actual product. A clever removal algorithm doesn't help much if the UI is janky.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hard part isn't removing the mark
&lt;/h2&gt;

&lt;p&gt;Removing a visible overlay sounds simple. Detect the region, patch it. Done.&lt;/p&gt;

&lt;p&gt;The problem is what the mark is sitting on top of.&lt;/p&gt;

&lt;p&gt;Watermarks land on gradients, skin tones, compressed JPEG noise, AI-generated texture, dark backgrounds with subtle detail. If the patch looks blurry or slightly wrong, users notice immediately — even if they can't articulate why.&lt;/p&gt;

&lt;p&gt;The real goal isn't "remove the mark." It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Make the processed area look like nothing happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds obvious, but it pushes against a common temptation: over-processing. A lot of image tools try to "fix" things they weren't asked to fix — smooth skin, sharpen edges, boost contrast. I specifically didn't want that. The best result for this workflow is boring. The image should look untouched except for the mark being gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scope as a feature
&lt;/h2&gt;

&lt;p&gt;The first version only targets the visible Gemini overlay. Not every watermark on the internet, not arbitrary logos, not text burns.&lt;/p&gt;

&lt;p&gt;That focus does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The UI doesn't need a "configure the region" step — common case just works&lt;/li&gt;
&lt;li&gt;The processing logic can be tuned around a known pattern&lt;/li&gt;
&lt;li&gt;Users with the specific problem immediately understand what the tool is&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of the more useful mental shifts I've had with small tools: &lt;strong&gt;narrow products are easier to trust.&lt;/strong&gt; If a tool claims to do everything, I'm skeptical. If it claims to do one thing and does it well, I'll actually use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A note on what this tool doesn't do
&lt;/h2&gt;

&lt;p&gt;Google's Gemini images also carry &lt;strong&gt;SynthID&lt;/strong&gt; — an invisible, embedded watermark for AI provenance tracking. This tool doesn't touch that. It's about the visible overlay in your export, not invisible cryptographic signatures baked into the pixel data.&lt;/p&gt;

&lt;p&gt;Worth being explicit: this is for images you own, generated, or have permission to edit. Not for stripping attribution or bypassing content transparency systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The browser-first model and what it means for the business
&lt;/h2&gt;

&lt;p&gt;Running in the browser keeps infrastructure costs low, which matters a lot for an indie project. No per-image compute, no storage costs, no deletion policy to maintain.&lt;/p&gt;

&lt;p&gt;It also clarifies where paid features make sense: batch processing, higher-volume workflows, and any future server-side features can live in a paid tier. The free tool can stay fast and simple without subsidizing heavy usage.&lt;/p&gt;

&lt;p&gt;That's a cleaner model than gating the core utility behind an account from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;The things I want to improve are mostly at the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better handling of images with complex backgrounds where the mark overlaps important detail&lt;/li&gt;
&lt;li&gt;Mobile UX — Canvas processing on mobile can be slow and I haven't optimized it properly yet&lt;/li&gt;
&lt;li&gt;A before/after slider that's actually good (the current one is functional, not great)&lt;/li&gt;
&lt;li&gt;Some kind of quality indicator so the user knows when a result is uncertain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core promise stays the same: upload, clean, download, move on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual takeaways
&lt;/h2&gt;

&lt;p&gt;Three things I'd say to anyone building something like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraints are productive.&lt;/strong&gt; Deciding not to build something is a real engineering decision. It's usually the right one on v1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Runs in your browser" is a feature, not a footnote.&lt;/strong&gt; Privacy-by-default is something users care about, and it's worth building around intentionally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline matters as much as the algorithm.&lt;/strong&gt; A tool can have a solid core and still feel terrible if the upload, preview, and export experience is rough. Get those right first.&lt;/p&gt;




&lt;p&gt;You can try it at &lt;strong&gt;&lt;a href="https://geminiwatermarkremover.ai/" rel="noopener noreferrer"&gt;geminiwatermarkremover.ai&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Happy to hear from other developers building small AI workflow tools — what problems are you solving, and what trade-offs did you end up making?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
