Overview

pngpaste is a macOS command-line tool that pastes images from the system clipboard to files, similar to how pbpaste works for text content. Built with Swift 6.2 and a clean service-oriented architecture.

Multiple Input Formats

PNG, PDF, GIF, TIFF, JPEG, HEIC

Multiple Output Formats

PNG, GIF, JPEG, TIFF

Flexible Output

File, stdout, or base64

PDF Support

2x scale rasterization

Installation & Usage

Requirements

  • macOS 15.0 (Sequoia) or later
  • Swift 6.2 or later (for building from source)

Install via Script

The easiest way to install pngpaste is via the install script. It automatically detects your architecture (Intel or Apple Silicon) and downloads the appropriate binary.

Terminal One-line installation
# Install via curl (recommended)
curl -fsSL https://raw.githubusercontent.com/yriveiro/pngpaste/main/scripts/install.sh | bash

# Or with explicit confirmation skip
curl -fsSL https://raw.githubusercontent.com/yriveiro/pngpaste/main/scripts/install.sh | bash -s -- --yes

# Custom install directory
curl -fsSL https://raw.githubusercontent.com/yriveiro/pngpaste/main/scripts/install.sh | bash -s -- --bin-dir ~/.local/bin

# Verify installation
pngpaste --version

Build from Source

Clone the repository and build using Swift Package Manager. The release build is optimized for performance.

Debug build

For development and testing

Release build

Optimized for production use

Terminal Build from source
# Clone the repository
git clone https://github.com/yriveiro/pngpaste.git
cd pngpaste

# Build (debug)
swift build

# Build (release)
swift build -c release

# Run directly
swift run pngpaste output.png

# Install to /usr/local/bin
cp .build/release/pngpaste /usr/local/bin/

Basic Usage

Copy an image to your clipboard (e.g., screenshot with Cmd+Shift+4 or copy from any application), then use pngpaste to save it.

Output Modes

  • File: Save to a specific path
  • Stdout: Pipe binary output
  • Base64: Output as base64 string
Terminal Usage examples
# Save clipboard image to PNG file
pngpaste screenshot.png

# Save as JPEG (format determined by extension)
pngpaste photo.jpg

# Save as GIF
pngpaste animation.gif

# Output to stdout (pipe to another command)
pngpaste - | convert - -resize 50% smaller.png

# Output as base64 (useful for embedding)
pngpaste -b | pbcopy

# Combine with other tools
pngpaste - | base64 | curl -X POST -d @- api.example.com/upload

Error Handling

pngpaste provides clear error messages when something goes wrong. Exit code 0 indicates success, non-zero indicates an error.

Common Errors

  • No image: Clipboard is empty or contains text
  • Unsupported format: Clipboard contains unsupported image type
  • Write failure: Cannot write to the specified path
Terminal Error handling
# Check if clipboard has an image
if pngpaste /tmp/test.png 2>/dev/null; then
    echo "Image saved successfully"
else
    echo "No image on clipboard"
fi

# Example error messages:
# pngpaste: no image data found on the clipboard
# pngpaste: failed to write to '/path': Permission denied
# pngpaste: clipboard contains unsupported image format

Architecture

flowchart LR
    subgraph CLI["CLI Layer"]
        A[PngPaste]
    end
    
    subgraph Protocols["Protocol Abstractions"]
        P1[ClipboardReading]
        P2[ImageRendering]
        P3[OutputWriting]
    end
    
    subgraph Services["Service Implementations"]
        S1[ClipboardService]
        S2["ImageRenderService
@MainActor"]
        S3[OutputService]
    end
    
    subgraph Output["Output Destinations"]
        O1[File]
        O2[stdout]
        O3[base64]
    end
    
    A --> P1
    A --> P2
    A --> P3
    
    P1 -.-> S1
    P2 -.-> S2
    P3 -.-> S3
    
    S1 -->|NSPasteboard| IMG[/"PNG, TIFF, HEIC,
JPEG, GIF, PDF"/]
    S2 -->|Render| FMT[/"PNG, GIF,
JPEG, TIFF"/]
    S3 --> O1
    S3 --> O2
    S3 --> O3
          

Design Principles

The project follows a clean service-oriented architecture with protocol-based dependency injection:

  • Separation of Concerns: Each service has a single responsibility
  • Protocol Abstractions: Services implement protocols for testability
  • Dependency Injection: Services are injected with default implementations
  • Swift Concurrency: Uses Swift 6 typed throws and @MainActor
PngPaste.swift Pipeline orchestration
private func performPaste(
    mode: OutputMode,
    format: OutputFormat,
    clipboardService: some ClipboardReading = ClipboardService(),
    renderService: some ImageRendering = ImageRenderService(),
    outputService: some OutputWriting = OutputService()
) async throws(PngPasteError) {
    let image = try clipboardService.readImage()
    let imageType = try clipboardService.imageType(for: image)
    let data = try await renderService.render(image, imageType: imageType, as: format)

    try outputService.write(data, to: mode)
}

Command Line Interface

PngPaste

@main struct AsyncParsableCommand

The main command-line interface for pngpaste. Similar to how pbpaste works for text, pngpaste extracts images from the macOS clipboard and saves them to files or outputs them to stdout.

Command Options

-b, --base64

Output to stdout as base64 encoded PNG

<output-path>

Output file path (use '-' for binary stdout)

PngPaste.swift Lines 1-27
import AppKit
import ArgumentParser

@main
struct PngPaste: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "pngpaste",
        abstract: "Paste PNG into files, much like pbpaste does for text.",
        discussion: """
        Supported input formats: PNG, PDF, GIF, TIF, JPEG, HEIC
        Supported output formats: PNG, GIF, JPEG, TIFF
        Output format is determined by the file extension, defaulting to PNG.
        """,
        version: "1.0.0"
    )

    @Flag(name: .short, help: "Output to stdout as base64 encoded PNG")
    var base64 = false

    @Argument(help: "Output file path (use '-' for binary stdout)")
    var outputPath: String?
}

run()

async throws

Executes the pngpaste command. Determines the output mode and format from command-line arguments, then performs the paste operation.

mutating func run() async throws
Throws

CleanExit.helpRequest if no output mode is specified, or ExitCode.failure if the paste operation fails.

PngPaste.swift Lines 35-48
mutating func run() async throws {
    guard let mode = determineOutputMode() else {
        throw CleanExit.helpRequest()
    }

    let format = determineOutputFormat(for: mode)

    do {
        try await performPaste(mode: mode, format: format)
    } catch {
        fputs("pngpaste: \(error.description)\n", stderr)
        throw ExitCode.failure
    }
}

determineOutputMode()

Determines the output mode based on command-line arguments. Evaluates the base64 flag and outputPath argument to determine whether output should be written to a file, stdout as binary, or stdout as base64-encoded data.

private func determineOutputMode() -> OutputMode?
Returns

The determined OutputMode, or nil if no valid output destination was specified.

PngPaste.swift Lines 83-93
private func determineOutputMode() -> OutputMode? {
    if base64 {
        return .base64
    }

    guard let path = outputPath else {
        return nil
    }

    return path == "-" ? .stdout : .file(path: path)
}

determineOutputFormat(for:)

Determines the output format based on the output mode. For file output, extracts the format from the file extension. For stdout and base64 modes, defaults to PNG format.

private func determineOutputFormat(for mode: OutputMode) -> OutputFormat
mode OutputMode

The output mode to determine the format for.

Returns

The appropriate OutputFormat for the given mode.

PngPaste.swift Lines 102-109
private func determineOutputFormat(for mode: OutputMode) -> OutputFormat {
    switch mode {
    case let .file(path):
        OutputFormat(fromFilename: path)
    case .stdout, .base64:
        .png
    }
}

Models

OutputFormat

enum Sendable

Defines the supported output image formats. Provides conversion utilities between file extensions and AppKit bitmap types.

Cases

  • .png - PNG format (default)
  • .gif - GIF format
  • .jpeg - JPEG format (0.9 compression)
  • .tiff - TIFF format

Properties

var bitmapType: NSBitmapImageRep.FileType
var displayName: String
Models/OutputFormat.swift Lines 1-28
import AppKit

enum OutputFormat: String, CaseIterable, Sendable, Equatable {
    case png
    case gif
    case jpeg
    case tiff

    private static let jpegCompressionQuality: Double = 0.9

    var bitmapType: NSBitmapImageRep.FileType {
        switch self {
        case .png: .png
        case .gif: .gif
        case .jpeg: .jpeg
        case .tiff: .tiff
        }
    }

    var displayName: String {
        rawValue.uppercased()
    }
}

OutputFormat Initializers

Creates an output format from file extensions. Performs case-insensitive matching and supports common extension variations (e.g., both "jpg" and "jpeg" map to .jpeg).

init(fromExtension ext: String)
init(fromExtension:)

Creates format from extension string without dot

init(fromFilename filename: String)
init(fromFilename:)

Extracts extension from filename path

Models/OutputFormat.swift Lines 49-71
init(fromExtension ext: String) {
    switch ext.lowercased() {
    case "gif":
        self = .gif
    case "jpg", "jpeg":
        self = .jpeg
    case "tif", "tiff":
        self = .tiff
    default:
        self = .png
    }
}

init(fromFilename filename: String) {
    let ext = URL(fileURLWithPath: filename).pathExtension
    self.init(fromExtension: ext)
}

OutputMode

enum Sendable

Defines the output destinations for image data.

Cases

  • .file(path: String) - Write to file at path
  • .stdout - Write binary to stdout
  • .base64 - Write base64 encoded to stdout
Models/OutputMode.swift Complete file
enum OutputMode: Sendable, Equatable {
    case file(path: String)
    case stdout
    case base64
}

ImageType

enum Sendable

Classifies source images to determine the appropriate rendering strategy. PDF images require special rasterization during rendering.

Cases

  • .bitmap - Standard bitmap images
  • .pdf - PDF images requiring rasterization
Models/ImageType.swift Complete file
enum ImageType: Sendable {
    case bitmap
    case pdf
}

PngPasteError

enum Error Sendable

Defines all error cases that can occur during the paste operation. Implements CustomStringConvertible for user-friendly messages.

Cases

  • .noImageOnClipboard - No image data found
  • .unsupportedImageFormat - Format not supported
  • .conversionFailed(format:) - Conversion error
  • .writeFailure(path:reason:) - File write error
  • .stdoutWriteFailure(reason:) - Stdout write error
Models/PngPasteError.swift Complete file
import Foundation

enum PngPasteError: Error, CustomStringConvertible, Sendable, Equatable {
    case noImageOnClipboard
    case unsupportedImageFormat
    case conversionFailed(format: String)
    case writeFailure(path: String, reason: String)
    case stdoutWriteFailure(reason: String)

    var description: String {
        switch self {
        case .noImageOnClipboard:
            "no image data found on the clipboard"
        case .unsupportedImageFormat:
            "clipboard contains unsupported image format"
        case let .conversionFailed(format):
            "failed to convert image to \(format)"
        case let .writeFailure(path, reason):
            "failed to write to '\(path)': \(reason)"
        case let .stdoutWriteFailure(reason):
            "failed to write to stdout: \(reason)"
        }
    }
}

Protocols

ClipboardReading

protocol Sendable

Defines the interface for reading image data from the system clipboard. Supports multiple image formats including PNG, TIFF, HEIC, JPEG, GIF, and PDF.

Required Methods

func readImage() throws(PngPasteError) -> NSImage
readImage()

Reads and returns an image from the system pasteboard.

func imageType(for image: NSImage) throws(PngPasteError) -> ImageType
imageType(for:)

Determines the image type classification for rendering strategy.

Protocols/ClipboardReading.swift Complete file
import AppKit

protocol ClipboardReading: Sendable {
    func readImage() throws(PngPasteError) -> NSImage

    func imageType(for image: NSImage) throws(PngPasteError) -> ImageType
}

ImageRendering

protocol @MainActor Sendable

Defines the interface for rendering images to various output formats. Marked @MainActor because it requires AppKit drawing operations.

Required Methods

func render(_ image: NSImage, imageType: ImageType, as format: OutputFormat) throws(PngPasteError) -> Data
render(_:imageType:as:)

Renders an image to the specified output format. PDF images are rasterized at 2x scale.

Protocols/ImageRendering.swift Complete file
import AppKit

@MainActor
protocol ImageRendering: Sendable {
    func render(
        _ image: NSImage,
        imageType: ImageType,
        as format: OutputFormat
    ) throws(PngPasteError) -> Data
}

OutputWriting

protocol Sendable

Defines the interface for writing data to various output destinations. Supports writing to files, standard output (binary), or standard output as base64.

Required Methods

func write(_ data: Data, to mode: OutputMode) throws(PngPasteError)
write(_:to:)

Writes data to the specified output destination.

Protocols/OutputWriting.swift Complete file
import Foundation

protocol OutputWriting: Sendable {
    func write(_ data: Data, to mode: OutputMode) throws(PngPasteError)
}

Services

ClipboardService

struct ClipboardReading

A service that reads image data from the macOS system pasteboard. Supports reading images in PNG, TIFF, HEIC, JPEG, GIF, and PDF formats with automatic format detection.

Supported Pasteboard Types

  • .png - PNG images
  • .tiff - TIFF images
  • public.heic - HEIC images
  • public.jpeg - JPEG images
  • com.compuserve.gif - GIF images
  • .pdf - PDF documents

Methods

func readImage() throws(PngPasteError) -> NSImage
Services/ClipboardService.swift Lines 1-42
import AppKit

struct ClipboardService: ClipboardReading {
    private static let supportedTypes: [NSPasteboard.PasteboardType] = [
        .png,
        .tiff,
        NSPasteboard.PasteboardType("public.heic"),
        NSPasteboard.PasteboardType("public.jpeg"),
        NSPasteboard.PasteboardType("com.compuserve.gif"),
        .pdf,
    ]

    func readImage() throws(PngPasteError) -> NSImage {
        let pasteboard = NSPasteboard.general

        if let availableType = pasteboard.availableType(from: Self.supportedTypes),
           let data = pasteboard.data(forType: availableType),
           let image = NSImage(data: data),
           isValidImage(image) {
            return image
        }

        if pasteboard.canReadItem(withDataConformingToTypes: NSImage.imageTypes),
           let image = NSImage(pasteboard: pasteboard),
           isValidImage(image) {
            return image
        }

        throw .noImageOnClipboard
    }
}

ClipboardService.imageType(for:)

Determines the image type classification for the given image. Inspects the image's representations to determine whether it should be treated as a bitmap or PDF image. PDF images require special rasterization.

func imageType(for image: NSImage) throws(PngPasteError) -> ImageType
image NSImage

The image to analyze.

Returns

.pdf if image contains a PDF representation, .bitmap otherwise.

Services/ClipboardService.swift Lines 53-76
func imageType(for image: NSImage) throws(PngPasteError) -> ImageType {
    guard isValidImage(image) else {
        throw .unsupportedImageFormat
    }

    if image.representations.contains(where: { $0 is NSPDFImageRep }) {
        return .pdf
    }

    if image.representations.contains(where: { $0 is NSBitmapImageRep }) {
        return .bitmap
    }

    if image.tiffRepresentation != nil {
        return .bitmap
    }

    throw .unsupportedImageFormat
}

private func isValidImage(_ image: NSImage) -> Bool {
    image.size.width > 0 && image.size.height > 0 && !image.representations.isEmpty
}

ImageRenderService

struct @MainActor ImageRendering

A service that renders images to various output formats. Handles both bitmap and PDF source images, using appropriate rendering strategies for each type. PDF images are rasterized at 2x scale factor for improved quality.

Properties

pdfScaleFactor CGFloat = 2.0

Scale factor for PDF rasterization

Methods

func render(_ image: NSImage, imageType: ImageType, as format: OutputFormat) throws(PngPasteError) -> Data
Services/ImageRenderService.swift Lines 1-39
import AppKit

@MainActor
struct ImageRenderService: ImageRendering {
    private let pdfScaleFactor: CGFloat = 2.0

    func render(
        _ image: NSImage,
        imageType: ImageType,
        as format: OutputFormat
    ) throws(PngPasteError) -> Data {
        let data: Data? = switch imageType {
        case .bitmap:
            renderBitmap(image, as: format)
        case .pdf:
            renderPDF(image, as: format)
        }

        guard let data else {
            throw .conversionFailed(format: format.displayName)
        }

        return data
    }
}

ImageRenderService.renderBitmap(_:as:)

Renders a bitmap image to the specified output format. First attempts direct representation conversion for efficiency. If that fails, falls back to creating a bitmap representation from the image's TIFF data.

private func renderBitmap(_ image: NSImage, as format: OutputFormat) -> Data?
image NSImage

The bitmap image to render.

format OutputFormat

The target output format.

Services/ImageRenderService.swift Lines 50-66
private func renderBitmap(_ image: NSImage, as format: OutputFormat) -> Data? {
    if let data = NSBitmapImageRep.representationOfImageReps(
        in: image.representations,
        using: format.bitmapType,
        properties: format.encodingProperties
    ), !data.isEmpty {
        return data
    }

    guard let tiffData = image.tiffRepresentation,
          let bitmapRep = NSBitmapImageRep(data: tiffData)
    else {
        return nil
    }

    return bitmapRep.representation(using: format.bitmapType, properties: format.encodingProperties)
}

ImageRenderService.renderPDF(_:as:)

Renders a PDF image to the specified output format by rasterizing it. Creates a bitmap representation at 2x the PDF's native resolution, draws the PDF content onto a white background, and encodes the result in the target format.

private func renderPDF(_ image: NSImage, as format: OutputFormat) -> Data?

Implementation Details

  • Uses 8 bits per sample, 4 samples per pixel (RGBA)
  • Creates context in deviceRGB color space
  • Fills with white background before drawing
  • Saves and restores graphics state
Services/ImageRenderService.swift Lines 77-120
private func renderPDF(_ image: NSImage, as format: OutputFormat) -> Data? {
    guard let pdfRep = image.representations
        .compactMap({ $0 as? NSPDFImageRep }).first else {
        return nil
    }

    let scaledWidth = Int(pdfRep.bounds.width * pdfScaleFactor)
    let scaledHeight = Int(pdfRep.bounds.height * pdfScaleFactor)

    guard scaledWidth > 0, scaledHeight > 0 else { return nil }

    guard let bitmapRep = NSBitmapImageRep(
        bitmapDataPlanes: nil,
        pixelsWide: scaledWidth,
        pixelsHigh: scaledHeight,
        bitsPerSample: 8,
        samplesPerPixel: 4,
        hasAlpha: true,
        isPlanar: false,
        colorSpaceName: .deviceRGB,
        bytesPerRow: 0,
        bitsPerPixel: 0
    ) else { return nil }

    NSGraphicsContext.saveGraphicsState()
    defer { NSGraphicsContext.restoreGraphicsState() }

    guard let context = NSGraphicsContext(bitmapImageRep: bitmapRep) else {
        return nil
    }

    NSGraphicsContext.current = context
    NSColor.white.setFill()
    NSRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight).fill()

    let drawRect = NSRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight)
    pdfRep.draw(in: drawRect)

    return bitmapRep.representation(using: format.bitmapType, properties: format.encodingProperties)
}

OutputService

struct OutputWriting

A service that writes image data to various output destinations. Supports writing to files, standard output as binary data, or standard output as base64-encoded data.

Methods

func write(_ data: Data, to mode: OutputMode) throws(PngPasteError)
  • writeToFile(_:path:) - Atomic file write
  • writeToStdout(_:) - Binary stdout write
  • writeBase64ToStdout(_:) - Base64 stdout write
Services/OutputService.swift Lines 1-26
import Foundation

struct OutputService: OutputWriting {
    func write(_ data: Data, to mode: OutputMode) throws(PngPasteError) {
        switch mode {
        case let .file(path):
            try writeToFile(data, path: path)
        case .stdout:
            try writeToStdout(data)
        case .base64:
            try writeBase64ToStdout(data)
        }
    }
}

OutputService Write Methods

Internal methods for writing data to different destinations.

private func writeToFile(_ data: Data, path: String) throws(PngPasteError)
writeToFile(_:path:)

Uses atomic writing to ensure data integrity. Path is normalized via URL standardization.

private func writeToStdout(_ data: Data) throws(PngPasteError)
writeToStdout(_:)

Writes binary data directly to FileHandle.standardOutput.

private func writeBase64ToStdout(_ data: Data) throws(PngPasteError)
writeBase64ToStdout(_:)

Base64 encodes data before writing, suitable for text-based contexts.

Services/OutputService.swift Lines 38-73
private func writeToFile(_ data: Data, path: String) throws(PngPasteError) {
    let url = URL(fileURLWithPath: path).standardized

    do {
        try data.write(to: url, options: .atomic)
    } catch {
        let reason = (error as NSError).localizedDescription
        throw .writeFailure(path: path, reason: reason)
    }
}

private func writeToStdout(_ data: Data) throws(PngPasteError) {
    let stdout = FileHandle.standardOutput

    do {
        try stdout.write(contentsOf: data)
    } catch {
        throw .stdoutWriteFailure(reason: error.localizedDescription)
    }
}

private func writeBase64ToStdout(_ data: Data) throws(PngPasteError) {
    let base64Data = data.base64EncodedData()
    try writeToStdout(base64Data)
}