1#![deny(clippy::trivially_copy_pass_by_ref)]
7
8mod ct_helpers;
9mod gradient;
10mod text;
11
12use std::borrow::Cow;
13use std::sync::Arc;
14
15use core_graphics::base::{
16 kCGImageAlphaLast, kCGImageAlphaPremultipliedLast, kCGRenderingIntentDefault, CGFloat,
17};
18use core_graphics::color_space::CGColorSpace;
19use core_graphics::context::{CGContextRef, CGInterpolationQuality, CGLineCap, CGLineJoin};
20use core_graphics::data_provider::CGDataProvider;
21use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize};
22use core_graphics::gradient::CGGradientDrawingOptions;
23use core_graphics::image::CGImage;
24
25use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size};
26
27use piet::{
28 Color, Error, FixedGradient, Image, ImageFormat, InterpolationMode, IntoBrush, LineCap,
29 LineJoin, RenderContext, RoundInto, StrokeStyle,
30};
31
32pub use crate::text::{CoreGraphicsText, CoreGraphicsTextLayout, CoreGraphicsTextLayoutBuilder};
33
34use gradient::Gradient;
35
36const GRADIENT_DRAW_BEFORE_AND_AFTER: CGGradientDrawingOptions =
38 CGGradientDrawingOptions::from_bits_truncate(
39 CGGradientDrawingOptions::CGGradientDrawsAfterEndLocation.bits()
40 | CGGradientDrawingOptions::CGGradientDrawsBeforeStartLocation.bits(),
41 );
42
43pub struct CoreGraphicsContext<'a> {
44 ctx: &'a mut CGContextRef,
47 text: CoreGraphicsText,
48 transform_stack: Vec<Affine>,
53 y_down: bool,
54 height: f64,
55}
56
57impl<'a> CoreGraphicsContext<'a> {
58 pub fn new_y_up(
67 ctx: &mut CGContextRef,
68 height: f64,
69 text: Option<CoreGraphicsText>,
70 ) -> CoreGraphicsContext {
71 Self::new_impl(ctx, Some(height), text, false)
72 }
73
74 pub fn new_y_down(
81 ctx: &mut CGContextRef,
82 text: Option<CoreGraphicsText>,
83 ) -> CoreGraphicsContext {
84 Self::new_impl(ctx, None, text, true)
85 }
86
87 fn new_impl(
88 ctx: &mut CGContextRef,
89 height: Option<f64>,
90 text: Option<CoreGraphicsText>,
91 y_down: bool,
92 ) -> CoreGraphicsContext {
93 ctx.save();
94 if let Some(height) = height {
95 let xform = Affine::FLIP_Y * Affine::translate((0.0, -height));
96 ctx.concat_ctm(to_cgaffine(xform));
97 }
98 let text = text.unwrap_or_else(CoreGraphicsText::new_with_unique_state);
99
100 CoreGraphicsContext {
101 ctx,
102 text,
103 transform_stack: Vec::new(),
104 y_down,
105 height: height.unwrap_or_default(),
106 }
107 }
108}
109
110impl<'a> Drop for CoreGraphicsContext<'a> {
111 fn drop(&mut self) {
112 self.ctx.restore();
113 }
114}
115
116#[derive(Clone)]
117pub enum Brush {
118 Solid(Color),
119 Gradient(Gradient),
120}
121
122#[derive(Clone)]
124pub enum CoreGraphicsImage {
125 Empty,
128 YUp(CGImage),
129 YDown(CGImage),
130}
131
132impl CoreGraphicsImage {
133 fn from_cgimage_and_ydir(image: CGImage, y_down: bool) -> Self {
134 match y_down {
135 true => CoreGraphicsImage::YDown(image),
136 false => CoreGraphicsImage::YUp(image),
137 }
138 }
139 pub fn as_cgimage(&self) -> Option<&CGImage> {
140 match self {
141 CoreGraphicsImage::Empty => None,
142 CoreGraphicsImage::YUp(image) | CoreGraphicsImage::YDown(image) => Some(image),
143 }
144 }
145}
146
147impl<'a> RenderContext for CoreGraphicsContext<'a> {
148 type Brush = Brush;
149 type Text = CoreGraphicsText;
150 type TextLayout = CoreGraphicsTextLayout;
151 type Image = CoreGraphicsImage;
152
153 fn clear(&mut self, region: impl Into<Option<Rect>>, color: Color) {
154 let _ = self.save();
156 self.ctx.reset_clip();
158 let current_xform = self.current_transform();
160 let xform = current_xform.inverse();
161 self.transform(xform);
162
163 let region = region
164 .into()
165 .map(to_cgrect)
166 .unwrap_or_else(|| self.ctx.clip_bounding_box());
167 let (r, g, b, a) = color.as_rgba();
168 self.ctx
169 .set_blend_mode(core_graphics::context::CGBlendMode::Copy);
170 self.ctx.set_rgb_fill_color(r, g, b, a);
171 self.ctx.fill_rect(region);
172 self.restore().unwrap();
174 }
175
176 fn solid_brush(&mut self, color: Color) -> Brush {
177 Brush::Solid(color)
178 }
179
180 fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<Brush, Error> {
181 let gradient = Gradient::from_piet_gradient(gradient.into());
182 Ok(Brush::Gradient(gradient))
183 }
184
185 fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
187 let brush = brush.make_brush(self, || shape.bounding_box());
188 self.set_path(shape);
189 match brush.as_ref() {
190 Brush::Solid(color) => {
191 self.set_fill_color(*color);
192 self.ctx.fill_path();
193 }
194 Brush::Gradient(grad) => {
195 self.ctx.save();
196 self.ctx.clip();
197 grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
198 self.ctx.restore();
199 }
200 }
201 }
202
203 fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
204 let brush = brush.make_brush(self, || shape.bounding_box());
205 self.set_path(shape);
206 match brush.as_ref() {
207 Brush::Solid(color) => {
208 self.set_fill_color(*color);
209 self.ctx.eo_fill_path();
210 }
211 Brush::Gradient(grad) => {
212 self.ctx.save();
213 self.ctx.eo_clip();
214 grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
215 self.ctx.restore();
216 }
217 }
218 }
219
220 fn clip(&mut self, shape: impl Shape) {
221 self.set_path(shape);
222 self.ctx.clip();
223 }
224
225 fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
226 let brush = brush.make_brush(self, || shape.bounding_box());
227 self.set_path(shape);
228 self.set_stroke(width.round_into(), None);
229 match brush.as_ref() {
230 Brush::Solid(color) => {
231 self.set_stroke_color(*color);
232 self.ctx.stroke_path();
233 }
234 Brush::Gradient(grad) => {
235 self.ctx.save();
236 self.ctx.replace_path_with_stroked_path();
237 self.ctx.clip();
238 grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
239 self.ctx.restore();
240 }
241 }
242 }
243
244 fn stroke_styled(
245 &mut self,
246 shape: impl Shape,
247 brush: &impl IntoBrush<Self>,
248 width: f64,
249 style: &StrokeStyle,
250 ) {
251 let brush = brush.make_brush(self, || shape.bounding_box());
252 self.set_path(shape);
253 self.set_stroke(width.round_into(), Some(style));
254 match brush.as_ref() {
255 Brush::Solid(color) => {
256 self.set_stroke_color(*color);
257 self.ctx.stroke_path();
258 }
259 Brush::Gradient(grad) => {
260 self.ctx.save();
261 self.ctx.replace_path_with_stroked_path();
262 self.ctx.clip();
263 grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
264 self.ctx.restore();
265 }
266 }
267 }
268
269 fn text(&mut self) -> &mut Self::Text {
270 &mut self.text
271 }
272
273 fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into<Point>) {
274 let pos = pos.into();
275 self.ctx.save();
276 self.ctx.translate(pos.x, layout.frame_size.height + pos.y);
279 self.ctx.scale(1.0, -1.0);
280 layout.draw(self.ctx);
281 self.ctx.restore();
282 }
283
284 fn save(&mut self) -> Result<(), Error> {
285 self.ctx.save();
286 let state = self.transform_stack.last().copied().unwrap_or_default();
287 self.transform_stack.push(state);
288 Ok(())
289 }
290
291 fn restore(&mut self) -> Result<(), Error> {
292 if self.transform_stack.pop().is_some() {
293 self.ctx.restore();
296 Ok(())
297 } else {
298 Err(Error::StackUnbalance)
299 }
300 }
301
302 fn finish(&mut self) -> Result<(), Error> {
303 Ok(())
304 }
305
306 fn transform(&mut self, transform: Affine) {
307 if let Some(last) = self.transform_stack.last_mut() {
308 *last *= transform;
309 } else {
310 self.transform_stack.push(transform);
311 }
312 self.ctx.concat_ctm(to_cgaffine(transform));
313 }
314
315 fn make_image_with_stride(
316 &mut self,
317 width: usize,
318 height: usize,
319 stride: usize,
320 buf: &[u8],
321 format: ImageFormat,
322 ) -> Result<Self::Image, Error> {
323 if width == 0 || height == 0 {
324 return Ok(CoreGraphicsImage::Empty);
325 }
326 assert!(!buf.is_empty() && buf.len() <= format.bytes_per_pixel() * width * height);
327 let data = Arc::new(piet::util::image_buffer_to_tightly_packed(
328 buf, width, height, stride, format,
329 )?);
330 let data_provider = CGDataProvider::from_buffer(data);
331 let (colorspace, bitmap_info, bytes) = match format {
332 ImageFormat::Rgb => (CGColorSpace::create_device_rgb(), 0, 3),
333 ImageFormat::RgbaPremul => (
334 CGColorSpace::create_device_rgb(),
335 kCGImageAlphaPremultipliedLast,
336 4,
337 ),
338 ImageFormat::RgbaSeparate => (CGColorSpace::create_device_rgb(), kCGImageAlphaLast, 4),
339 ImageFormat::Grayscale => (CGColorSpace::create_device_gray(), 0, 1),
340 _ => unimplemented!(),
341 };
342 let bits_per_component = 8;
343 let should_interpolate = false;
345 let rendering_intent = kCGRenderingIntentDefault;
346 let image = CGImage::new(
347 width,
348 height,
349 bits_per_component,
350 bytes * bits_per_component,
351 width * bytes,
352 &colorspace,
353 bitmap_info,
354 &data_provider,
355 should_interpolate,
356 rendering_intent,
357 );
358
359 Ok(CoreGraphicsImage::from_cgimage_and_ydir(image, self.y_down))
360 }
361
362 fn draw_image(
363 &mut self,
364 src_image: &Self::Image,
365 rect: impl Into<Rect>,
366 interp: InterpolationMode,
367 ) {
368 let image_y_down: bool;
369 let image = match src_image {
370 CoreGraphicsImage::YDown(img) => {
371 image_y_down = true;
372 img
373 }
374 CoreGraphicsImage::YUp(img) => {
375 image_y_down = false;
376 img
377 }
378 CoreGraphicsImage::Empty => return,
379 };
380
381 self.ctx.save();
382 let quality = match interp {
384 InterpolationMode::NearestNeighbor => {
385 CGInterpolationQuality::CGInterpolationQualityNone
386 }
387 InterpolationMode::Bilinear => CGInterpolationQuality::CGInterpolationQualityDefault,
388 };
389 self.ctx.set_interpolation_quality(quality);
390 let rect = rect.into();
391
392 if self.y_down && !image_y_down {
393 self.ctx.draw_image(to_cgrect(rect), image);
395 } else {
396 self.ctx.translate(rect.min_x(), rect.max_y());
399 self.ctx.scale(1.0, -1.0);
400 self.ctx
401 .draw_image(to_cgrect(rect.with_origin(Point::ZERO)), image);
402 }
403
404 self.ctx.restore();
405 }
406
407 fn draw_image_area(
408 &mut self,
409 image: &Self::Image,
410 src_rect: impl Into<Rect>,
411 dst_rect: impl Into<Rect>,
412 interp: InterpolationMode,
413 ) {
414 if let CoreGraphicsImage::YDown(image) = image {
415 if let Some(cropped) = image.cropped(to_cgrect(src_rect)) {
416 self.draw_image(&CoreGraphicsImage::YDown(cropped), dst_rect, interp);
417 }
418 } else if let CoreGraphicsImage::YUp(image) = image {
419 if let Some(cropped) = image.cropped(to_cgrect(src_rect)) {
420 self.draw_image(&CoreGraphicsImage::YUp(cropped), dst_rect, interp);
421 }
422 }
423 }
424
425 fn capture_image_area(&mut self, src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
426 let src_rect = src_rect.into();
427
428 let src_cgrect = if self.y_down {
434 let matrix = self.ctx.get_ctm();
437 to_cgrect(src_rect).apply_transform(&matrix)
438 } else {
439 let y_dir_adjusted_src_rect = Rect::new(
442 src_rect.x0,
443 self.height - src_rect.y0,
444 src_rect.x1,
445 self.height - src_rect.y1,
446 );
447 let matrix = self.ctx.get_ctm();
448 to_cgrect(y_dir_adjusted_src_rect).apply_transform(&matrix)
449 };
450
451 if src_cgrect.size.width < 1.0 || src_cgrect.size.height < 1.0 {
452 return Err(Error::InvalidInput);
453 }
454
455 if src_cgrect.size.width > self.ctx.width() as f64
456 || src_cgrect.size.height > self.ctx.height() as f64
457 {
458 return Err(Error::InvalidInput);
459 }
460
461 let full_image = self.ctx.create_image().ok_or(Error::InvalidInput)?;
462
463 if src_cgrect.size.width.round() as usize == self.ctx.width()
464 && src_cgrect.size.height.round() as usize == self.ctx.height()
465 {
466 return Ok(CoreGraphicsImage::from_cgimage_and_ydir(
467 full_image,
468 self.y_down,
469 ));
470 }
471
472 let cropped_image_result = full_image.cropped(src_cgrect);
473 if let Some(image) = cropped_image_result {
474 let cropped_image_size = Size::new(src_cgrect.size.width, src_cgrect.size.height);
480 let cropped_image_rect = Rect::from_origin_size(Point::ZERO, cropped_image_size);
481 let cropped_image_context = core_graphics::context::CGContext::create_bitmap_context(
482 None,
483 cropped_image_size.width as usize,
484 cropped_image_size.height as usize,
485 8,
486 0,
487 &core_graphics::color_space::CGColorSpace::create_device_rgb(),
488 core_graphics::base::kCGImageAlphaPremultipliedLast,
489 );
490 cropped_image_context.draw_image(to_cgrect(cropped_image_rect), &image);
491 let cropped_image = cropped_image_context
492 .create_image()
493 .expect("Failed to capture cropped image from resize context");
494
495 Ok(CoreGraphicsImage::from_cgimage_and_ydir(
496 cropped_image,
497 self.y_down,
498 ))
499 } else {
500 Err(Error::InvalidInput)
501 }
502 }
503
504 fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush<Self>) {
505 let (image, rect) = compute_blurred_rect(rect, blur_radius);
506 let cg_rect = to_cgrect(rect);
507 self.ctx.save();
508 self.ctx.clip_to_mask(cg_rect, &image);
509 self.fill(rect, brush);
510 self.ctx.restore()
511 }
512
513 fn current_transform(&self) -> Affine {
514 self.transform_stack.last().copied().unwrap_or_default()
515 }
516
517 fn status(&mut self) -> Result<(), Error> {
518 Ok(())
519 }
520}
521
522impl<'a> IntoBrush<CoreGraphicsContext<'a>> for Brush {
523 fn make_brush<'b>(
524 &'b self,
525 _piet: &mut CoreGraphicsContext,
526 _bbox: impl FnOnce() -> Rect,
527 ) -> std::borrow::Cow<'b, Brush> {
528 Cow::Borrowed(self)
529 }
530}
531
532impl Image for CoreGraphicsImage {
533 fn size(&self) -> Size {
534 match self {
538 CoreGraphicsImage::Empty => Size::new(0., 0.),
539 CoreGraphicsImage::YDown(image) | CoreGraphicsImage::YUp(image) => {
540 Size::new(image.width() as f64, image.height() as f64)
541 }
542 }
543 }
544}
545
546fn convert_line_join(line_join: LineJoin) -> CGLineJoin {
547 match line_join {
548 LineJoin::Miter { .. } => CGLineJoin::CGLineJoinMiter,
549 LineJoin::Round => CGLineJoin::CGLineJoinRound,
550 LineJoin::Bevel => CGLineJoin::CGLineJoinBevel,
551 }
552}
553
554fn convert_line_cap(line_cap: LineCap) -> CGLineCap {
555 match line_cap {
556 LineCap::Butt => CGLineCap::CGLineCapButt,
557 LineCap::Round => CGLineCap::CGLineCapRound,
558 LineCap::Square => CGLineCap::CGLineCapSquare,
559 }
560}
561
562impl<'a> CoreGraphicsContext<'a> {
563 fn set_fill_color(&mut self, color: Color) {
564 let (r, g, b, a) = Color::as_rgba(color);
565 self.ctx.set_rgb_fill_color(r, g, b, a);
566 }
567
568 fn set_stroke_color(&mut self, color: Color) {
569 let (r, g, b, a) = Color::as_rgba(color);
570 self.ctx.set_rgb_stroke_color(r, g, b, a);
571 }
572
573 fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) {
575 let default_style = StrokeStyle::default();
576 let style = style.unwrap_or(&default_style);
577 self.ctx.set_line_width(width);
578
579 self.ctx.set_line_join(convert_line_join(style.line_join));
580 self.ctx.set_line_cap(convert_line_cap(style.line_cap));
581
582 if let Some(limit) = style.miter_limit() {
583 self.ctx.set_miter_limit(limit);
584 }
585
586 self.ctx
587 .set_line_dash(style.dash_offset, &style.dash_pattern);
588 }
589
590 fn set_path(&mut self, shape: impl Shape) {
591 self.ctx.begin_path();
594 let mut last = Point::default();
595 for el in shape.path_elements(1e-3) {
596 match el {
597 PathEl::MoveTo(p) => {
598 self.ctx.move_to_point(p.x, p.y);
599 last = p;
600 }
601 PathEl::LineTo(p) => {
602 self.ctx.add_line_to_point(p.x, p.y);
603 last = p;
604 }
605 PathEl::QuadTo(p1, p2) => {
606 let q = QuadBez::new(last, p1, p2);
607 let c = q.raise();
608 self.ctx
609 .add_curve_to_point(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y);
610 last = p2;
611 }
612 PathEl::CurveTo(p1, p2, p3) => {
613 self.ctx
614 .add_curve_to_point(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
615 last = p3;
616 }
617 PathEl::ClosePath => self.ctx.close_path(),
618 }
619 }
620 }
621}
622
623fn compute_blurred_rect(rect: Rect, radius: f64) -> (CGImage, Rect) {
624 let size = piet::util::size_for_blurred_rect(rect, radius);
625 let width = size.width as usize;
626 let height = size.height as usize;
627
628 let mut data = vec![0u8; width * height];
629 let rect_exp = piet::util::compute_blurred_rect(rect, radius, width, &mut data);
630
631 let data_provider = CGDataProvider::from_buffer(Arc::new(data));
632 let color_space = CGColorSpace::create_device_gray();
633 let image = CGImage::new(
634 width,
635 height,
636 8,
637 8,
638 width,
639 &color_space,
640 0,
641 &data_provider,
642 false,
643 0,
644 );
645 (image, rect_exp)
646}
647
648fn to_cgpoint(point: Point) -> CGPoint {
649 CGPoint::new(point.x as CGFloat, point.y as CGFloat)
650}
651
652fn to_cgsize(size: Size) -> CGSize {
653 CGSize::new(size.width, size.height)
654}
655
656fn to_cgrect(rect: impl Into<Rect>) -> CGRect {
657 let rect = rect.into();
658 CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size()))
659}
660
661fn to_cgaffine(affine: Affine) -> CGAffineTransform {
662 let [a, b, c, d, tx, ty] = affine.as_coeffs();
663 CGAffineTransform::new(a, b, c, d, tx, ty)
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use core_graphics::color_space::CGColorSpace;
670 use core_graphics::context::CGContext;
671
672 fn make_context(size: impl Into<Size>) -> CGContext {
673 let size = size.into();
674 CGContext::create_bitmap_context(
675 None,
676 size.width as usize,
677 size.height as usize,
678 8,
679 0,
680 &CGColorSpace::create_device_rgb(),
681 core_graphics::base::kCGImageAlphaPremultipliedLast,
682 )
683 }
684
685 fn equalish_affine(one: Affine, two: Affine) -> bool {
686 one.as_coeffs()
687 .iter()
688 .zip(two.as_coeffs().iter())
689 .all(|(a, b)| (a - b).abs() < f64::EPSILON)
690 }
691
692 macro_rules! assert_affine_eq {
693 ($left:expr, $right:expr) => {{
694 if !equalish_affine($left, $right) {
695 panic!(
696 "assertion failed: `(one == two)`\n\
697 one: {:?}\n\
698 two: {:?}",
699 $left.as_coeffs(),
700 $right.as_coeffs()
701 )
702 }
703 }};
704 }
705
706 #[test]
707 fn get_affine_y_up() {
708 let mut ctx = make_context((400.0, 400.0));
709 let mut piet = CoreGraphicsContext::new_y_up(&mut ctx, 400.0, None);
710 let affine = piet.current_transform();
711 assert_affine_eq!(affine, Affine::default());
712
713 let one = Affine::translate((50.0, 20.0));
714 let two = Affine::rotate(2.2);
715 let three = Affine::FLIP_Y;
716 let four = Affine::scale_non_uniform(2.0, -1.5);
717
718 piet.save().unwrap();
719 piet.transform(one);
720 piet.transform(one);
721 piet.save().unwrap();
722 piet.transform(two);
723 piet.save().unwrap();
724 piet.transform(three);
725 assert_affine_eq!(piet.current_transform(), one * one * two * three);
726 piet.transform(four);
727 piet.save().unwrap();
728
729 assert_affine_eq!(piet.current_transform(), one * one * two * three * four);
730 piet.restore().unwrap();
731 assert_affine_eq!(piet.current_transform(), one * one * two * three * four);
732 piet.restore().unwrap();
733 assert_affine_eq!(piet.current_transform(), one * one * two);
734 piet.restore().unwrap();
735 assert_affine_eq!(piet.current_transform(), one * one);
736 piet.restore().unwrap();
737 assert_affine_eq!(piet.current_transform(), Affine::default());
738 }
739
740 #[test]
741 fn capture_image_area() {
742 let mut ctx = make_context((400.0, 400.0));
743 let mut piet = CoreGraphicsContext::new_y_down(&mut ctx, None);
744
745 assert!(piet
746 .capture_image_area(Rect::new(0.0, 0.0, 0.0, 0.0))
747 .is_err());
748 assert!(piet
749 .capture_image_area(Rect::new(0.0, 0.0, 500.0, 400.0))
750 .is_err());
751 assert!(piet
752 .capture_image_area(Rect::new(100.0, 100.0, 200.0, 200.0))
753 .is_ok());
754
755 let copy = piet
756 .capture_image_area(Rect::new(100.0, 100.0, 200.0, 200.0))
757 .unwrap();
758
759 let unwrapped_copy = copy.as_cgimage().unwrap();
760 let rewrapped_copy = CoreGraphicsImage::from_cgimage_and_ydir(unwrapped_copy.clone(), true);
761
762 piet.draw_image(
763 &rewrapped_copy,
764 Rect::new(0.0, 0.0, 400.0, 400.0),
765 InterpolationMode::Bilinear,
766 );
767 }
768}