Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use HttpClient to download the source image, check the HTTP response, decode it with an image library, draw either text or a second image over it, and encode the result to a new file or response. The example below uses System.Drawing.Common because its drawing calls are familiar, but that implementation is supported only on Windows in .NET 6 and later. For Linux, macOS, containers, or cross-platform services, use a library such as ImageSharp, SkiaSharp, Windows Imaging Components, or Microsoft.Maui.Graphics and adapt the same pipeline.

A text watermark and a logo watermark are different operations: text uses a font and brush; a logo uses a decoded image, position, size, and usually transparency.

What the pipeline does

  1. Reuse one appropriately managed HttpClient instead of constructing one for every request.
  2. Request the remote image asynchronously and require a successful HTTP status.
  3. Stream large responses. Microsoft guidance treats downloads of 50 MB or more as candidates for streaming rather than default buffering.
  4. Decode the bytes or stream with a library supported by your target operating system.
  5. Calculate a position from the image dimensions, draw text or a logo, and encode an explicit output format.
  6. Dispose the response, streams, and image objects.

Do not trust a URL or a Content-Type header as proof that a response is an image. A server can return an HTML error page with status 200, so the decoder must be allowed to reject invalid data.

Windows implementation with System.Drawing.Common

In .NET 6 and later, Microsoft states that System.Drawing.Common is supported only on Windows. The following console example therefore targets a Windows deployment. It downloads an image, applies a semi-transparent text watermark in the lower-right corner, and writes a JPEG.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Project setup

Create a console project and add the package version appropriate for your application:

dotnet new console -n WatermarkDemo
cd WatermarkDemo
dotnet add package System.Drawing.Common

Set a Windows target framework in the project file if necessary:

<PropertyGroup>
  <TargetFramework>net8.0-windows</TargetFramework>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
</PropertyGroup>

Complete text-watermark program

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Net;

internal static class Program
{
    // One client for the lifetime of the process. Configure a timeout for your workload.
    private static readonly HttpClient Http = new()
    {
        Timeout = TimeSpan.FromSeconds(90)
    };

    private static async Task Main(string[] args)
    {
        if (args.Length < 2)
        {
            Console.Error.WriteLine("Usage: WatermarkDemo <image-url> <output-file> [watermark]");
            return;
        }

        var sourceUrl = args[0];
        var outputPath = args[1];
        var text = args.Length >= 3 ? args[2] : "© Example";

        using var response = await Http.GetAsync(
            sourceUrl,
            HttpCompletionOption.ResponseHeadersRead);
        response.EnsureSuccessStatusCode();

        await using var network = await response.Content.ReadAsStreamAsync();
        using var source = usingImage(network);
        using var canvas = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);

        using (var graphics = Graphics.FromImage(canvas))
        using (var font = new Font("Arial", Math.Max(12, source.Width / 45f), FontStyle.Bold))
        using (var brush = new SolidBrush(Color.FromArgb(170, Color.White)))
        using (var shadow = new SolidBrush(Color.FromArgb(150, Color.Black)))
        {
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

            var margin = Math.Max(12, source.Width / 50);
            var measured = graphics.MeasureString(text, font);
            var x = source.Width - measured.Width - margin;
            var y = source.Height - measured.Height - margin;
            var layout = new PointF(Math.Max(margin, x), Math.Max(margin, y));

            // A small offset makes light text readable on bright backgrounds.
            graphics.DrawString(text, font, shadow,
                new PointF(layout.X + 2, layout.Y + 2));
            graphics.DrawString(text, font, brush, layout);
        }

        // Choose the encoder deliberately; this example always emits JPEG.
        canvas.Save(outputPath, ImageFormat.Jpeg);
        Console.WriteLine($"Saved {outputPath}");
    }

    private static Bitmap usingImage(Stream input)
    {
        // Clone the decoded image so the decoder is not tied to the network stream.
        using var decoded = Image.FromStream(input, useEmbeddedColorManagement: true,
            validateImageData: true);
        return new Bitmap(decoded);
    }
}

ResponseHeadersRead lets your code begin consuming the body as a stream instead of asking HttpClient to buffer it first. The sample still holds a decoded bitmap in memory, because drawing requires random pixel access; for very large originals, enforce a maximum size, resize before drawing, or use a library designed for lower-memory processing.

Preserving transparency and choosing output

JPEG has no alpha channel. If the source or watermark needs transparency, save as PNG (for example, canvas.Save(outputPath, ImageFormat.Png)) or WebP through a library that supports it. Always match the file extension, encoder, and HTTP response Content-Type; do not merely rename a JPEG to .png.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Watermarking with a logo image

For a logo, decode a second image and call Graphics.DrawImage. Scale it relative to the source while preserving its aspect ratio, and place it inside a margin so it is not clipped.

using var logoResponse = await Http.GetAsync(logoUrl, HttpCompletionOption.ResponseHeadersRead);
logoResponse.EnsureSuccessStatusCode();
await using var logoStream = await logoResponse.Content.ReadAsStreamAsync();
using var logo = usingImage(logoStream);

var margin = Math.Max(16, source.Width / 40);
var targetWidth = Math.Max(1, source.Width / 5);
var targetHeight = (int)Math.Round(logo.Height * (targetWidth / (double)logo.Width));
var destination = new Rectangle(
    source.Width - targetWidth - margin,
    source.Height - targetHeight - margin,
    targetWidth,
    targetHeight);

graphics.DrawImage(logo, destination);

Draw the logo after creating the destination bitmap and before saving it. A PNG logo with an alpha channel gives the expected transparent result; a flattened logo will cover the photograph with its background.

Returning the result from an ASP.NET Core endpoint

Instead of writing a file, return the encoded bytes from an API. Keep the same status check, stream handling, and decoder validation. For modest images, a MemoryStream is straightforward:

[ApiController]
[Route("watermark")]
public sealed class WatermarkController : ControllerBase
{
    private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(90) };

    [HttpGet]
    public async Task Get(string url, CancellationToken cancellationToken)
    {
        using var response = await Http.GetAsync(url,
            HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        response.EnsureSuccessStatusCode();

        await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
        using var source = new Bitmap(Image.FromStream(input, true, true));
        using var output = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
        using (var g = Graphics.FromImage(output))
        using (var font = new Font("Arial", Math.Max(12, source.Width / 45f), FontStyle.Bold))
        using (var brush = new SolidBrush(Color.FromArgb(170, Color.White)))
        {
            g.DrawImage(source, 0, 0, source.Width, source.Height);
            g.DrawString("© Example", font, brush, 20, source.Height - font.Height - 20);
        }

        await using var encoded = new MemoryStream();
        output.Save(encoded, ImageFormat.Png);
        return File(encoded.ToArray(), "image/png");
    }
}

In a public endpoint, validate allowed schemes and hosts to reduce server-side request forgery risk, impose response-size and pixel-count limits, and avoid accepting arbitrary local paths. Pass cancellation tokens through every asynchronous operation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Cross-platform library choices

If the service runs on Linux, macOS, or a cross-platform container, do not deploy the System.Drawing.Common sample unchanged. Microsoft lists ImageSharp, SkiaSharp, Windows Imaging Components, and Microsoft.Maui.Graphics as alternatives. Their APIs, format support, native dependencies, package maintenance, and licenses are not identical, so verify the current documentation and terms for the version you select.

Decision What to verify
Operating system and target framework Whether the package supports your runtime and container image without unsupported native calls.
Input and output formats JPEG, PNG, WebP, animated formats, color profiles, and alpha-channel behavior.
Overlay needs Font loading, text measurement, alpha compositing, logo scaling, rotation, and clipping.
Deployment Native libraries, fonts installed in production, trimming/AOT behavior, and container size.
License and maintenance Current package license, supported versions, security updates, and release cadence.

Reliability, performance, and cost controls

  • Reuse the client: a long-lived, configured HttpClient avoids per-call construction and is the pattern described in Microsoft guidance.
  • Use asynchronous I/O: prefer GetAsync, GetStreamAsync, or SendAsync so request threads are not blocked while downloading.
  • Stream at 50 MB and above: Microsoft presents 50 MB or more as practical guidance for avoiding default buffering. Also set your own maximum response length.
  • Limit decoded pixels: a small compressed file can expand into a huge bitmap. Reject dimensions or estimated pixel memory above your service budget.
  • Set timeouts and cancellation: distinguish a timeout from an HTTP 404/500 and report an actionable error to callers.
  • Cache when appropriate: if the same source and watermark recur, cache the finished derivative using a key containing the source identity, watermark settings, and output format.
  • Be explicit about quality: JPEG quality, PNG compression, color profiles, and resizing affect output size and visual fidelity.

Troubleshooting

“The request succeeded, but the image decoder fails”

The body may be HTML, JSON, a bot-check page, or a truncated response. Log status, final URI, content length, and a bounded prefix of the content type; do not log credentials. Confirm the URL in a browser and inspect redirects.

“System.Drawing.Common is unsupported”

You are likely running on Linux, macOS, or a non-Windows container with .NET 6 or later. Move this code to a Windows deployment or port the drawing operations to ImageSharp, SkiaSharp, Windows Imaging Components, or Microsoft.Maui.Graphics after checking their current requirements.

“The watermark is clipped or unreadable”

Measure text with the actual font, calculate coordinates from Width and Height, and keep a margin. Add a shadow, translucent backing rectangle, or contrasting color instead of assuming every photograph has the same background.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“The output is black, loses alpha, or has the wrong extension”

Check the encoder. JPEG discards transparency; PNG preserves it. Ensure the returned MIME type and filename match the encoder you used.

“Memory usage spikes”

Stream the download, reject oversized responses before decoding, cap pixel dimensions, dispose every image promptly, and resize large originals before creating additional bitmaps.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your source is a web page rather than a direct image URL, ScreenshotNeo can produce the clean capture before your C# watermarking step. Its API removes cookie/consent banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.

One-call cURL example (see the ScreenshotNeo documentation for all parameters):

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const buffer = Buffer.from(await res.arrayBuffer());

ScreenshotNeo includes 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Security checklist

  • Allow only https (and explicitly approved hosts) for user-supplied URLs.
  • Block private IP ranges and cloud metadata endpoints to mitigate SSRF.
  • Use maximum download bytes, dimensions, and processing time.
  • Do not execute scripts or trust embedded metadata from untrusted images.
  • Keep API keys, cookies, and authorization headers out of logs and output files.

Frequently Asked Questions

Can I watermark an image without saving the original download to disk?

Yes. Read the HTTP response stream directly into the decoder, draw into a new bitmap, and encode to a file, response stream, or memory buffer. Streaming avoids a temporary source file, although the decoded bitmap still occupies memory.

Should the watermark be applied before or after resizing?

Apply it after the final resize when you want consistent relative size and sharp text. If you watermark first and then resize, the mark can become too small or blurred.

Why does a transparent logo show a white rectangle?

The logo was likely encoded without an alpha channel, or the destination encoder discarded transparency. Use an alpha-capable source and PNG/WebP output, then verify the selected library’s compositing behavior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.