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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Short answer: watermark the image first with an image-drawing library such as Six Labors ImageSharp.Drawing, then upload the encoded result with HttpClient. These are separate jobs: ImageSharp changes pixels; HttpClient sends an HTTP request. The receiving API determines whether the request body must be multipart form data, raw image bytes, or a stream.

What the workflow actually does

A reliable implementation has five stages:

  1. Load the source image.
  2. Apply EXIF orientation and resize it to its final dimensions.
  3. Lay out and draw the watermark text.
  4. Encode the processed image as PNG, JPEG, or WebP.
  5. Send that encoded data using the upload format documented by the destination endpoint.

HttpClient does not render text or edit pixels. Conversely, ImageSharp does not decide how an HTTP service wants its request body. Keeping those responsibilities separate makes it easier to test the image output independently from network failures.

Packages and project requirements

Create a current .NET application and add the ImageSharp packages from the versions approved for your project:

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

ImageSharp.Drawing’s introduction states that projects directly depending on version 3.0.0 or later require a valid Six Labors license at build time. Check the current Six Labors licensing terms before shipping a production dependency; the applicable terms and price can change.

You also need a font available to the application. A desktop machine may have system fonts, but a container or Linux deployment might not. Bundle a font file that your project is permitted to distribute, or choose a known installed font and fail clearly when it is absent.

Complete example: render the watermark, then upload it

The following example shows the full separation. It normalizes orientation, resizes before drawing, places a semitransparent white mark with a dark outline in the lower-right corner, writes a WebP file, and then uploads that file as multipart form data. Replace the endpoint and field names with the contract for your service.

using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.PixelFormats;
using System.Net.Http.Headers;

const string inputPath = "original.jpg";
const string outputPath = "watermarked.webp";
const string watermarkText = "© Example Studio";
const string uploadUrl = "https://api.example.com/v1/images";

// Reuse this instance for the lifetime of the application.
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };

using (var image = Image.Load<Rgba32>(inputPath))
{
    // Orientation must be applied before positioning the mark.
    image.Mutate(ctx => ctx.AutoOrient());

    // Resize before drawing so the text is not softened by a later resample.
    const int maxWidth = 2400;
    if (image.Width > maxWidth)
    {
        var targetHeight = (int)Math.Round(image.Height * (maxWidth / (double)image.Width));
        image.Mutate(ctx => ctx.Resize(maxWidth, targetHeight));
    }

    var fontFamily = SystemFonts.Families.FirstOrDefault(f => f.Name == "Arial")
        ?? SystemFonts.Families.First();
    var font = fontFamily.CreateFont(Math.Max(18, image.Width / 45f), FontStyle.Bold);

    var layout = new RichTextOptions(font)
    {
        Origin = new PointF(image.Width - 36, image.Height - 28),
        HorizontalAlignment = HorizontalAlignment.Right,
        VerticalAlignment = VerticalAlignment.Bottom,
        WrappingLength = image.Width * 0.75f,
        LineSpacing = 1.1f
    };

    image.Mutate(ctx => ctx.DrawText(
        layout,
        watermarkText,
        Brushes.Solid(Color.White.WithAlpha(0.72f)),
        Pens.Solid(Color.Black.WithAlpha(0.75f), 2f)));

    image.Save(outputPath, new WebpEncoder { Quality = 88 });
}

using var form = new MultipartFormDataContent();
await using var file = File.OpenRead(outputPath);
using var fileContent = new StreamContent(file);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("image/webp");
form.Add(fileContent, "file", Path.GetFileName(outputPath));
form.Add(new StringContent("catalog-image-123"), "asset_id");

using var response = await http.PostAsync(uploadUrl, form);
var responseBody = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
Console.WriteLine(responseBody);

The API may require an authorization header, a different multipart field (such as image), or additional metadata. Those names are examples, not universal values.

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.

Positioning and text legibility

Use layout alignment instead of guessing text width

ImageSharp.Drawing’s text API uses Six Labors.Fonts. RichTextOptions carries the font, origin, wrapping length, and horizontal or vertical alignment. Anchoring at the lower-right lets the layout engine account for the actual string width, including when user-supplied text changes.

Choose contrast deliberately

A translucent white fill plus a darker outline works across many photographs. For mostly dark images, reverse the treatment or calculate a contrasting style. Keep the alpha value high enough to read but low enough that the underlying image remains visible.

Handle long or multilingual marks

Set WrappingLength when text can be long. Confirm that the selected font contains the required glyphs; a missing glyph can appear as a box. If the mark must never wrap, shorten or validate the input and reserve a safe margin around the anchor point.

Why orientation and resizing come first

Many camera images store their visual rotation in EXIF metadata rather than in already-rotated pixels. Calling AutoOrient() before drawing ensures “lower right” means the viewer’s lower right. Resizing after drawing forces the watermark through another interpolation step, which can make letter edges soft. Perform both operations before creating the font and layout values.

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

Choose the HTTP body that the endpoint specifies

Microsoft documents HttpContent as the representation of an HTTP entity body and its content headers. The relevant choices are distinct:

Endpoint contract C# content Typical headers or shape
Multipart upload with fields MultipartFormDataContent containing StreamContent or ByteArrayContent Boundary-managed multipart/form-data; include the field name and filename
Raw encoded image body ByteArrayContent or StreamContent Content-Type: image/webp, image/jpeg, or image/png
JSON with base64 data StringContent containing serialized JSON application/json; only use when the service explicitly requires it

Do not assume multipart works for every API. Read the destination’s upload documentation, including its required field name, maximum size, authentication scheme, and accepted media types.

Raw-byte upload example

var bytes = await File.ReadAllBytesAsync("watermarked.webp");
using var content = new ByteArrayContent(bytes);
content.Headers.ContentType = new MediaTypeHeaderValue("image/webp");
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("UPLOAD_TOKEN"));

using var response = await http.PutAsync(
    "https://api.example.com/v1/assets/catalog-image-123",
    content);
response.EnsureSuccessStatusCode();

For large files, prefer StreamContent so the entire image does not need to be held in a second byte array.

HttpClient lifetime, cancellation and retries

Microsoft’s API documentation says: “HttpClient is intended to be instantiated once per application, rather than per-use.” Register it with IHttpClientFactory in ASP.NET Core or reuse a long-lived instance in a worker. Creating one per image can exhaust sockets and lose connection pooling.

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

Network operations are I/O-bound, so use asynchronous methods. Pass a cancellation token from the request or job:

using var request = new HttpRequestMessage(HttpMethod.Post, uploadUrl)
{
    Content = form
};
using var response = await http.SendAsync(
    request,
    HttpCompletionOption.ResponseHeadersRead,
    cancellationToken);
response.EnsureSuccessStatusCode();

Retry only transient failures such as connection resets, 408, 429, and selected 5xx responses. Use exponential backoff, honor Retry-After, and avoid retrying a request when the server may have accepted it unless the API provides an idempotency key or an upload-status check.

Format, quality and memory decisions

  • PNG: lossless and suitable for screenshots, logos, or transparency, but often larger for photographs.
  • JPEG: broadly accepted for photos; choose a quality setting and expect some recompression.
  • WebP: often efficient for web delivery, provided the receiving API accepts it.

Encode once in the format the endpoint expects. Large full-resolution images consume memory during decode, mutation, and encode; enforce input-size and pixel-count limits for untrusted uploads. A maximum compressed file size alone is not enough because a highly compressed image can expand to a very large pixel buffer.

Troubleshooting common failures

The watermark is in the wrong corner

Apply AutoOrient() before layout and drawing. Also verify that your coordinate origin and alignment settings match the image dimensions after resizing.

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

Text is blurry

Resize and orient before drawing, not afterward. Ensure the output encoder is not applying an unexpectedly low quality setting, and avoid repeatedly decoding and re-encoding the same image.

Text is clipped or wraps unexpectedly

Increase the margin, set a suitable WrappingLength, and use alignment. Test the longest permitted string and fonts with non-Latin characters.

“Font family not found” or missing glyphs

System fonts differ between development machines and containers. Bundle and load a licensed font, or select from the fonts actually installed in the deployment image.

The server returns 400 or 415

A 400 commonly means a missing field, wrong field name, or invalid metadata. A 415 means the media type is unsupported. Compare the exact request body and Content-Type with the endpoint contract; do not change multipart to raw bytes (or vice versa) without checking the API.

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

The server returns 401 or 403

Check whether the service expects a bearer token, API-key header, query parameter, or a multipart field. Never log secret headers or embed credentials in source control.

The request times out

Increase the timeout only when the endpoint’s documented processing time justifies it. Reduce image dimensions, stream the request, and inspect server-side limits. A timeout does not prove the server did not receive the image, so use an idempotency key or status endpoint before retrying.

ImageSharp throws on a hostile file

Validate extension and content type but do not rely on either alone. Set pixel and file limits, catch decode exceptions, and reject images that exceed your service’s resource budget.

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 webpage rather than a local camera file, ScreenshotNeo can return a screenshot through one GET request. It removes cookie/consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads and cache hits are not billed, with the result identified by X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf. You can then feed the returned image into the same ImageSharp watermark step above.

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.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for parameters such as viewport, full-page capture, CSS selectors, custom CSS or JavaScript, waiting rules, headers, cookies, caching and asynchronous jobs.

The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account to get started.

Equivalent calls in Python and Node.js

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(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', data);

Production checklist

  • Confirm the ImageSharp.Drawing license terms for the version you deploy.
  • Normalize orientation and resize before drawing.
  • Use a font present and licensed in every deployment environment.
  • Set margins, wrapping, alpha and outline for the longest expected watermark.
  • Encode exactly once in an accepted format.
  • Match HttpContent, field names, headers and authentication to the endpoint contract.
  • Reuse HttpClient, use async I/O and propagate cancellation.
  • Limit pixels and bytes for untrusted images.
  • Log status, duration and a correlation ID, but never access tokens or image contents unless required.

FAQ

Can HttpClient add the watermark by itself?

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

Should I use WebClient.UploadFile instead?

No for new development. Microsoft marks WebClient, WebRequest and related APIs obsolete and advises using HttpClient.

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

Is ImageSharp.Drawing faster than SkiaSharp?

The cited documentation does not establish a fair benchmark or a universal winner. Select a library based on text layout, styling, formats, deployment support and licensing requirements, then benchmark your own image sizes and workload.

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.