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

Do it in two separate stages: first render the text onto the image with an imaging library such as Six Labors ImageSharp.Drawing; then encode that processed image and send it with HttpClient. HttpClient sends HTTP content, but it does not draw or watermark pixels. The receiving API determines whether your request must contain multipart form data, the raw encoded image, or another body format.

The example below targets a modern .NET console application and uses ImageSharp.Drawing for the image work. Adapt the final upload method to the contract of your own endpoint.

What you need before writing code

  • A .NET application with network access and an input image.
  • ImageSharp and ImageSharp.Drawing packages, plus a font available to the application.
  • The upload endpoint’s documentation: URL, authentication scheme, field name, accepted image formats, and required content type.
  • A writable output location if you want to keep a local copy of the watermarked file.

ImageSharp.Drawing 3.0.0 and later requires a valid Six Labors license at build time for projects that directly depend on it. Verify the current license terms for your project before shipping.

Install the image packages

From the project directory, add the packages (pin versions that your application has approved):

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
dotnet add package SixLabors.ImageSharp
dotnet add package SixLabors.ImageSharp.Drawing

The drawing API loads or creates an image, mutates it, and draws through ImageSharp’s drawing pipeline. Text layout is supplied by SixLabors.Fonts; RichTextOptions carries the font, origin, wrapping, and alignment settings.

Render a text watermark before uploading

Normalize orientation and resize to the final export dimensions before drawing. If you draw first and resize later, the text can be resampled and softened. If you draw before auto-orientation, the mark may end up in the wrong visual corner.

This method places a user-supplied string in the lower-right corner, adds a semitransparent white fill and a dark outline for contrast, and writes a WebP file. Change the encoder to JPEG or PNG when the receiving service requires another format.

using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;

static void CreateWatermarkedImage(
    string inputPath,
    string outputPath,
    string watermarkText,
    int outputWidth,
    int outputHeight)
{
    using Image image = Image.Load(inputPath);

    // Orientation must be corrected before positioning the mark.
    image.Mutate(ctx =>
    {
        ctx.AutoOrient();
        ctx.Resize(new ResizeOptions
        {
            Size = new Size(outputWidth, outputHeight),
            Mode = ResizeMode.Max
        });
    });

    FontFamily family = SystemFonts.Get("Arial");
    float fontSize = Math.Max(18, image.Width / 28f);
    Font font = family.CreateFont(fontSize, FontStyle.Bold);

    const float margin = 24;
    var options = new RichTextOptions(font)
    {
        Origin = new PointF(image.Width - margin, image.Height - margin),
        HorizontalAlignment = HorizontalAlignment.Right,
        VerticalAlignment = VerticalAlignment.Bottom,
        WrappingLength = image.Width - (margin * 2)
    };

    var fill = Color.White.WithAlpha(0.78f);
    var outline = Color.Black.WithAlpha(0.72f);

    image.Mutate(ctx =>
    {
        ctx.DrawText(options, watermarkText, fill, outline);
    });

    image.Save(outputPath);
}

CreateWatermarkedImage(
    inputPath: "input.jpg",
    outputPath: "watermarked.webp",
    watermarkText: "© Example Company",
    outputWidth: 1800,
    outputHeight: 1200);

The alignment settings anchor the text rather than requiring you to estimate its rendered width. WrappingLength is important when the text comes from a user: a long legal notice should wrap inside the image instead of running off the edge. For a short, fixed label you can omit wrapping, but keeping a bounded layout is safer for public input.

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

Choosing the visual treatment

  • Fill: a partially transparent white or black fill preserves some image detail while remaining visible.
  • Outline: a contrasting stroke helps the same watermark survive both light and dark backgrounds.
  • Font size: derive it from the final pixel dimensions, not the original file, so output sizes remain readable.
  • Position: use alignment and margins instead of hard-coded text-width calculations.

Encode the result for the endpoint

Once the image has been mutated, save it in the format the service accepts. ImageSharp chooses an encoder from the file extension in the simple example. For deterministic behavior, configure an explicit PNG, JPEG, or WebP encoder in your project and match the endpoint’s documented media type.

Keep the local output until the upload succeeds if you need a recovery path. A failed request should not force you to repeat the rendering step.

Upload with HttpClient

Microsoft defines HttpContent as the representation of the HTTP entity body and its content headers. The available forms include ByteArrayContent, StreamContent, and MultipartFormDataContent. Select one from the endpoint contract; no single upload shape works for every API.

Microsoft’s API documentation states: “HttpClient is intended to be instantiated once per application, rather than per-use.” Reuse one client through the application’s lifetime, and use asynchronous I/O for network calls.

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

Multipart form-data upload

Use this pattern when the API documents a file field such as image or file, possibly alongside text fields.

using System.Net.Http.Headers;

static readonly HttpClient Http = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(90)
};

static async Task UploadMultipartAsync(
    string endpoint,
    string imagePath,
    string accessToken,
    CancellationToken cancellationToken = default)
{
    await using FileStream stream = File.OpenRead(imagePath);
    using var form = new MultipartFormDataContent();
    using var file = new StreamContent(stream);
    file.Headers.ContentType = new MediaTypeHeaderValue("image/webp");
    form.Add(file, "image", Path.GetFileName(imagePath));

    using var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
    {
        Content = form
    };
    request.Headers.Authorization =
        new AuthenticationHeaderValue("Bearer", accessToken);

    using HttpResponseMessage response =
        await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
    string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
    response.EnsureSuccessStatusCode();
    Console.WriteLine(responseBody);
}

Do not manually set the multipart Content-Type boundary. MultipartFormDataContent generates it and adds it to the header. Replace image, the media type, and the authorization method with the names required by your service.

Raw image bytes upload

Some endpoints expect the encoded image itself as the request body rather than a multipart part.

static async Task UploadRawAsync(
    string endpoint,
    string imagePath,
    string accessToken,
    CancellationToken cancellationToken = default)
{
    byte[] bytes = await File.ReadAllBytesAsync(imagePath, cancellationToken);
    using var content = new ByteArrayContent(bytes);
    content.Headers.ContentType = new MediaTypeHeaderValue("image/webp");

    using var request = new HttpRequestMessage(HttpMethod.Put, endpoint)
    {
        Content = content
    };
    request.Headers.Authorization =
        new AuthenticationHeaderValue("Bearer", accessToken);

    using HttpResponseMessage response = await Http.SendAsync(request, cancellationToken);
    response.EnsureSuccessStatusCode();
}

For large files, prefer StreamContent so the whole image does not have to remain in a byte array. The HTTP verb, URL, authentication header, and media type above are examples; copy the exact values from your endpoint documentation.

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.

Handle errors without losing diagnostics

Call EnsureSuccessStatusCode only after reading the response body when the service returns useful validation details. Log status code and a bounded response excerpt, but never log bearer tokens, cookies, or image data that contains sensitive information.

End-to-end console example

A typical flow is:

  1. Load the source image.
  2. Auto-orient and resize it to its final dimensions.
  3. Draw the text with a bounded layout and contrasting colors.
  4. Save an encoded output file.
  5. Send that file using the request body shape required by the endpoint.
  6. Check the HTTP status and preserve the response body for troubleshooting.

Keep rendering and transport in separate methods. That separation lets you test watermark appearance without a network dependency and lets you switch from multipart to raw bytes without touching the drawing code.

Performance, reliability, and security considerations

Reuse and concurrency

Reuse a long-lived HttpClient rather than constructing one per image. For batches, limit concurrent requests to what the service and your network can sustain; unbounded parallelism can exhaust sockets, memory, or rate limits.

Retries

Retry only transient failures such as connection resets or selected 5xx responses, with exponential backoff and a maximum attempt count. Do not blindly retry 400-level validation errors, authentication failures, or non-idempotent operations unless the API documents an idempotency key.

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

Memory and file handling

Use streams for large images, dispose images, files, requests, and content, and write to a temporary file before replacing a final asset. Validate maximum dimensions and file sizes before decoding untrusted uploads; decompression bombs can consume far more memory than their compressed size.

Credentials and privacy

Read API keys from a secret store or environment variable, not source control. Use HTTPS, avoid writing original images to verbose logs, and consider whether EXIF metadata should be removed before distribution.

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

Troubleshooting common failures

The watermark is in the wrong corner

Apply AutoOrient() before resizing and drawing. Camera files can contain EXIF orientation rather than pixels rotated to their displayed direction.

The text is blurry

Resize before drawing and export at the final dimensions. Drawing at a large size and then shrinking the complete image resamples the watermark along with the photo.

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

The text is clipped or runs off the image

Set WrappingLength, use alignment, reduce the font size, and leave a margin. User-provided strings need more room than a fixed short label.

Font lookup fails on the server

Do not assume a desktop font exists in a container or Linux host. Install and deploy an approved font, or load a font file explicitly according to the Six Labors.Fonts documentation.

The server returns 400 or 415

Inspect the endpoint contract. A 400 commonly means the field name, required parameter, or JSON wrapper is wrong; a 415 means the media type or body shape is unsupported. Switch between multipart and raw content only when the API specifies it.

The server returns 401 or 403

Check the authorization scheme, token scope, expiration, and required headers. Confirm that the credential is being sent to the intended host and is not accidentally included in a redirect.

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

The request times out

Increase the timeout only after measuring image size and server behavior. Stream large files, avoid unnecessary source dimensions, and check whether the service documents an asynchronous upload or job endpoint.

WebClient examples conflict with current guidance

Microsoft marks WebClient, WebRequest, and related APIs as obsolete for new work and advises using HttpClient instead. See the HttpClient class documentation.

Or skip the browser setup

If the image you need is a webpage screenshot rather than a local photo, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF; it can accept consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

For a direct screenshot call, see the ScreenshotNeo API documentation:

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
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Features include full-page and element capture, device presets, custom CSS and JavaScript, waits, request blocking, headers and cookies, geolocation, PDF options, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API.

The Free plan includes 1,000 screenshots per month 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.

Frequently Asked Questions

Can I watermark an image using only HttpClient?

No. HttpClient transports HTTP content; an image-processing library must render the text into the pixels first.

Should the API receive JPEG, PNG, or WebP?

Use the format and media type required by that endpoint. The encoder and the HttpContent header must agree.

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

Is multipart always better than raw bytes?

Neither is universally better. Multipart is appropriate when the contract defines a file field; raw byte or stream content is appropriate when the image itself is the entity body.

Can I use a custom font?

Yes, provided the font is deployed and licensed for your application. Server environments often lack the fonts installed on a developer workstation.

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.