PicTex
repository·main·Indexed 42 days ago
https://github.com/francozanardi/pictexA Python library for programmatically generating high-quality images using CSS Flexbox layouts. It utilizes Skia for rendering and Taffy for layout, allowing developers to build complex visuals like social media cards or data tables using web standards. Version 2.3.0 supports a fluent API for styling components such as Row, Column, Image, and Text, and provides both BitmapImage and VectorImage outputs.
What's inside PicTex
- PicTex is a Python library designed for creating styled visual compositions. It provides a component-based and chainable interface that abstracts low-level graphics engines, making it suitable for generating dynamic social media images, video overlays, or digital art tools. Key capabilities include a powerful layout engine for composing elements, a flexible box model for styling, and support for exporting to both raster and vector formats.
How style inheritance works in Canvas
mainThe
Canvasacts as a global style template. Properties set on theCanvasare inherited by all child builders unless they are explicitly overridden on the individual builder.Inherited properties include:
- Typography:
font_family,font_size,font_weight,font_style,color - Text layout:
text_align,line_height - Text effects:
text_shadows,text_stroke,underline,strikethrough
Properties that are NOT inherited (element-specific):
- Box model:
padding,margin,border,border_radius - Background:
background_color,background_image - Effects:
box_shadows - Layout:
size,position,flex_grow, etc.
from pictex import Canvas, Column, Text # Canvas sets global typography canvas = ( Canvas() .font_family("Arial") .font_size(75) .color("blue") ) # These inherit the font settings but can override them layout = Column( Text("Inherits blue"), Text("Overrides to red").color("red"), Text("Inherits blue again") ) canvas.render(layout).save("inheritance_example.png")- Typography:
Understand the Builder concept in PicTex
mainIn PicTex, every visual element is a Builder. You compose images by nesting different types of builders together. There are three main categories:
- Content Builders: The basic building blocks for content.
Text(): For displaying text.Image(): For displaying raster images.
- Layout Builders: Containers that arrange other builders using flexbox principles.
Row(): Arranges children horizontally.Column(): Arranges children vertically.
- Root Builder:
Canvas(): The top-level container that holds the entire composition and defines global styles. It is used to trigger the.render()process.
You can nest these builders (e.g., putting
Textbuilders inside aColumnbuilder, which is itself inside aRowbuilder) to create complex visual layouts.from pictex import * composition = Row( Column( Text("Col1").background_color("blue"), Text("Col1").background_color("red") ), Column( Text("Col2").background_color("blue") Text("Col2").background_color("red") ) ) Canvas().render(composition).save("introduction.png")- Content Builders: The basic building blocks for content.
Inspect the rendered element hierarchy using Render Tree classes
mainTo access or manipulate the hierarchical structure of rendered elements, use the Render Tree classes:
pictex.RenderNode: Represents an individual node within the rendered tree structure.pictex.NodeType: An enumeration used to identify the type of a specificRenderNode.
How Font Fallbacks and Emoji Support work
mainPicTex uses an automatic fallback system to prevent missing characters (rendered as
□). If a character (like an emoji or a specific language script) is not supported by your primary font, PicTex follows this chain:- Primary Font: Set via
.font_family()(defaults to bundled Inter Variable). - Custom Fallbacks: Any fonts provided via
.font_fallbacks(). - System Discovery: Automatic search through system fonts for a compatible grapheme.
Use
.font_fallbacks()to provide a specific list of fonts for multi-lingual support.from pictex import Canvas primary_font = "Lato-BoldItalic.ttf" japanese_font = "NotoSansJP-Regular.ttf" canvas = ( Canvas() .font_family(primary_font) .font_fallbacks(japanese_font) .font_size(80) .color("olive") .padding(20) ) text = "Hello, 世界 ✨" canvas.render(text).save("font_fallback_example.png")- Primary Font: Set via
Understand PicTex output types: BitmapImage and VectorImage
mainPicTex compositions result in one of two primary output classes depending on the rendering method used:
pictex.BitmapImage: Represents a rasterized image output (e.g., PNG, JPEG).pictex.VectorImage: Represents a vector-based image output (e.g., SVG), which allows for infinite scaling without loss of quality.
When working with the PicTex API, the specific class returned will determine how you save or display the final rendered product.
Configure element sizing with .size()
mainThe
.size()method sets explicit dimensions for an element. PicTex uses aborder-boxmodel, meaning the size you define is the total visible dimension, including padding and borders. Padding and borders are applied inward, reducing the available space for content.Sizing Modes
Value Type Example Behavior 'auto'(Default)size()Context-dependent. Usually wraps content, but allows the element to be stretched by parent alignment properties. 'fit-content'size(height='fit-content')Forces the element to wrap its content exactly, preventing parent stretching. 'fit-background-image'size(width='fit-background-image')Forces the element to match the dimensions of its background image. Absolute (pixels) size(width=200, height=150)Sets a fixed pixel size, overriding parent stretching. Percentage size(width="50%")Sets size as a percentage of the parent's content area, overriding parent stretching. Note: To fill available space in a layout, use
.flex_grow(1)instead of sizing modes.# This element's final width in the layout is exactly 300px. box = Row(Text("Content"))\\ .size(width=300)\\ .padding(20)\\ .border(5, "blue") # The content area inside is now 300 - (2*20 padding) - (2*5 border) = 250px wide.Use different positioning modes: absolute, fixed, and relative
mainPicTex supports three CSS-compliant positioning modes to control how elements relate to their containers or the canvas:
absolute_position(): Positions the element relative to its nearest ancestor (similar to CSSposition: absolute).fixed_position(): Positions the element relative to the canvas viewport, regardless of nesting or parent position (similar to CSSposition: fixed).relative_position(): Positions the element relative to its normal flow position (where it would naturally sit in the flex layout).
When using these modes, the element is removed from the normal layout flow.
from pictex import Canvas, Text ( Canvas() .margin(25) .padding(25) .border(25, "red") .background_color("blue") .size(200, 200) .font_size(20) .color("orange") .render( Text("FIXED").fixed_position(top=0, left=0), Text("ABSOLUTE").absolute_position(top=0, left=0) ) ).save("position.png")Handle Text Direction (LTR / RTL) and BiDi
mainPicTex automatically applies the Unicode Bidirectional Algorithm (BiDi) to handle mixed-direction content (e.g., English and Arabic) without manual configuration.
Key behaviors:
- Automatic Alignment: In an RTL context,
text_align()defaults toTextAlign.RIGHT. - Layout Mirroring:
Rowcontainers automatically reverse the visual order of their children when.direction("rtl")is applied. - Direction Inheritance: The direction property is inherited by all children.
- Manual Override: Use
.direction("rtl")or.direction("ltr")to set an explicit base direction.
from pictex import Canvas, Row, Column, Text # RTL base direction - affects alignment and layout container = ( Column( # This Row will be mirrored: [B] [A] Row( Text("A").background_color("red").padding(5), Text("B").background_color("orange").padding(5), ).gap(10), # This text will be right-aligned automatically Text("Right-aligned text").size(width=400), # Explicit override to LTR Text("LTR Override").direction("ltr"), ) .direction("rtl") .padding(20) .gap(20) .background_color("lightgray") ) Canvas().render(container).save("direction_example.png")- Automatic Alignment: In an RTL context,
Configure font handling for SVG exports
mainWhen exporting to SVG, font portability is managed via the
embed_fontparameter in.render_as_svg(). The behavior depends on whether you are using a font file path or a system font name.Using a Font File (e.g.,
.font_family("path/to/font.ttf"))embed_font=True(Default): The font is Base64 encoded and embedded directly in the SVG. This creates a fully self-contained and portable file, but increases the file size by approximately 133% of the original font size.embed_font=False: The SVG references the font, and PicTex automatically copies the font files to a subdirectory. This creates a portable package where the SVG is small, but must be distributed alongside itsfonts/directory.
Customizing Font Copying:
- Use
fonts_subdir="name"to change the destination directory name. - Use
copy_fonts=Falseto disable automatic copying entirely.
Using a System Font (e.g.,
.font_family("Arial"))- The
embed_fontparameter has no effect because PicTex cannot access the file path of a system font. - The SVG will reference the font by name (e.g.,
font-family: 'Arial'). - Rendering depends on the viewer having the font installed. If the font is missing, the viewer will use a fallback, which may change the appearance.
- A warning is issued if you attempt to use
embed_font=Truewith a system font.
# This creates a completely portable SVG (Base64 embedded) vector_image = canvas.render_as_svg("Portable & Perfect", embed_font=True) vector_image.save("portable_text.svg") # This creates a lightweight SVG with automatic font copying to a custom directory vector_image = canvas.render_as_svg("Linked Font", embed_font=False) vector_image.save("output/linked_text.svg", fonts_subdir="my-fonts") # This disables automatic font copying (SVG will only work if fonts exist in the same directory) vector_image.save("output/linked_text.svg", copy_fonts=False)How builders and Canvas work together
mainPicTex uses a compositional model to create visuals. The workflow consists of three main parts:
- Builders: The fundamental building blocks.
- Content Builders: Such as
TextandImage. - Layout Builders: Such as
RowandColumnused to arrange content builders.
- Content Builders: Such as
- Canvas: The top-level container. It is used to set global styles (like default fonts or background colors) and serves as the entry point for rendering.
- Rendering: You create a tree structure by nesting builders inside one another, then pass that tree to a
Canvasinstance to produce a final image.
This tree structure allows you to build complex components (like a user banner) by nesting simple elements (like an avatar and text) inside layout containers.
from pictex import Canvas, Row, Column, Text, Image # Compose a tree of builders avatar = Image("avatar.jpg").size(60, 60).border_radius('50%') user_info = Column(Text("Alex Doe"), Text("@alexdoe")) user_banner = Row(avatar, user_info) # Render via Canvas canvas = Canvas() image = canvas.render(user_banner)- Builders: The fundamental building blocks.
Quickstart: Create a composed image
mainTo create a composed image, define your content and layout builders, nest them, and then use a
Canvasto render the result. You can then save the resultingBitmapImageto a file.from pictex import Canvas, Row, Column, Text, Image # 1. Create the individual content builders avatar = ( Image("avatar.jpg") .size(60, 60) .border_radius('50%') # Make it circular ) user_info = Column( Text("Alex Doe").font_size(20).font_weight(700), Text("@alexdoe").color("#657786") ).gap(4) # Add a 4px vertical gap between the texts # 2. Compose the builders in a layout container user_banner = Row( avatar, user_info ).gap(15).align_items('center') # Vertically center the avatar and user info # 3. Create a Canvas and render the final composition canvas = Canvas().padding(20).background_color("#F5F8FA") image = canvas.render(user_banner) # 4. Save the result image.save("user_banner.png")