1use crate::FillRule;
2use crate::color::ColorSpace;
3use crate::context::Context;
4use crate::convert::{convert_line_cap, convert_line_join};
5use crate::device::Device;
6use crate::font::{Font, FontData, FontQuery, StandardFont};
7use crate::interpret::path::{
8 close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path,
9};
10use crate::interpret::state::{TextStateFont, handle_gs};
11use crate::interpret::text::TextRenderingMode;
12use crate::pattern::{Pattern, ShadingPattern};
13use crate::shading::Shading;
14use crate::util::{OptionLog, RectExt};
15use crate::x_object::{
16 FormXObject, ImageXObject, XObject, draw_form_xobject, draw_image_xobject, draw_xobject,
17};
18use hayro_syntax::content::TypedIter;
19use hayro_syntax::content::ops::TypedInstruction;
20use hayro_syntax::object::dict::keys::{ANNOTS, AP, F, MCID, N, OC, RECT};
21use hayro_syntax::object::{Array, Dict, Object, Rect, Stream, dict_or_stream};
22use hayro_syntax::page::{Page, Resources};
23use kurbo::{Affine, Point, Shape};
24use smallvec::smallvec;
25use std::sync::Arc;
26
27pub(crate) mod path;
28pub(crate) mod state;
29pub(crate) mod text;
30
31pub use state::ActiveTransferFunction;
32
33pub type FontResolverFn = Arc<dyn Fn(&FontQuery) -> Option<(FontData, u32)> + Send + Sync>;
38pub type CMapResolverFn =
40 Arc<dyn Fn(hayro_cmap::CMapName<'_>) -> Option<&'static [u8]> + Send + Sync>;
41pub type WarningSinkFn = Arc<dyn Fn(InterpreterWarning) + Send + Sync>;
43
44#[derive(Clone)]
45pub struct InterpreterSettings {
47 pub font_resolver: FontResolverFn,
83 pub cmap_resolver: CMapResolverFn,
96 pub warning_sink: WarningSinkFn,
99 pub render_annotations: bool,
104}
105
106impl Default for InterpreterSettings {
107 fn default() -> Self {
108 Self {
109 #[cfg(not(feature = "embed-fonts"))]
110 font_resolver: Arc::new(|_| None),
111 #[cfg(feature = "embed-fonts")]
112 font_resolver: Arc::new(|query| match query {
113 FontQuery::Standard(s) => Some(s.get_font_data()),
114 FontQuery::Fallback(f) => Some(f.pick_standard_font().get_font_data()),
115 }),
116 #[cfg(feature = "embed-cmaps")]
117 cmap_resolver: Arc::new(hayro_cmap::load_embedded),
118 #[cfg(not(feature = "embed-cmaps"))]
119 cmap_resolver: Arc::new(|_| None),
120 warning_sink: Arc::new(|_| {}),
121 render_annotations: true,
122 }
123 }
124}
125
126#[derive(Copy, Clone, Debug)]
127pub enum InterpreterWarning {
129 UnsupportedFont,
133 ImageDecodeFailure,
135}
136
137pub fn interpret_page<'a>(
139 page: &Page<'a>,
140 context: &mut Context<'a>,
141 device: &mut impl Device<'a>,
142) {
143 let resources = page.resources();
144 interpret(page.typed_operations(), resources, context, device);
145
146 if context.settings.render_annotations
147 && let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
148 {
149 for annot in annot_arr.iter::<Dict<'_>>() {
150 let flags = annot.get::<u32>(F).unwrap_or(0);
151
152 if flags & 2 != 0 {
154 continue;
155 }
156
157 if let Some(apx) = annot
158 .get::<Dict<'_>>(AP)
159 .and_then(|ap| ap.get::<Stream<'_>>(N))
160 .and_then(|o| FormXObject::new(&o))
161 {
162 let Some(rect) = annot.get::<Rect>(RECT) else {
163 continue;
164 };
165
166 let annot_rect = rect.to_kurbo();
167 let transformed_rect = (apx.matrix
177 * kurbo::Rect::new(
178 apx.bbox[0] as f64,
179 apx.bbox[1] as f64,
180 apx.bbox[2] as f64,
181 apx.bbox[3] as f64,
182 )
183 .to_path(0.1))
184 .bounding_box();
185
186 let affine = Affine::new([
195 annot_rect.width() / transformed_rect.width(),
196 0.0,
197 0.0,
198 annot_rect.height() / transformed_rect.height(),
199 annot_rect.x0 - transformed_rect.x0,
200 annot_rect.y0 - transformed_rect.y0,
201 ]);
202
203 context.save_state();
207 context.pre_concat_affine(affine);
208 context.push_root_transform();
209
210 draw_form_xobject(resources, &apx, context, device);
211 context.pop_root_transform();
212 context.restore_state(device);
213 }
214 }
215 }
216}
217
218pub fn interpret<'a>(
220 mut ops: TypedIter<'_>,
221 resources: &Resources<'a>,
222 context: &mut Context<'a>,
223 device: &mut impl Device<'a>,
224) {
225 let num_states = context.num_states();
226
227 context.save_state();
228
229 while let Some(op) = ops.next() {
230 match op {
231 TypedInstruction::SaveState(_) => context.save_state(),
232 TypedInstruction::StrokeColorDeviceRgb(s) => {
233 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
234 context.get_mut().graphics_state.stroke_color =
235 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
236 context.get_mut().graphics_state.stroke_pattern = None;
237 }
238 TypedInstruction::StrokeColorDeviceGray(s) => {
239 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
240 context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
241 context.get_mut().graphics_state.stroke_pattern = None;
242 }
243 TypedInstruction::StrokeColorCmyk(s) => {
244 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
245 context.get_mut().graphics_state.stroke_color =
246 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
247 context.get_mut().graphics_state.stroke_pattern = None;
248 }
249 TypedInstruction::LineWidth(w) => {
250 context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
251 }
252 TypedInstruction::LineCap(c) => {
253 context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
254 }
255 TypedInstruction::LineJoin(j) => {
256 context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
257 }
258 TypedInstruction::MiterLimit(l) => {
259 context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
260 }
261 TypedInstruction::Transform(t) => {
262 context.pre_concat_transform(t);
263 }
264 TypedInstruction::RectPath(r) => {
265 let rect = kurbo::Rect::new(
266 r.0.as_f64(),
267 r.1.as_f64(),
268 r.0.as_f64() + r.2.as_f64(),
269 r.1.as_f64() + r.3.as_f64(),
270 )
271 .to_path(0.1);
272 context.path_mut().extend(rect);
273 }
274 TypedInstruction::MoveTo(m) => {
275 let p = Point::new(m.0.as_f64(), m.1.as_f64());
276 *(context.last_point_mut()) = p;
277 *(context.sub_path_start_mut()) = p;
278 context.path_mut().move_to(p);
279 }
280 TypedInstruction::FillPathEvenOdd(_) => {
281 fill_path(context, device, FillRule::EvenOdd);
282 }
283 TypedInstruction::FillPathNonZero(_) => {
284 fill_path(context, device, FillRule::NonZero);
285 }
286 TypedInstruction::FillPathNonZeroCompatibility(_) => {
287 fill_path(context, device, FillRule::NonZero);
288 }
289 TypedInstruction::FillAndStrokeEvenOdd(_) => {
290 fill_stroke_path(context, device, FillRule::EvenOdd);
291 }
292 TypedInstruction::FillAndStrokeNonZero(_) => {
293 fill_stroke_path(context, device, FillRule::NonZero);
294 }
295 TypedInstruction::CloseAndStrokePath(_) => {
296 close_path(context);
297 stroke_path(context, device);
298 }
299 TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
300 close_path(context);
301 fill_stroke_path(context, device, FillRule::EvenOdd);
302 }
303 TypedInstruction::CloseFillAndStrokeNonZero(_) => {
304 close_path(context);
305 fill_stroke_path(context, device, FillRule::NonZero);
306 }
307 TypedInstruction::NonStrokeColorDeviceGray(s) => {
308 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
309 context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
310 context.get_mut().graphics_state.non_stroke_pattern = None;
311 }
312 TypedInstruction::NonStrokeColorDeviceRgb(s) => {
313 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
314 context.get_mut().graphics_state.non_stroke_color =
315 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
316 context.get_mut().graphics_state.non_stroke_pattern = None;
317 }
318 TypedInstruction::NonStrokeColorCmyk(s) => {
319 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
320 context.get_mut().graphics_state.non_stroke_color =
321 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
322 context.get_mut().graphics_state.non_stroke_pattern = None;
323 }
324 TypedInstruction::LineTo(m) => {
325 if !context.path().elements().is_empty() {
326 let last_point = *context.last_point();
327 let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
328 *(context.last_point_mut()) = p;
329 if last_point == p {
330 p.x += 0.0001;
332 }
333
334 context.path_mut().line_to(p);
335 }
336 }
337 TypedInstruction::CubicTo(c) => {
338 if !context.path().elements().is_empty() {
339 let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
340 let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
341 let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
342
343 *(context.last_point_mut()) = p3;
344
345 context.path_mut().curve_to(p1, p2, p3);
346 }
347 }
348 TypedInstruction::CubicStartTo(c) => {
349 if !context.path().elements().is_empty() {
350 let p1 = *context.last_point();
351 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
352 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
353
354 *(context.last_point_mut()) = p3;
355
356 context.path_mut().curve_to(p1, p2, p3);
357 }
358 }
359 TypedInstruction::CubicEndTo(c) => {
360 if !context.path().elements().is_empty() {
361 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
362 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
363
364 *(context.last_point_mut()) = p3;
365
366 context.path_mut().curve_to(p2, p3, p3);
367 }
368 }
369 TypedInstruction::ClosePath(_) => {
370 close_path(context);
371 }
372 TypedInstruction::SetGraphicsState(gs) => {
373 if let Some(gs) = resources
374 .get_ext_g_state(gs.0)
375 .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
376 {
377 handle_gs(&gs, context, resources);
378 }
379 }
380 TypedInstruction::StrokePath(_) => {
381 stroke_path(context, device);
382 }
383 TypedInstruction::EndPath(_) => {
384 if let Some(clip) = *context.clip()
385 && !context.path().elements().is_empty()
386 {
387 let clip_path = context.get().ctm * context.path().clone();
388 context.push_clip_path(clip_path, clip, device);
389
390 *(context.clip_mut()) = None;
391 }
392
393 context.path_mut().truncate(0);
394 }
395 TypedInstruction::NonStrokeColor(c) => {
396 let gs = &mut context.get_mut().graphics_state;
397 gs.non_stroke_color = c.0.into_iter().map(|n| n.as_f32()).collect();
398 gs.non_stroke_pattern = None;
399 }
400 TypedInstruction::StrokeColor(c) => {
401 let gs = &mut context.get_mut().graphics_state;
402 gs.stroke_color = c.0.into_iter().map(|n| n.as_f32()).collect();
403 gs.stroke_pattern = None;
404 }
405 TypedInstruction::ClipNonZero(_) => {
406 *(context.clip_mut()) = Some(FillRule::NonZero);
407 }
408 TypedInstruction::ClipEvenOdd(_) => {
409 *(context.clip_mut()) = Some(FillRule::EvenOdd);
410 }
411 TypedInstruction::RestoreState(_) => context.restore_state(device),
412 TypedInstruction::FlatnessTolerance(_) => {
413 }
415 TypedInstruction::ColorSpaceStroke(c) => {
416 let cs = if let Some(named) = ColorSpace::new_from_name(c.0) {
417 named
418 } else {
419 context
420 .get_color_space(resources, c.0)
421 .unwrap_or(ColorSpace::device_gray())
422 };
423
424 if !cs.is_pattern() {
425 context.get_mut().graphics_state.stroke_pattern = None;
426 }
427 context.get_mut().graphics_state.stroke_color = cs.initial_color();
428 context.get_mut().graphics_state.stroke_cs = cs;
429 }
430 TypedInstruction::ColorSpaceNonStroke(c) => {
431 let cs = if let Some(named) = ColorSpace::new_from_name(c.0) {
432 named
433 } else {
434 context
435 .get_color_space(resources, c.0)
436 .unwrap_or(ColorSpace::device_gray())
437 };
438
439 if !cs.is_pattern() {
440 context.get_mut().graphics_state.non_stroke_pattern = None;
441 }
442 context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
443 context.get_mut().graphics_state.none_stroke_cs = cs;
444 }
445 TypedInstruction::DashPattern(p) => {
446 context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
447 context.get_mut().graphics_state.stroke_props.dash_array =
449 p.0.iter::<f32>()
450 .map(|n| if n == 0.0 { 0.01 } else { n })
451 .collect();
452 }
453 TypedInstruction::RenderingIntent(_) => {
454 }
456 TypedInstruction::NonStrokeColorNamed(n) => {
457 context.get_mut().graphics_state.non_stroke_color =
458 n.0.into_iter().map(|n| n.as_f32()).collect();
459 context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
460 resources
461 .get_pattern(name)
462 .and_then(|d| Pattern::new(d, context, resources))
463 });
464 }
465 TypedInstruction::StrokeColorNamed(n) => {
466 context.get_mut().graphics_state.stroke_color =
467 n.0.into_iter().map(|n| n.as_f32()).collect();
468 context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
469 resources
470 .get_pattern(name)
471 .and_then(|d| Pattern::new(d, context, resources))
472 });
473 }
474 TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
475 let mcid = dict_or_stream(bdc.1).and_then(|(props, _)| props.get::<i32>(MCID));
480
481 let oc = bdc
482 .1
483 .clone()
484 .into_name()
485 .and_then(|name| {
486 let r = resources.properties.get_ref(name.as_ref())?;
487 let d = resources
488 .properties
489 .get::<Dict<'_>>(name)
490 .unwrap_or_default();
491 Some((d, r))
492 })
493 .or_else(|| {
494 let (props, _) = dict_or_stream(bdc.1)?;
495 let r = props.get_ref(OC)?;
496 let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
497 Some((d, r))
498 });
499
500 if let Some((dict, oc_ref)) = oc {
501 context.ocg_state.begin_ocg(&dict, oc_ref.into());
502 } else {
503 context.ocg_state.begin_marked_content();
504 }
505
506 device.begin_marked_content(bdc.0, mcid);
507 }
508 TypedInstruction::MarkedContentPointWithProperties(_) => {}
509 TypedInstruction::EndMarkedContent(_) => {
510 context.ocg_state.end_marked_content();
511 device.end_marked_content();
512 }
513 TypedInstruction::MarkedContentPoint(_) => {}
514 TypedInstruction::BeginMarkedContent(bmc) => {
515 context.ocg_state.begin_marked_content();
516 device.begin_marked_content(bmc.0, None);
517 }
518 TypedInstruction::BeginText(_) => {
519 context.get_mut().text_state.text_matrix = Affine::IDENTITY;
520 context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
521 }
522 TypedInstruction::SetTextMatrix(m) => {
523 let m = Affine::new([
524 m.0.as_f64(),
525 m.1.as_f64(),
526 m.2.as_f64(),
527 m.3.as_f64(),
528 m.4.as_f64(),
529 m.5.as_f64(),
530 ]);
531 context.get_mut().text_state.text_line_matrix = m;
532 context.get_mut().text_state.text_matrix = m;
533 }
534 TypedInstruction::EndText(_) => {
535 let has_outline = context
536 .get()
537 .text_state
538 .clip_paths
539 .segments()
540 .next()
541 .is_some();
542
543 if has_outline {
544 let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
545
546 context.push_clip_path(clip_path, FillRule::NonZero, device);
547 }
548
549 context.get_mut().text_state.clip_paths.truncate(0);
550 }
551 TypedInstruction::TextFont(t) => {
552 let name = t.0;
553
554 let font = if let Some(font_dict) = resources.get_font(name) {
561 context.resolve_font(&font_dict)
562 } else {
563 Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver)
564 .map(TextStateFont::Fallback)
565 };
566
567 context.get_mut().text_state.font_size = t.1.as_f32();
568 context.get_mut().text_state.font = font;
569 }
570 TypedInstruction::ShowText(s) => {
571 if context.get().text_state.font.is_none() {
572 context.get_mut().text_state.font = Font::new_standard(
575 StandardFont::Helvetica,
576 &context.settings.font_resolver,
577 )
578 .map(TextStateFont::Fallback);
579 }
580
581 text::show_text_string(context, device, resources, s.0);
582 }
583 TypedInstruction::ShowTexts(s) => {
584 if context.get().text_state.font.is_none() {
585 context.get_mut().text_state.font = Font::new_standard(
588 StandardFont::Helvetica,
589 &context.settings.font_resolver,
590 )
591 .map(TextStateFont::Fallback);
592 }
593
594 for obj in s.0.iter::<Object<'_>>() {
595 match obj {
596 Object::Number(num) => {
597 context.get_mut().text_state.apply_adjustment(num.as_f32());
598 }
599 Object::String(text) => {
600 text::show_text_string(context, device, resources, &text);
601 }
602 _ => {}
603 }
604 }
605 }
606 TypedInstruction::HorizontalScaling(h) => {
607 context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
608 }
609 TypedInstruction::TextLeading(tl) => {
610 context.get_mut().text_state.leading = tl.0.as_f32();
611 }
612 TypedInstruction::CharacterSpacing(c) => {
613 context.get_mut().text_state.char_space = c.0.as_f32();
614 }
615 TypedInstruction::WordSpacing(w) => {
616 context.get_mut().text_state.word_space = w.0.as_f32();
617 }
618 TypedInstruction::NextLine(n) => {
619 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
620 text::next_line(context, tx, ty);
621 }
622 TypedInstruction::NextLineUsingLeading(_) => {
623 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
624 }
625 TypedInstruction::NextLineAndShowText(n) => {
626 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
627 text::show_text_string(context, device, resources, n.0);
628 }
629 TypedInstruction::TextRenderingMode(r) => {
630 let mode = match r.0.as_i64() {
631 0 => TextRenderingMode::Fill,
632 1 => TextRenderingMode::Stroke,
633 2 => TextRenderingMode::FillStroke,
634 3 => TextRenderingMode::Invisible,
635 4 => TextRenderingMode::FillAndClip,
636 5 => TextRenderingMode::StrokeAndClip,
637 6 => TextRenderingMode::FillAndStrokeAndClip,
638 7 => TextRenderingMode::Clip,
639 _ => {
640 warn!("unknown text rendering mode {}", r.0.as_i64());
641
642 TextRenderingMode::Fill
643 }
644 };
645
646 context.get_mut().text_state.render_mode = mode;
647 }
648 TypedInstruction::NextLineAndSetLeading(n) => {
649 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
650 context.get_mut().text_state.leading = -ty as f32;
651 text::next_line(context, tx, ty);
652 }
653 TypedInstruction::ShapeGlyph(_) => {}
654 TypedInstruction::XObject(x) => {
655 let cache = context.interpreter_cache.object_cache.clone();
656 let transfer_function = context.get().graphics_state.transfer_function.clone();
657 if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
658 XObject::new(
659 &s,
660 &context.settings.warning_sink,
661 &cache,
662 transfer_function.clone(),
663 )
664 }) {
665 draw_xobject(&x_object, resources, context, device);
666 }
667 }
668 TypedInstruction::InlineImage(i) => {
669 let warning_sink = context.settings.warning_sink.clone();
670 let transfer_function = context.get().graphics_state.transfer_function.clone();
671 let cache = context.interpreter_cache.object_cache.clone();
672 if let Some(x_object) = ImageXObject::new(
673 i.0,
674 |name| context.get_color_space(resources, name),
675 &warning_sink,
676 &cache,
677 false,
678 transfer_function,
679 ) {
680 draw_image_xobject(&x_object, context, device);
681 }
682 }
683 TypedInstruction::TextRise(t) => {
684 context.get_mut().text_state.rise = t.0.as_f32();
685 }
686 TypedInstruction::Shading(s) => {
687 if !context.ocg_state.is_visible() {
688 continue;
689 }
690
691 let transfer_function = context.get().graphics_state.transfer_function.clone();
692
693 if let Some(sp) = resources
694 .get_shading(s.0)
695 .and_then(|o| {
696 let (dict, stream) = dict_or_stream(&o)?;
697 Shading::new(dict, stream, &context.interpreter_cache.object_cache)
698 })
699 .map(|s| {
700 Pattern::Shading(ShadingPattern {
701 shading: Arc::new(s),
702 matrix: Affine::IDENTITY,
703 opacity: context.get().graphics_state.non_stroke_alpha,
704 transfer_function: transfer_function.clone(),
705 })
706 })
707 {
708 context.save_state();
709 context.push_root_transform();
710 let st = context.get_mut();
711 st.graphics_state.non_stroke_pattern = Some(sp);
712 st.graphics_state.none_stroke_cs = ColorSpace::pattern();
713
714 device.set_soft_mask(st.graphics_state.soft_mask.clone());
715 device.set_blend_mode(st.graphics_state.blend_mode);
716
717 let bbox = context.bbox().to_path(0.1);
718 let inverted_bbox = context.get().ctm.inverse() * bbox;
719 fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
720
721 context.pop_root_transform();
722 context.restore_state(device);
723 } else {
724 warn!("failed to process shading");
725 }
726 }
727 TypedInstruction::BeginCompatibility(_) => {}
728 TypedInstruction::EndCompatibility(_) => {}
729 TypedInstruction::ColorGlyph(_) => {}
730 TypedInstruction::ShowTextWithParameters(t) => {
731 context.get_mut().text_state.word_space = t.0.as_f32();
732 context.get_mut().text_state.char_space = t.1.as_f32();
733 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
734 text::show_text_string(context, device, resources, t.2);
735 }
736 _ => {
737 warn!("failed to read an operator");
738 }
739 }
740 }
741
742 while context.num_states() > num_states {
743 context.restore_state(device);
744 }
745}