Skip to content

Image Resize & Watermark — Swift source

Batch-resize images by width, height or percentage and stamp a text watermark with position, opacity and size control — entirely in your browser, nothing uploaded.

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Image Resize & Watermark — Swift (5.9, CoreGraphics + CoreText) port of the image-resize-and-watermark tool.

import CoreGraphics
import CoreText
import Foundation
import ImageIO
import UniformTypeIdentifiers

enum WatermarkError: Error { case unreadableInput, noGraphicsContext, encodeFailed }

// Mirrors the TS resizeAndWatermark: aspect-preserving resize, then a
// bottom-right watermark — white fill at 70% alpha over a dark outline —
// then a lossy WebP encode at 0.92.
func resizeAndWatermark(_ input: URL, _ output: URL, targetWidth: Int, watermark: String) throws {
    // 1. Decode.
    guard let src = CGImageSourceCreateWithURL(input as CFURL, nil),
          let image = CGImageSourceCreateImageAtIndex(src, 0, nil) else {
        throw WatermarkError.unreadableInput
    }

    // 2. Aspect-preserving size: h = round(h / w * targetWidth).
    let w = targetWidth
    let h = Int((Double(image.height) / Double(image.width) * Double(w)).rounded())

    // 3. Draw scaled (interpolation ~ imageSmoothingQuality 'high').
    let space = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
    guard let ctx = CGContext(
        data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: 0, space: space,
        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
    ) else { throw WatermarkError.noGraphicsContext }
    ctx.interpolationQuality = .high
    ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))

    // 4. Watermark. Core Text merges the TS strokeText + fillText: a negative
    //    kCTStrokeWidthAttributeName means "stroke AND fill" in one draw, with
    //    the width as a percentage of the font size (−4 ⇒ 4%).
    let fontSize = Int((Double(min(w, h)) * 6 / 100).rounded())
    let attrs = [
        kCTFontAttributeName: CTFontCreateUIFontForLanguage(.system, CGFloat(fontSize), nil)!,
        kCTForegroundColorAttributeName: CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.7),
        kCTStrokeColorAttributeName: CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.55),
        kCTStrokeWidthAttributeName: -4.0,
    ] as CFDictionary
    guard let attrString = CFAttributedStringCreate(nil, watermark as CFString, attrs) else {
        throw WatermarkError.noGraphicsContext
    }
    let line = CTLineCreateWithAttributedString(attrString)
    // CG's origin is bottom-left, so the TS baseline h-8 lands at y = 8.
    let textWidth = CTLineGetTypographicBounds(line, nil, nil, nil)
    ctx.textPosition = CGPoint(x: CGFloat(w) - textWidth - 8, y: 8)
    CTLineDraw(line, ctx)

    // 5. Encode WebP at 0.92 (kCGImageDestinationLossyCompressionQuality).
    guard let outImage = ctx.makeImage(),
          let dest = CGImageDestinationCreateWithURL(
              output as CFURL, UTType.webP.identifier as CFString, 1, nil
          ) else { throw WatermarkError.encodeFailed }
    CGImageDestinationAddImage(
        dest, outImage,
        [kCGImageDestinationLossyCompressionQuality: 0.92] as CFDictionary
    )
    guard CGImageDestinationFinalize(dest) else { throw WatermarkError.encodeFailed }
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →