pub struct Context {Show 80 fields
pub o_stack: Stack,
pub e_stack: Stack,
pub d_stack: Vec<EntityId>,
pub strings: DualStringStore,
pub arrays: DualArrayStore,
pub dicts: DualDictStore,
pub names: NameTable,
pub files: FileStore,
pub loops: Vec<LoopState>,
pub operators: Vec<OpEntry>,
pub systemdict: EntityId,
pub globaldict: EntityId,
pub userdict: EntityId,
pub errordict: EntityId,
pub dollar_error: EntityId,
pub rand_state: u64,
pub rand_seed: i64,
pub current_source_line: u32,
pub packing_mode: bool,
pub echo: bool,
pub name_cache: NameCache,
pub stdout: Box<dyn Write>,
pub save_stack: SaveStack,
pub job_start_save_depth: usize,
pub vm_alloc_mode: bool,
pub object_format: i32,
pub current_operator: Option<NameId>,
pub null_device_used: bool,
pub in_error_handler: bool,
pub initializing: bool,
pub allow_ps_resolution: bool,
pub exit_code: Option<i32>,
pub gstate: GraphicsState,
pub gstate_stack: Vec<GstateEntry>,
pub gstate_store: Vec<GraphicsState>,
pub device: Option<Box<dyn OutputDevice>>,
pub display_list: DisplayList,
pub group_stack: Vec<GroupFrame>,
pub save_group_depths: FxHashMap<u32, usize>,
pub ocg_registry: FxHashMap<NameId, OcgRecord>,
pub next_ocg_id: u32,
pub doc_structure: DocumentStructure,
pub capture_display_lists: Option<Vec<(DisplayList, f64)>>,
pub display_list_sender: Option<Sender<(DisplayList, f64, u32, u32, Option<Arc<Vec<u8>>>, bool)>>,
pub page_width: u32,
pub page_height: u32,
pub output_path: Option<String>,
pub output_template: Option<OutputTemplate>,
pub pages_emitted: u32,
pub page_filter: Option<HashSet<i32>>,
pub device_factory: Option<Box<dyn Fn(u32, u32) -> Box<dyn OutputDevice>>>,
pub font_directory: EntityId,
pub font_resource_path: Option<String>,
pub next_fid: i32,
pub global_resources: EntityId,
pub local_resources: EntityId,
pub category_registry: EntityId,
pub resource_base_path: Option<String>,
pub user_params: EntityId,
pub system_params: EntityId,
pub internaldict: EntityId,
pub icc_cache: IccCache,
pub exec_sync_fn: Option<ExecSyncFn>,
pub char_width: Option<(f64, f64)>,
pub char_width_mode1: Option<((f64, f64), (f64, f64))>,
pub glyph_caches: FxHashMap<EntityId, GlyphCache>,
pub char_cache_mode: Option<Type3CacheMode>,
pub cshow_pending_cid: Option<i32>,
pub charpath_capture: Option<PsPath>,
pub pattern_store: Vec<PatternData>,
pub form_cache: FxHashMap<EntityId, DisplayList>,
pub cie_decode_cache: FxHashMap<CieDecodeKey, Vec<f64>>,
pub start_time: Option<Instant>,
pub dict_version: u64,
pub name_resolve_cache: Vec<(u64, PsObject)>,
pub interrupt_flag: Option<Arc<AtomicBool>>,
pub exec_sync_depth: u32,
pub max_local_vm: usize,
pub deadline: Option<Instant>,
pub yield_after_showpage: bool,
/* private fields */
}Fields§
§o_stack: Stack§e_stack: Stack§d_stack: Vec<EntityId>§strings: DualStringStore§arrays: DualArrayStore§dicts: DualDictStore§names: NameTable§files: FileStore§loops: Vec<LoopState>§operators: Vec<OpEntry>§systemdict: EntityId§globaldict: EntityId§userdict: EntityId§errordict: EntityId§dollar_error: EntityId§rand_state: u64§rand_seed: i64Last value handed to srand, returned by rrand. Widened with
PsValue::Int: rrand must return exactly what srand was given.
current_source_line: u32Current source line number (1-based), updated during scanning.
packing_mode: boolPacking mode for array/procedure creation (setpacking/currentpacking).
echo: boolEcho mode for %lineedit/%statementedit (PLRM echo operator).
name_cache: NameCache§stdout: Box<dyn Write>§save_stack: SaveStack§job_start_save_depth: usizeSave stack depth when the current job started (for startjob condition 3).
vm_alloc_mode: bool§object_format: i32Binary object format (0-4). Default 0.
current_operator: Option<NameId>§null_device_used: boolWhether nulldevice was installed at any point during this job.
Sticky for the job, and deliberately not cleared by a grestore that
puts a real device back: it records intent, not current state. A
program that asked for the null device said it wants no output, so
marks it leaves unemitted at end of job are expected rather than a
dropped page worth reporting. The PS test suite is the motivating
case — its files paint into gsave nulldevice ... grestore to
exercise operators, never call showpage, and must not be nagged
about it.
in_error_handler: bool§initializing: boolTrue during init script execution — relaxes access checks.
allow_ps_resolution: boolWhen true, PS programs can change HWResolution via setpagedevice. Set by WASM frontend; CLI leaves false to keep DPI under user control.
exit_code: Option<i32>Process exit code requested by the running PS program via the
.quitwithcode operator. None means “use the default” (0 on
success). The CLI reads this on PsError::Quit and propagates
to std::process::exit.
gstate: GraphicsState§gstate_stack: Vec<GstateEntry>§gstate_store: Vec<GraphicsState>Storage for gstate objects (PsValue::Gstate indexes into this).
device: Option<Box<dyn OutputDevice>>§display_list: DisplayList§group_stack: Vec<GroupFrame>Stack of active transparency-group capture frames. While non-empty,
paint operators emit into the topmost frame’s display list instead
of display_list. endtransparencygroup pops the top frame and
emits a stet_graphics::display_list::DisplayElement::Group into
the next-innermost target. See op_begintransparencygroup /
op_endtransparencygroup in stet-ops::transparency_ops.
save_group_depths: FxHashMap<u32, usize>group_stack.len() recorded at each save. restore consults
this to refuse a revert that would unwind across an unbalanced
begintransparencygroup / endtransparencygroup pair.
ocg_registry: FxHashMap<NameId, OcgRecord>Registry of OCGs (PDF Optional Content Groups) declared via the
defineocg operator. Keyed by the interned NameId of the
human-readable layer name from the OCG dict’s /Name entry.
beginoptionalcontent looks up an OCG by name to obtain the
ocg_id and default_visible it embeds into the emitted
stet_graphics::display_list::OcgVisibility::Single.
next_ocg_id: u32Monotonic counter feeding OcgRecord::ocg_id. Each call to
defineocg increments this; ids never recycle.
doc_structure: DocumentStructureDocument-level structural data — outline, annotations, metadata,
page boxes, etc. — parallel IR to display_list.
PostScript pdfmark operators populate this; the PDF output device
drains it at end-of-job; non-PDF devices ignore it. Document-global:
save / restore do not roll this back. See
stet_graphics::document_structure.
capture_display_lists: Option<Vec<(DisplayList, f64)>>When Some, each showpage clones the display list here before consuming it.
Used by the WASM frontend to retain display lists for viewport re-rendering.
Each entry is (DisplayList, dpi) where dpi is from the pagedevice HWResolution.
display_list_sender: Option<Sender<(DisplayList, f64, u32, u32, Option<Arc<Vec<u8>>>, bool)>>When Some, each showpage sends a clone of the display list through this channel.
Used by the CLI viewer for incremental display list delivery.
Tuple: (DisplayList, dpi, page_width, page_height, effective_cmyk_bytes, cmyk_proofing). PostScript pages always pass
None/false (no PDF/X concept); PDF pages may set these from the
document’s OutputIntent context.
page_width: u32§page_height: u32§output_path: Option<String>§output_template: Option<OutputTemplate>Explicit -o / --output template, when the user supplied one.
Takes precedence over the name derived from the input file in
Self::output_path. Held parsed so that a malformed template is
rejected at the command line rather than at the first showpage.
pages_emitted: u32Count of pages actually written so far, as opposed to the logical page
number, which advances even for pages excluded by Self::page_filter.
A no-token --output template is only valid while this is 1.
page_filter: Option<HashSet<i32>>Page filter: if set, only render pages in this set (1-based).
device_factory: Option<Box<dyn Fn(u32, u32) -> Box<dyn OutputDevice>>>Factory closure for creating raster devices (registered by CLI).
font_directory: EntityId§font_resource_path: Option<String>§next_fid: i32§global_resources: EntityId§local_resources: EntityId§category_registry: EntityId§resource_base_path: Option<String>§user_params: EntityId§system_params: EntityId§internaldict: EntityIdBacking dict for the internaldict operator.
Created during bootstrap rather than on first use. Context holds the
EntityId for the whole life of the interpreter, so the dict has to
outlive every restore; creating it lazily inside a save bracket would
leave this handle pointing at storage that restore reclaims. Entries
written into it after a save are still reverted normally, by the
dict’s own copy-on-write.
icc_cache: IccCache§exec_sync_fn: Option<ExecSyncFn>§char_width: Option<(f64, f64)>§char_width_mode1: Option<((f64, f64), (f64, f64))>§glyph_caches: FxHashMap<EntityId, GlyphCache>§char_cache_mode: Option<Type3CacheMode>§cshow_pending_cid: Option<i32>§charpath_capture: Option<PsPath>Set while a Type 3 glyph procedure runs under charpath.
PLRM: charpath “obtains the path for the glyph outlines that would
result if string were shown”; for a Type 3 font that means running the
glyph procedure without painting. While this is Some, fill,
eofill and stroke contribute the path they were given here instead
of marking the page — in particular stroke contributes the path as
constructed, since charpath’s own boolean operand, not the glyph
procedure, decides whether the result gets stroked.
pattern_store: Vec<PatternData>Storage for pattern instances created by makepattern.
form_cache: FxHashMap<EntityId, DisplayList>Cache of form display lists keyed by dict EntityId.
cie_decode_cache: FxHashMap<CieDecodeKey, Vec<f64>>Memo of sampled CIE decode tables, keyed by a structural fingerprint of the decode procedure together with the sampled range.
Sampling one table runs the procedure 256 times, and a CIE colour
space installs up to six of them, so a file that re-installs the same
space per page (what pdftops emits for every ICCBased space) pays
thousands of exec_sync calls per page. Worse, decode procedures
routinely contain inline array literals, and every evaluation
allocates a fresh array in the non-reclaiming array arena — turning
the repeated sampling into unbounded memory growth.
Memoising is sound because the PLRM already requires a CIE decode
procedure to be a pure function of its single input: sampling it at
256 points and interpolating (which this code has always done) is the
same assumption. See stet_ops::color_ops::eval_decode_table_range.
start_time: Option<Instant>§dict_version: u64§name_resolve_cache: Vec<(u64, PsObject)>Name resolution cache indexed by NameId. Each entry is (dict_version, resolved_object). Public for inline cache checks in the eval loop’s hot path.
interrupt_flag: Option<Arc<AtomicBool>>When set, the eval loop aborts with PsError::Quit on the next iteration.
Used by the interactive viewer to cancel an in-flight parse when the
user drops a new file.
exec_sync_depth: u32Current re-entrancy depth of exec_sync.
exec_sync runs a nested eval loop in a fresh native stack frame, and
roughly 47 call sites reach it — tint transforms, Type 3 BuildChar,
image data procedures, and most operators that need a procedure’s
result before they can continue. Several of those are re-entrant from
PostScript: a /Separation colour space whose tint transform sets that
same colour space recurses until the native stack is gone, which is an
abort rather than a catchable error, from about 200 bytes of input.
Tracked on the context rather than threaded as a parameter because the call sites are spread across four crates and the value is genuinely interpreter state, not an argument.
max_local_vm: usizeCeiling on local VM, in bytes.
PLRM 3.7.1 gives this as the device-dependent MaxLocalVM user
parameter; stet stores it here and exposes it through
setuserparams and the CLI’s --max-vm.
The default is generous rather than absent. A large allocation that
fails is not a catchable error — Rust’s allocator aborts the process —
so 500000000 array asking for 16 GB took stet down rather than
raising VMerror. Refusing before allocating turns that into an
ordinary PostScript error. 8 GiB is far past any real job: this bounds
PostScript VM — strings, arrays, dictionaries — which is a separate
pool from the band and image buffers the renderer works in, and those
are where a large prepress job actually spends memory.
deadline: Option<Instant>Wall-clock deadline for interpretation, if one was set.
PostScript is Turing-complete, so no static analysis bounds how long a
program runs — {} loop is three characters. Anything that feeds it
untrusted input needs a deadline, and a deadline is also the only thing
that bounds a program which makes progress but never terminates, where
the recursion and allocation guards elsewhere do not apply: a
/Separation tint transform that re-enters its own colour space hits
the exec_sync depth cap, unwinds, and retries forever.
None — the default — means no limit, preserving the behaviour the
REPL and CLI have always had. Set it with Context::set_timeout.
yield_after_showpage: boolWhen true, each successful showpage / copypage sets interrupt_flag
after capturing the display list, so the eval loop yields back to the
caller one page at a time. The caller clears the flag and re-enters
eval to drive the next page. Used by the WASM viewer to stream
multi-page PostScript documents: page 1 renders while pages 2..N are
still pending interpretation. Requires interrupt_flag to be set.
Implementations§
Source§impl Context
impl Context
Sourcepub fn set_timeout(&mut self, limit: Option<Duration>)
pub fn set_timeout(&mut self, limit: Option<Duration>)
Stop interpreting once limit has elapsed from now.
Raises PsError::Timeout from the eval loop when the deadline
passes. Pass None to interpret without a limit, which is the default.
The deadline is absolute, set at the moment of this call: it bounds the whole job, not each operator, so re-arm it per job rather than once for a long-lived context.
Sourcepub fn vm_bytes(&self) -> usize
pub fn vm_bytes(&self) -> usize
Bytes of local VM currently held by the arena stores.
Read from the stores rather than tracked in a counter, so it cannot
drift: restore truncates the stores directly, and a counter would
have to be decremented in lockstep at every such site to stay honest.
Measures reserved capacity, not length — see
crate::string_store::StringStore::data_capacity for why.
Sourcepub fn check_vm_alloc(&self, bytes: usize) -> Result<(), PsError>
pub fn check_vm_alloc(&self, bytes: usize) -> Result<(), PsError>
Refuse an allocation of bytes that would exceed Self::max_local_vm.
Call this before allocating, from any operator whose allocation size comes from the operand stack. Checking beforehand is the whole point: an allocation that fails aborts the process, so there is no error to catch afterwards.
Catches accumulation as well as single requests — { 1000000 string pop } loop never asks for more than a megabyte at a time, and only
the running total reveals it.
Sourcepub fn check_deadline(&mut self) -> Result<(), PsError>
pub fn check_deadline(&mut self) -> Result<(), PsError>
Check the deadline, cheaply.
Call once per eval-loop iteration. Almost every call decrements a
counter and returns; only every [DEADLINE_CHECK_INTERVAL]th consults
the clock.
Sourcepub fn exec_sync(&mut self, proc_obj: PsObject) -> Result<(), PsError>
pub fn exec_sync(&mut self, proc_obj: PsObject) -> Result<(), PsError>
Execute a PostScript procedure synchronously and return.
Sourcepub fn pump_proc_sources(&mut self, entity: EntityId) -> Result<(), PsError>
pub fn pump_proc_sources(&mut self, entity: EntityId) -> Result<(), PsError>
Run any not-yet-executed procedure data source underneath entity,
replacing it with the bytes it produces.
A filter’s data source may be a procedure (PLRM 3.8.4), which the
filter is supposed to call for more data as the consumer reads. Running
it when filter is called instead is observably wrong: the procedure
runs against whatever is on the operand stack at that moment. pdftops
builds an inline image as
<< /ImageType 1 ... /DataSource { pdfImStr } /LZWDecode filter >> imagemaskso filter is reached while the enclosing << ... >> is still on the
stack, and pdfImStr — which reads its array index state off the
stack — would pick up the half-built dictionary instead.
So the procedure is run here, from the read path, when the consumer’s
operands are the ones in place. Every entry point that reads from a
file must call this first; see [FileHandle::PendingProc].
The drain is one-shot rather than incremental: the procedure is run to completion and the result installed as a plain byte source. That keeps the filters themselves unchanged — none of them has to cope with a source that is temporarily dry — while still running the procedure at the right moment, which is what the bug above is about.
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a new context with empty stacks and stores.
Call build_system_dict afterward to populate operators.
Sourcepub fn new_with_output(output: Box<dyn Write>) -> Self
pub fn new_with_output(output: Box<dyn Write>) -> Self
Create a context that captures stdout to a buffer (for testing).
Sourcepub fn dict_load(&mut self, key: &DictKey) -> Option<PsObject>
pub fn dict_load(&mut self, key: &DictKey) -> Option<PsObject>
Look up a name in the dictionary stack (top to bottom).
Sourcepub fn invalidate_name_cache(&mut self)
pub fn invalidate_name_cache(&mut self)
Invalidate the name resolution cache (call on begin/end/def).
Sourcepub fn dict_where(&self, key: &DictKey) -> Option<(EntityId, PsObject)>
pub fn dict_where(&self, key: &DictKey) -> Option<(EntityId, PsObject)>
Look up and return (dict_entity, value) pair.
Sourcepub fn dict_def(&mut self, key: DictKey, value: PsObject) -> Result<(), PsError>
pub fn dict_def(&mut self, key: DictKey, value: PsObject) -> Result<(), PsError>
Store in current dict (top of d_stack).
Sourcepub fn dict_store(
&mut self,
key: DictKey,
value: PsObject,
) -> Result<(), PsError>
pub fn dict_store( &mut self, key: DictKey, value: PsObject, ) -> Result<(), PsError>
Store in first dict that contains key, or current dict if not found.
Sourcepub fn make_dict_key(&mut self, obj: &PsObject) -> Result<DictKey, PsError>
pub fn make_dict_key(&mut self, obj: &PsObject) -> Result<DictKey, PsError>
Convert a PsObject to a DictKey.
Sourcepub fn alloc_loop(&mut self, state: LoopState) -> EntityId
pub fn alloc_loop(&mut self, state: LoopState) -> EntityId
Allocate a new loop state, returning its EntityId.
Sourcepub fn get_loop_mut(&mut self, entity: EntityId) -> &mut LoopState
pub fn get_loop_mut(&mut self, entity: EntityId) -> &mut LoopState
Get a mutable loop state by EntityId.
Sourcepub fn current_display_list_mut(&mut self) -> &mut DisplayList
pub fn current_display_list_mut(&mut self) -> &mut DisplayList
Return the display list paint operators should currently append to.
While a transparency group is active (group_stack non-empty),
the topmost frame’s display list is returned. Otherwise the
page-level display_list is returned. Every paint-emitting
operator must route through this helper to keep group capture
correct.
Sourcepub fn current_display_list(&self) -> &DisplayList
pub fn current_display_list(&self) -> &DisplayList
Read-only counterpart to Self::current_display_list_mut.
Sourcepub fn take_display_list(&mut self) -> DisplayList
pub fn take_display_list(&mut self) -> DisplayList
Take the display list, optionally capturing a clone for viewport re-rendering.
This replaces std::mem::take(&mut ctx.display_list) at showpage/copypage
call sites. When capture_display_lists is active, a clone is saved
along with the current page DPI from the pagedevice HWResolution.
Sourcepub fn current_page_dpi(&self) -> f64
pub fn current_page_dpi(&self) -> f64
Read the current page DPI from the pagedevice HWResolution, defaulting to 72.
pub fn vm_save(&mut self) -> PsObject
Sourcepub fn vm_restore(&mut self, save_id: u32) -> Result<(), PsError>
pub fn vm_restore(&mut self, save_id: u32) -> Result<(), PsError>
Perform a restore: revert VM to the given save state.
Sourcepub fn cow_check_string(&mut self, entity: EntityId)
pub fn cow_check_string(&mut self, entity: EntityId)
Check if a string entity needs COW before mutation. If yes, creates a backup copy and records it.
Sourcepub fn cow_check_array(&mut self, entity: EntityId)
pub fn cow_check_array(&mut self, entity: EntityId)
Check if an array entity needs COW before mutation.
Sourcepub fn dict_put_cow(&mut self, entity: EntityId, key: DictKey, value: PsObject)
pub fn dict_put_cow(&mut self, entity: EntityId, key: DictKey, value: PsObject)
Check if a dict entity needs COW before mutation. Store into a dictionary, copy-on-writing it first.
Prefer this over a bare ctx.dicts.put for any write into a dictionary
the current operation did not itself allocate — FontDirectory, the
resource dictionaries, userdict, a caller-supplied dict. Writing
straight through bypasses save/restore: if the dictionary predates the
current save, no backup is taken and restore will not revert the
entry. That leaves the dictionary holding a value the restore released.
cow_check_dict is cheap and idempotent — it
returns immediately at save level 0, for global entities, and for
dictionaries already copied at this level — so there is no reason to
skip it when in doubt.
pub fn cow_check_dict(&mut self, entity: EntityId)
Sourcepub fn token_to_object(&mut self, token: Token) -> Result<PsObject, PsError>
pub fn token_to_object(&mut self, token: Token) -> Result<PsObject, PsError>
Convert a tokenizer token into a PsObject.
Sourcepub fn reset_local_vm(&mut self)
pub fn reset_local_vm(&mut self)
Reset local VM stores (for job boundary cleanup). Full implementation deferred until job server loop is built.