@@ -39,14 +39,14 @@ PostScript Source ──► Tokenizer ──► Execution ──► │ Display
3939 like ` fill ` , ` stroke ` , and ` show ` append elements here.
4040
41415 . ** Output Device** — When ` showpage ` fires, the accumulated display list is
42- handed to a device for rendering. Devices live in ` postforge/devices/ ` and
43- the current implementation typically delegates to a shared Cairo rendering
44- backend, but this is not a requirement — an output device can use whatever
45- rendering method it wants to process the display list. After rendering,
46- ` showpage ` erases the display list and reinitializes the graphics state for
47- the next page. ` copypage ` follows the same rendering path but preserves
48- both the display list and graphics state, allowing further drawing on top
49- of the existing page contents.
42+ handed to a device for rendering. Devices live in ` postforge/devices/ ` —
43+ raster devices (PNG, TIFF, Qt) and SVG use a shared Cairo rendering
44+ backend, while the PDF device generates content streams directly from the
45+ display list. An output device can use whatever rendering method it wants.
46+ After rendering, ` showpage ` erases the display list and reinitializes the
47+ graphics state for the next page. ` copypage ` follows the same rendering
48+ path but preserves both the display list and graphics state, allowing
49+ further drawing on top of the existing page contents.
5050
5151
5252## The Execution Engine
@@ -357,7 +357,7 @@ The display list is a flat Python list containing instances of these classes
357357| ` Stroke ` | ` stroke ` | Stroked path with line properties and CTM |
358358| ` PatternFill ` | ` fill ` with pattern color space | Pattern-tiled fill |
359359| ` ImageElement ` | ` image ` , ` imagemask ` , ` colorimage ` | Raster image data |
360- | ` TextObj ` | ` show ` (in TextObjs mode) | Text for native PDF output |
360+ | ` TextObj ` | ` show ` (in TextObjs mode) | Structured text for PDF output |
361361| ` ClipElement ` | ` clip ` , ` eoclip ` , ` initclip ` | Clipping path update |
362362| ` GlyphRef ` | show (cache hit) | Reference to cached glyph bitmap |
363363| ` GlyphStart ` /` GlyphEnd ` | show (cache miss) | Glyph bitmap capture markers |
@@ -421,19 +421,23 @@ conversion formulas (NTSC weighting for gray, etc.).
421421### Color Conversion at Rendering Time
422422
423423Color conversion is * lazy* — ` setcolor ` stores the color in the graphics
424- state, but the conversion to device color (RGB for the Cairo renderer) happens
425- only when a painting operator builds a display list element:
424+ state, but the conversion to device color happens only when a painting operator
425+ builds a display list element:
426426
4274271 . A painting operator (` fill ` , ` stroke ` , etc.) calls
428428 ` ColorSpaceEngine.convert_to_device_color() ` .
4294292 . The engine dispatches based on color space family — device spaces pass
430430 through (with cross-conversion if needed), CIE-based spaces run through
431431 their decode/matrix/XYZ pipeline, and ICCBased spaces apply an lcms2
432432 transform.
433- 3 . The resulting RGB values are stored in the display list element (` Fill ` ,
434- ` Stroke ` , etc.).
435- 4 . The rendering device receives pre-converted RGB and passes it straight
436- to Cairo.
433+ 3 . The resulting color values are stored in the display list element (` Fill ` ,
434+ ` Stroke ` , etc.). When a ` /ColorModel ` is set in the page device (e.g.,
435+ ` /DeviceRGB ` for Cairo-based raster devices), colors are converted to that
436+ model. When no ` /ColorModel ` is set (the PDF device), original device color
437+ spaces (CMYK, Gray, RGB) are preserved.
438+ 4 . The rendering device consumes these colors — Cairo-based devices receive
439+ RGB, while the PDF device emits the appropriate PDF color operators for
440+ whatever color space was used.
437441
438442### ICC Color Management Tiers
439443
@@ -472,10 +476,10 @@ Output devices render the display list into a final format. Each device
472476consists of two parts that work together: a PostScript configuration file
473477in ` postforge/resources/OutputDevice/ ` (e.g., ` png.ps ` ) that defines the page device
474478dictionary, and a Python module in ` postforge/devices/ ` that implements a
475- ` showpage(ctxt, pd) ` function to perform the actual rendering. The built-in
476- devices use a shared Cairo rendering backend, but this is a convenience, not
477- a requirement — a custom device can use any rendering approach it wants
478- without involving Cairo at all .
479+ ` showpage(ctxt, pd) ` function to perform the actual rendering. The raster
480+ devices (PNG, TIFF, Qt) use a shared Cairo rendering backend, while the PDF
481+ device generates PDF content streams directly from the display list. A custom
482+ device can use any rendering approach it wants .
479483
480484### Device Architecture
481485
@@ -486,32 +490,43 @@ without involving Cairo at all.
486490 │ │
487491 │ cairo_renderer.py - dispatch cairo_patterns.py - patterns │
488492 │ cairo_images.py - images cairo_shading.py - shading │
489- └─────┬────────────┬────────────┬─────────────┬────────────┬─────┘
490- │ │ │ │ │
491- ▼ ▼ ▼ ▼ ▼
492- ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
493- │ PNG │ │ PDF │ │ SVG │ │ TIFF │ │ Qt │
494- │ device │ │ device │ │ device │ │ device │ │ device │
495- └──────────┘ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘
496- │
497- ┌────────┴────────┐
498- │ Font embedding │
499- │ (font_embedder, │
500- │ cid_font_ │
501- │ embedder, │
502- │ pdf_injector) │
503- └─────────────────┘
493+ └─────────┬──────────────┬──────────────┬──────────────┬─────────┘
494+ │ │ │ │
495+ ▼ ▼ ▼ ▼
496+ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
497+ │ PNG │ │ SVG │ │ TIFF │ │ Qt │
498+ │ device │ │ device │ │ device │ │ device │
499+ └──────────┘ └──────────┘ └──────────┘ └──────────┘
500+
501+ ┌─────────────────────────────────────────┐
502+ │ PDF device │
503+ │ (devices/pdf/) │
504+ │ │
505+ │ pdf.py ──► content_stream.py │
506+ │ ├─ stroke_ops.py │
507+ │ ├─ text_ops.py │
508+ │ ├─ type3_ops.py │
509+ │ ├─ image_ops.py │
510+ │ └─ shading_ops.py │
511+ │ ──► pdf_builder.py │
512+ │ ├─ font_embedder.py │
513+ │ ├─ cid_font_embedder.py │
514+ │ ├─ cff_font_embedder.py │
515+ │ └─ font_tracker.py │
516+ └─────────────────────────────────────────┘
504517```
505518
506519** PNG** (` postforge/devices/png/png.py ` ) — Creates a Cairo ImageSurface, calls
507520` render_display_list() ` , writes a ` .png ` file. The simplest device and a good
508521starting point for understanding the rendering pipeline.
509522
510- ** PDF** (` postforge/devices/pdf/ ` ) — Renders to a Cairo PDFSurface, then
511- post-processes the PDF with pypdf to inject embedded fonts. Text in PDF mode
512- uses ` TextObj ` elements that are written as native PDF text operators, producing
523+ ** PDF** (` postforge/devices/pdf/ ` ) — Generates PDF content streams directly
524+ from the display list (does not use Cairo). Preserves original color spaces
525+ (CMYK, Gray, RGB) instead of converting everything to RGB. Text uses ` TextObj `
526+ elements written as PDF text operators with TJ arrays and kern values, producing
513527searchable/selectable text. Font embedding handles Type 1 reconstruction,
514- CID/TrueType extraction, and subsetting.
528+ CID/TrueType extraction, CFF, Type 42, Type 3, and subsetting. The final PDF
529+ is assembled at document end via pypdf.
515530
516531** SVG** (` postforge/devices/svg/svg.py ` ) — Renders to a Cairo SVGSurface,
517532then post-processes the SVG to convert text from outlines to selectable ` <text> `
@@ -541,8 +556,10 @@ dictionary is loaded and merged into the graphics state's `page_device`.
541556### Shared Cairo Renderer
542557
543558` render_display_list() ` in ` postforge/devices/common/cairo_renderer.py ` is the
544- main dispatch loop. It iterates over display list elements and delegates to
545- type-specific rendering functions:
559+ main dispatch loop used by the raster and vector-surface devices (PNG, SVG,
560+ TIFF, Qt). The PDF device does not use this renderer — it generates PDF content
561+ streams directly. The Cairo renderer iterates over display list elements and
562+ delegates to type-specific rendering functions:
546563
547564- Path construction → Cairo ` move_to ` , ` line_to ` , ` curve_to ` , ` close_path `
548565- Fill/Stroke → Cairo ` fill ` / ` stroke ` with color and line properties
@@ -554,10 +571,11 @@ type-specific rendering functions:
554571
555572** Stroke method** : For bitmap devices (PNG, Qt), strokes are converted to filled
556573paths by the interpreter before they reach the display list. This works around
557- bugs in Cairo's stroke rasterization, particularly with dashed lines. The PDF
558- device uses Cairo's native stroke rendering instead. This behavior is controlled
559- per-device by the ` /StrokeMethod ` entry in the page device dictionary (set in
560- each device's ` .ps ` configuration file).
574+ bugs in Cairo's stroke rasterization, particularly with dashed lines. Vector
575+ devices (PDF, SVG) use native stroke rendering instead — PDF emits stroke
576+ operators directly, while SVG uses Cairo's vector surface. This behavior is
577+ controlled per-device by the ` /StrokeMethod ` entry in the page device dictionary
578+ (set in each device's ` .ps ` configuration file).
561579
562580
563581## Resource System
0 commit comments