# Raster

Raster algorithms engrave an image row by row. Output uses absolute coordinates (G90) in millimeters (G21), with the origin at the engraving's bottom-left and Y growing upward. width_mm sets the physical width; the height follows the image's aspect ratio. laser_on is "M4" (dynamic power, requires $32=1) by default, or "M3" for constant power.

# Line-to-Line (l2l_gcode + L2LProfile)

Line-to-Line maps each gray level to a laser power between s_min and s_max, in a serpentine scan with run-length-encoded power segments; fully blank rows are skipped. It accepts grayscale images only: a color image raises ValueError, with LaserGRBL's own test, so convert it in an image editor first. Transparency is honored: transparent pixels are blank.

from pygrbl_build import L2LProfile, l2l_gcode, write_gcode

profile = L2LProfile(
    width_mm=120.0,
    lines_per_mm=10.0,       # LaserGRBL's "Quality"; 10 lines/mm is about 254 DPI
    feed=3000,               # mm/min
    s_min=0,                 # power for the lightest non-white gray
    s_max=1000,              # power for pure black, relative to $30
    white_threshold=250,     # gray at or above this is white; 250 is LaserGRBL's WhiteClip=5
    overscan_mm=2.0,         # beam-off travel past row ends; 0 is LaserGRBL-faithful
    bidirectional=True,      # False always scans left to right
    invert=False,            # True engraves the negative
)
write_gcode(l2l_gcode("portrait.png", profile), "portrait.nc")

A profile is frozen and validated at construction: a bad value raises TypeError or ValueError there, never in the G-code.

# Jarvis dithering (jarvis_gcode + JarvisProfile)

from pygrbl_build import JarvisProfile, jarvis_gcode, write_gcode

profile = JarvisProfile(width_mm=80.0, lines_per_mm=3.0, feed=3000, s_max=1000)
write_gcode(jarvis_gcode("photo.png", profile), "photo.nc")

Jarvis converts a color or gray image to a black-and-white dot pattern with Jarvis-Judice-Ninke error diffusion. Dots are engraved at s_max during horizontal raster moves; white pixels use S0, and lines_per_mm sets the dot pitch. The profile exposes LaserGRBL's grayscale formula, channel weights, brightness, contrast and white clip, plus this library's bidirectional scan and optional overscan. The diffusion follows the coefficients and edge behavior of LaserGRBL's Jarvis implementation.

# Image inputs

Every image function (l2l_gcode, jarvis_gcode, img2vector_gcode and img2svg) accepts a file path, encoded image bytes or bytearray, or an already loaded PIL.Image.Image. In-memory services work without writing a temporary image:

from PIL import Image

from pygrbl_build import L2LProfile, l2l_gcode

profile = L2LProfile(width_mm=80.0)

with open("shield.png", "rb") as source:
    from_bytes = l2l_gcode(source.read(), profile)

from_pillow = l2l_gcode(Image.open("shield.png"), profile)

In-memory inputs get the same traceability header as paths: encoded bytes are hashed directly, and Pillow images are hashed from their mode, size and pixel content. The ImageSource type alias describes every accepted input.