All visual properties of objects come from a small set of style classes: TextStyle, FillStyle, StrokeStyle, Gradient, ShadowStyle, CellBorder, BorderStyle, plus the geometric Transform.
Throughout edof, colors are tuples of integers in the range 0–255:
red = (255, 0, 0) # RGB — alpha defaults to 255
red_solid = (255, 0, 0, 255) # RGBA — equivalent
red_50pct = (255, 0, 0, 128) # RGBA with 50% alpha
Functions and styles accept both 3-tuples (RGB) and 4-tuples (RGBA). 3-tuples implicitly use full opacity.
None as a color means “no color” (transparent). For fills, None lets the gradient take over if set.
Defaults for text rendering inside a TextBox or TableCell.
tb.style.font_family = "Helvetica"
tb.style.font_size = 12.0 # in points (pt)
tb.style.bold = False
tb.style.italic = False
tb.style.underline = False
tb.style.strikethrough = False
tb.style.color = (0, 0, 0) # RGB
tb.style.alignment = "left" # "left" | "center" | "right" | "justify"
tb.style.vertical_align = "top" # "top" | "middle" | "bottom"
tb.style.line_height = 1.2 # multiplier of font_size
tb.style.letter_spacing = 0.0 # in pt
tb.style.wrap = True # soft-wrap on whitespace
tb.style.auto_shrink = False # shrink font to fit box
tb.style.auto_fill = False # grow font to fill box
tb.style.background = None # text-frame background; usually use FillStyle.color instead
By default, edof tries fonts in this priority:
font_family you specified (system font lookup via Pillow)Arial → DejaVu Sans on Linux, etc.)For PDF vector export, only the Standard 14 PDF fonts work in vector mode (Helvetica / Times / Courier with bold and italic variants). Other fonts are mapped to the closest match. To use a custom font in PDF, fall back to raster mode: doc.export_pdf(path, vector=False) — this uses Pillow which can render any installed TTF.
To list which fonts edof discovered on the current system:
import edof
from edof.engine.text_engine import discovered_fonts
print(discovered_fonts())
Solid color or gradient fill for shapes and text-box backgrounds.
from edof import FillStyle, Gradient
# Solid color
shape.fill = FillStyle(color=(100, 149, 237, 255))
# Gradient
shape.fill = FillStyle(gradient=Gradient(
type="linear", angle=0,
stops=[(0.0, (255, 0, 0, 255)),
(1.0, (0, 0, 255, 255))]
))
# Or set them on the existing fill
shape.fill.color = None # disable solid color
shape.fill.gradient = my_gradient
color: tuple | None — RGB or RGBA. If gradient is also set, gradient wins.gradient: Gradient | None — see belowLinear or radial gradient with multiple color stops.
from edof import Gradient
g = Gradient(
type="linear", # "linear" | "radial"
angle=45, # for linear: degrees, 0 = left-to-right
radius=0.5, # for radial: radius as fraction of the bounding box
stops=[
(0.0, (255, 0, 0, 255)), # red at start
(0.5, (255, 255, 0, 255)), # yellow in middle
(1.0, ( 0, 0, 255, 255)), # blue at end
],
)
type: str — "linear" or "radial"angle: float — degrees, only used for linear gradients. 0 = left-to-right; 90 = top-to-bottom; 45 = top-left to bottom-rightradius: float — fraction of the bounding box, only for radial. 0.5 = inscribed circle; 1.0 = encompasses cornersstops: list[tuple[float, tuple[int, int, int, int]]] — list of (offset, color) pairs. Offset is 0.0 to 1.0. Stops are sorted at render.The renderer interpolates colors between consecutive stops in linear or radial space. For best results, ensure the first stop is at offset 0.0 and the last at 1.0.
Outline / stroke for shapes.
from edof import StrokeStyle
shape.stroke = StrokeStyle(
color = (0, 0, 0, 255),
width = 0.5, # in mm
dash = "solid", # "solid" | "dashed" | "dotted"
cap = "round", # "butt" | "round" | "square"
join = "round", # "miter" | "round" | "bevel"
)
color: tuple | None — RGB or RGBA. None (or (0, 0, 0, 0)) = no stroke.width: float — line width in mm (gets converted to pixels at render time)dash: str — "solid", "dashed", "dotted"cap: str — line endpoint shapejoin: str — corner shape between segmentsDrop shadow for any object.
from edof import ShadowStyle
obj.shadow = ShadowStyle(
offset_x = 1.0, # mm; positive = right
offset_y = 1.0, # mm; positive = down
blur = 2.0, # mm
color = (0, 0, 0, 80), # 80/255 = ~31% opacity
)
offset_x, offset_y: float — shift in mmblur: float — Gaussian blur radius in mmcolor: tuple — shadow color (typically with reduced alpha)Set obj.shadow = None (the default) for no shadow.
Per-side border on a TableCell. Each cell has four independent borders: border_top, border_right, border_bottom, border_left.
from edof import CellBorder
cell.border_top = CellBorder(
enabled = True,
color = (50, 50, 50, 255),
width = 0.3, # mm
style = "solid", # "solid" | "dashed" | "dotted"
)
enabled: bool — must be True for border to rendercolor: tuple — RGB or RGBAwidth: float — in mmstyle: str — line patternBorder around an entire TextBox or Table. Same fields as CellBorder but applies to the object as a whole rather than per-side.
tb.border = BorderStyle(
enabled = True,
color = (200, 200, 200, 255),
width = 0.2,
style = "solid",
radius = 2.0, # rounded corners in mm
)
Position, size, rotation, and flip — all combined.
Every object has a transform attribute. You usually access individual fields rather than the whole object:
obj.transform.x = 50.0
obj.transform.y = 100.0
obj.transform.width = 80.0
obj.transform.height = 30.0
obj.transform.rotation = 15.0 # degrees, clockwise
obj.transform.flip_h = False # mirror horizontally
obj.transform.flip_v = False # mirror vertically
(0, 0) is the top-left corner of the page.obj.transform.translate(dx, dy) # add to x, y
obj.transform.move_to(x, y) # set x, y absolutely
obj.transform.center() # returns (cx, cy) tuple
obj.transform.center_on(cx, cy) # set position by center point
obj.transform.scale(sx, sy=None) # multiply width/height
obj.transform.rotate_to(angle) # set rotation absolutely
obj.transform.rotate_by(delta) # add to rotation
obj.transform.bbox() # returns (x1, y1, x2, y2) — top-left and bottom-right
For convenience, common operations on the transform are exposed on the object:
obj.move(dx, dy) # → obj.transform.translate(dx, dy)
obj.move_to(x, y) # → obj.transform.move_to(x, y)
obj.resize(w, h) # set transform.width and transform.height
obj.rotate(degrees) # → obj.transform.rotate_by(degrees)
obj.center() # → obj.transform.center()
If you need to convert between mm and pixels (e.g. for direct Pillow operations):
from edof import mm_to_px, from_mm, to_mm
px = mm_to_px(15.0, dpi=300) # 15 mm at 300 DPI = 177.165 px
mm = to_mm(177, dpi=300) # round-trip back
# from_mm() is alias of mm_to_px
Calculations:
1 mm = (dpi / 25.4) px2480 × 3508 pxThese are not styles per object — they’re set on the Page:
from edof import CS_RGB, CS_RGBA, CS_GRAY, CS_BW, CS_CMYK, BD_8, BD_16
page.color_space = CS_RGB # "RGB"
page.bit_depth = BD_8 # 8
# Grayscale page (e.g. for one-color print)
page.color_space = CS_GRAY # "L"
Constants:
CS_RGB = "RGB" — 3-channel color (default)CS_RGBA = "RGBA" — 4-channel with alphaCS_GRAY = "L" — single channel grayscaleCS_BW = "1" — 1-bit black and whiteCS_CMYK = "CMYK" — 4-channel printBD_8 = 8 — 8 bits per channel (default)BD_16 = 16 — 16 bits per channel (high precision)These are passed through to Pillow at export time. Most users stick with the defaults.