ScreenshotNeo

BlogHow-to

How to Send Screenshots in 1 KB Chunks With Python

Capture a screenshot, encode it, split the bytes into explicit 1 KB pieces, and upload them safely with Python.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: capture the screen as an image, serialize it into bytes, slice those bytes at a fixed boundary, and send each piece with metadata that your receiving API defines. This article uses 1,024 bytes per chunk (a binary kibibyte). If your protocol means decimal kilobytes, set the size to 1,000 instead.

Application-level slicing is different from HTTP chunked transfer encoding. Slicing lets your protocol receive exact 1,024-byte pieces. HTTP chunked encoding only frames a request body for transport; it does not promise that the server sees 1,024-byte application pieces.

1. Install the capture library

python -m pip install Pillow requests

Pillow’s ImageGrab.grab() returns a screenshot in memory. With no bounding box it captures the whole screen. The returned mode and display behavior vary by platform; macOS Retina displays and Linux X11 setups can require special handling. See the Pillow ImageGrab documentation.

2. Capture and encode the screenshot

from io import BytesIO
from PIL import ImageGrab

image = ImageGrab.grab()                 # full screen
buffer = BytesIO()
image.save(buffer, format="PNG")         # choose PNG, JPEG, or another format
payload = buffer.getvalue()               # immutable bytes

print(f"encoded bytes: {len(payload)}")

BytesIO avoids a temporary file: getvalue() returns the complete binary buffer. PNG is lossless and may be larger; JPEG is usually smaller for photographic screens but loses detail. The Python io documentation describes the in-memory stream behavior.

3. Split the image into exact 1 KB pieces

CHUNK_SIZE = 1024  # 1 KiB; use 1000 for a decimal KB

chunks = [
    payload[start:start + CHUNK_SIZE]
    for start in range(0, len(payload), CHUNK_SIZE)
]

for index, chunk in enumerate(chunks):
    print(index, len(chunk))

# Every chunk except the last is exactly 1,024 bytes.
# The last chunk can be shorter.

The receiver needs an upload identifier, a zero-based or one-based part index, the total part count (or an explicit final flag), the original filename or format, and a way to verify integrity. Those fields belong to your receiver’s contract. A Python iterable by itself is not an upload protocol: the server must know how to order, validate, and reassemble the parts.

4. Upload each part to an application-level endpoint

The following client is complete for an API that accepts one part per request. Replace https://upload.example.test/v1/parts and the field names with the contract implemented by your server. It sends raw bytes and supplies metadata in headers.

import hashlib
import uuid
from io import BytesIO
from pathlib import Path

import requests
from PIL import ImageGrab

UPLOAD_URL = "https://upload.example.test/v1/parts"  # replace with your API
CHUNK_SIZE = 1024
TIMEOUT = 30


def capture_png() -> bytes:
    image = ImageGrab.grab()
    output = BytesIO()
    image.save(output, format="PNG")
    return output.getvalue()


def send_screenshot(data: bytes) -> str:
    upload_id = str(uuid.uuid4())
    total_parts = (len(data) + CHUNK_SIZE - 1) // CHUNK_SIZE
    whole_sha256 = hashlib.sha256(data).hexdigest()

    for part_index, start in enumerate(range(0, len(data), CHUNK_SIZE)):
        part = data[start:start + CHUNK_SIZE]
        headers = {
            "Content-Type": "application/octet-stream",
            "X-Upload-Id": upload_id,
            "X-Part-Index": str(part_index),
            "X-Part-Count": str(total_parts),
            "X-Part-SHA256": hashlib.sha256(part).hexdigest(),
            "X-Image-SHA256": whole_sha256,
            "X-Is-Final": "true" if part_index == total_parts - 1 else "false",
        }
        response = requests.put(UPLOAD_URL, headers=headers, data=part, timeout=TIMEOUT)
        response.raise_for_status()

    return upload_id


if __name__ == "__main__":
    screenshot = capture_png()
    upload_id = send_screenshot(screenshot)
    print(f"uploaded {len(screenshot)} bytes as upload {upload_id}")

This code intentionally does not claim a universal upload URL or authentication scheme. Add the authorization header, idempotency key, and completion request required by your server. If your API has a separate /complete operation, call it after all parts succeed.

5. Reassemble and validate on the server

  1. Create storage for upload_id and record the expected part count and whole-file hash.
  2. Reject a part whose index is duplicated with different bytes, whose size exceeds the configured maximum, or whose per-part hash fails.
  3. Store parts by index, because requests may arrive out of order.
  4. When all indexes are present (or the final flag is received), concatenate them in order.
  5. Verify the complete SHA-256 hash, then decode the image and mark the upload complete.

Do not infer completion merely from a short part: only the final piece is allowed to be shorter than 1,024 bytes.

6. Sending an iterable with HTTP chunked transfer

Python’s HTTP clients can stream an iterable of byte strings. That activates transport-level chunked transfer when no Content-Length is supplied; it does not define application part indexes, retries, or resumability. The http.client documentation states that iterable elements are sent as they are yielded. urllib.request similarly handles iterables and request framing.

import http.client
from urllib.parse import urlsplit

parts = (payload[i:i + 1024] for i in range(0, len(payload), 1024))
url = urlsplit("https://upload.example.test/v1/stream")
connection = http.client.HTTPSConnection(url.hostname, url.port or 443, timeout=30)
connection.request(
    "POST",
    url.path,
    body=parts,
    headers={"Content-Type": "application/octet-stream"},
)
response = connection.getresponse()
print(response.status, response.read())
connection.close()

Use this only when the receiving endpoint explicitly accepts a streamed body and has another way to identify the upload and its final boundary. If exact application-visible pieces matter, send each part as its own request with metadata instead.

7. Choosing 1,000 versus 1,024 bytes

Meaning Constant When to use it
Decimal kilobyte 1000 An API specification that says KB and defines SI units
Binary kibibyte 1024 Many programming and storage examples; state it explicitly

There is no universal convention in the title’s source material. Put the chosen number in your protocol documentation and tests.

8. Reliability, retries, and performance

  • Retries: retry only failed parts, using upload_id plus part index as an idempotency key. The server should treat an identical retry as a no-op.
  • Ordering: allow out-of-order arrival; sort by index during assembly.
  • Integrity: hash every part and the complete image. A successful HTTP status alone does not prove the bytes were assembled correctly.
  • Timeouts: set connect and read timeouts. Do not retry indefinitely; surface the failed part and upload ID for recovery.
  • Memory: the BytesIO example holds the encoded image in memory. For very large captures, write the encoded file to disk and read fixed ranges, or use a server protocol designed for streaming.
  • Chunk size: smaller parts improve retry granularity but increase request overhead. Keep 1,024 bytes when the protocol requires it; otherwise select a documented size based on your receiver’s limits.

9. Platform and capture edge cases

  • On macOS, Retina scaling can make pixel dimensions differ from the logical display size.
  • On Linux, ImageGrab may need an X11 display and fallback utilities when the default display cannot provide a snapshot.
  • Headless servers often have no desktop display. Use a virtual display or a browser/API capture service instead of assuming a physical screen exists.
  • Permission prompts, locked screens, multiple monitors, and a supplied bounding box can change what is captured.
  • PNG encoding can produce a final part of any length, including an empty upload only if the source image itself was not produced; reject empty captures explicitly.

10. Troubleshooting

Symptom Likely cause Fix
OSError from ImageGrab.grab() No display, missing Linux utility, or insufficient permission Run in a desktop session, install the platform utility, or use a browser/API capture path.
Parts are not exactly 1,024 bytes You used 1,000, changed the slice step, or inspected the final part Set CHUNK_SIZE = 1024; expect only the last part to be shorter.
Server cannot rebuild the image Missing indexes, wrong ordering, or a truncated request Persist upload ID and indexes, verify per-part hashes, and concatenate in numeric order.
Duplicate-part error on retry The server is not idempotent Send an idempotency key and make identical retries return the original success.
HTTP 411 or framing error Endpoint requires Content-Length and rejects chunked transfer Send each known-size part separately or provide the required length.
Upload succeeds but image is corrupt Wrong format, altered bytes, or failed final hash Transmit binary bytes unchanged and validate the complete SHA-256 before decoding.

11. Or skip the browser setup

If your real goal is obtaining a clean website screenshot rather than capturing a physical desktop, ScreenshotNeo returns an image or PDF from one GET request. Its capture options include full-page shots, element selectors, device presets, custom viewports, waits, custom CSS and JavaScript, headers, cookies, geolocation, blocking rules, resizing, caching, signed links, asynchronous jobs, bulk capture, and PDF settings.

See the ScreenshotNeo API docs for the request parameters. The response is already an image; if another system requires 1,024-byte application parts, apply the same BytesIO and slicing steps to the response body before uploading.

# cURL
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)
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}`);

Cookie banners, newsletter popups, and chat widgets are removed 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. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account and start with 1,000 screenshots per month at no charge.

12. FAQ

Is a 1 KB chunk always 1,024 bytes?

No. Choose 1,000 or 1,024 and document the choice. This guide uses 1,024 bytes.

Can I send the chunks in parallel?

Only if the receiver supports concurrent parts and idempotent indexes. Limit concurrency to avoid rate limits and memory pressure.

Can HTTP chunked encoding resume an interrupted upload?

No. It frames one request. Resumability requires an application protocol that stores upload IDs and accepts individual parts.

Do I need to split a screenshot at all?

Only when the receiving API imposes a body or part limit, or when you need retryable parts. Otherwise one binary upload is simpler.