public void DrawString(char[] buffer, int startAt, int len, double left, double top) { _vboBuilder.Clear(); _vboBuilder.SetTextureInfo(_glBmp.Width, _glBmp.Height, _glBmp.IsYFlipped, _pcx.OriginKind); //create temp buffer span that describe the part of a whole char buffer TextBufferSpan textBufferSpan = new TextBufferSpan(buffer, startAt, len); //ask text service to parse user input char buffer and create a glyph-plan-sequence (list of glyph-plan) //with specific request font GlyphPlanSequence glyphPlanSeq = _textServices.CreateGlyphPlanSeq(ref textBufferSpan, _font); float px_scale = _px_scale; //-------------------------- //TODO: //if (x,y) is left top //we need to adjust y again float scaleFromTexture = _font.SizeInPoints / _fontAtlas.OriginalFontSizePts; TextureKind textureKind = _fontAtlas.TextureKind; float g_left = 0; float g_top = 0; int baseLine = (int)Math.Round((float)top + _font.AscentInPixels); int bottom = (int)Math.Round((float)top + _font.AscentInPixels - _font.DescentInPixels); float acc_x = 0; //local accumulate x float acc_y = 0; //local accumulate y #if DEBUG if (s_dbugShowMarkers) { if (s_dbugShowGlyphTexture) { //show original glyph texture at top _pcx.DrawImage(_glBmp, 0, 0); } //draw red-line-marker for baseLine _painter.StrokeColor = Color.Red; _painter.DrawLine(left, baseLine, left + 200, baseLine); // //draw magenta-line-marker for bottom line _painter.StrokeColor = Color.Magenta; int bottomLine = (int)Math.Round((float)top + _font.LineSpacingInPixels); _painter.DrawLine(left, bottomLine, left + 200, bottomLine); //draw blue-line-marker for top line _painter.StrokeColor = Color.Blue; _painter.DrawLine(0, top, left + 200, top); } DrawingTechnique = s_dbugDrawTechnique; //for debug only UseVBO = s_dbugUseVBO; //for debug only #endif if (textureKind == TextureKind.Msdf) { DrawingTechnique = GlyphTexturePrinterDrawingTechnique.Msdf; } //---------- int seqLen = glyphPlanSeq.Count; for (int i = 0; i < seqLen; ++i) { UnscaledGlyphPlan glyph = glyphPlanSeq[i]; Typography.Rendering.TextureGlyphMapData glyphData; if (!_fontAtlas.TryGetGlyphMapData(glyph.glyphIndex, out glyphData)) { //if no glyph data, we should render a missing glyph *** continue; } //-------------------------------------- //TODO: review precise height in float //-------------------------------------- //paint src rect //temp fix, glyph texture img is not flipped //but the associate info is flipped => so //we need remap exact Y from the image Rectangle srcRect = new Rectangle(glyphData.Left, _glBmp.Height - (glyphData.Top + glyphData.Height), glyphData.Width, glyphData.Height); //offset length from 'base-line' float x_offset = acc_x + (float)Math.Round(glyph.OffsetX * px_scale - glyphData.TextureXOffset * scaleFromTexture); float y_offset = acc_y + (float)Math.Round(glyph.OffsetY * px_scale - glyphData.TextureYOffset * scaleFromTexture) + srcRect.Height; //*** //NOTE: // -glyphData.TextureXOffset => restore to original pos // -glyphData.TextureYOffset => restore to original pos //-------------------------- g_left = (float)(left + x_offset); g_top = (float)(bottom - y_offset); //*** acc_x += (float)Math.Round(glyph.AdvanceX * px_scale); g_top = (float)Math.Floor(g_top);//adjust to integer num *** #if DEBUG if (s_dbugShowMarkers) { if (s_dbugShowGlyphTexture) { //draw yellow-rect-marker on original texture _painter.DrawRectangle(srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height, Color.Yellow); } //draw debug-rect box at target glyph position _painter.DrawRectangle(g_left, g_top, srcRect.Width, srcRect.Height, Color.Black); _painter.StrokeColor = Color.Blue; //restore } //System.Diagnostics.Debug.WriteLine( // "ds:" + buffer[0] + "o=(" + left + "," + top + ")" + // "g=(" + g_left + "," + g_top + ")" + "srcRect=" + srcRect); #endif if (UseVBO) { _vboBuilder.WriteVboToList( ref srcRect, g_left, g_top, scaleFromTexture); } else { switch (DrawingTechnique) { case GlyphTexturePrinterDrawingTechnique.Msdf: _pcx.DrawSubImageWithMsdf(_glBmp, ref srcRect, g_left, g_top, scaleFromTexture); break; case GlyphTexturePrinterDrawingTechnique.Stencil: //stencil gray scale with fill-color _pcx.DrawGlyphImageWithStecil(_glBmp, ref srcRect, g_left, g_top, scaleFromTexture); break; case GlyphTexturePrinterDrawingTechnique.Copy: _pcx.DrawSubImage(_glBmp, ref srcRect, g_left, g_top, 1); break; case GlyphTexturePrinterDrawingTechnique.LcdSubPixelRendering: _pcx.DrawGlyphImageWithSubPixelRenderingTechnique2_GlyphByGlyph( _glBmp, ref srcRect, g_left, g_top, 1); break; } } } //------------------------------------------- // if (UseVBO) { switch (DrawingTechnique) { case GlyphTexturePrinterDrawingTechnique.Copy: _pcx.DrawGlyphImageWithCopy_VBO(_glBmp, _vboBuilder); break; case GlyphTexturePrinterDrawingTechnique.LcdSubPixelRendering: _pcx.DrawGlyphImageWithSubPixelRenderingTechnique3_DrawElements(_glBmp, _vboBuilder); break; case GlyphTexturePrinterDrawingTechnique.Stencil: _pcx.DrawGlyphImageWithStecil_VBO(_glBmp, _vboBuilder); break; case GlyphTexturePrinterDrawingTechnique.Msdf: _pcx.DrawImagesWithMsdf_VBO(_glBmp, _vboBuilder); break; } _vboBuilder.Clear(); } }
public override void DrawFromGlyphPlans(GlyphPlanSequence seq, int startAt, int len, float left, float top) { if (StartDrawOnLeftTop) { //version 2 //offset y down top += this.FontLineSpacingPx; } float fontSizePoint = this.FontSizeInPoints; float scale = _currentTypeface.CalculateScaleToPixelFromPointSize(fontSizePoint); //4. render each glyph float ox = _painter.OriginX; float oy = _painter.OriginY; Typography.OpenFont.Tables.COLR colrTable = _currentTypeface.COLRTable; Typography.OpenFont.Tables.CPAL cpalTable = _currentTypeface.CPALTable; bool hasColorGlyphs = (colrTable != null) && (cpalTable != null); //--------------------------------------------------- _glyphMeshStore.SetHintTechnique(this.HintTechnique); _glyphMeshStore.SetFont(_currentTypeface, fontSizePoint); _glyphMeshStore.SimulateOblique = this.SimulateSlant; //--------------------------------------------------- if (_currentTypeface.HasSvgTable()) { _glyphSvgStore.SetCurrentTypeface(_currentTypeface); int seqLen = seq.Count; if (len > seqLen) { len = seqLen; } var snapToPx = new GlyphPlanSequenceSnapPixelScaleLayout(seq, startAt, len, scale); while (snapToPx.Read()) { _painter.SetOrigin((float)Math.Round(left + snapToPx.ExactX) + 0.33f, (float)Math.Floor(top + snapToPx.ExactY)); GlyphBitmap glyphBmp = _glyphSvgStore.GetGlyphBitmap(snapToPx.CurrentGlyphIndex); //how to draw the image //1. if (glyphBmp != null) { _painter.DrawImage(glyphBmp.Bitmap); } } } else if (_currentTypeface.IsBitmapFont) { //check if we have exported all the glyph bitmap //to some 'ready' form? //if not then create it _glyphBitmapStore.SetCurrentTypeface(_currentTypeface); int seqLen = seq.Count; if (len > seqLen) { len = seqLen; } var snapToPx = new GlyphPlanSequenceSnapPixelScaleLayout(seq, startAt, len, scale); while (snapToPx.Read()) { _painter.SetOrigin((float)Math.Round(left + snapToPx.ExactX) + 0.33f, (float)Math.Floor(top + snapToPx.ExactY)); GlyphBitmap glyphBmp = _glyphBitmapStore.GetGlyphBitmap(snapToPx.CurrentGlyphIndex); //how to draw the image //1. _painter.DrawImage(glyphBmp.Bitmap); } } else { if (!hasColorGlyphs) { bool savedUseLcdMode = _painter.UseSubPixelLcdEffect; //save,restore later RenderQuality savedRederQuality = _painter.RenderQuality; _painter.RenderQuality = RenderQuality.HighQuality; _painter.UseSubPixelLcdEffect = true; int seqLen = seq.Count; if (len > seqLen) { len = seqLen; } var snapToPx = new GlyphPlanSequenceSnapPixelScaleLayout(seq, startAt, len, scale); while (snapToPx.Read()) { _painter.SetOrigin((float)Math.Round(left + snapToPx.ExactX) + 0.33f, (float)Math.Floor(top + snapToPx.ExactY)); _painter.Fill(_glyphMeshStore.GetGlyphMesh(snapToPx.CurrentGlyphIndex)); } //restore _painter.RenderQuality = savedRederQuality; _painter.UseSubPixelLcdEffect = savedUseLcdMode; } else { //------------- //this glyph has color information //------------- Color originalFillColor = _painter.FillColor; int seqLen = seq.Count; if (len > seqLen) { len = seqLen; } var snapToPx = new GlyphPlanSequenceSnapPixelScaleLayout(seq, startAt, len, scale); while (snapToPx.Read()) { _painter.SetOrigin((float)Math.Round(left + snapToPx.ExactX), (float)Math.Floor(top + snapToPx.ExactY)); ushort colorLayerStart; if (colrTable.LayerIndices.TryGetValue(snapToPx.CurrentGlyphIndex, out colorLayerStart)) { //TODO: optimize this //we found color info for this glyph ushort colorLayerCount = colrTable.LayerCounts[snapToPx.CurrentGlyphIndex]; byte r, g, b, a; for (int c = colorLayerStart; c < colorLayerStart + colorLayerCount; ++c) { ushort gIndex = colrTable.GlyphLayers[c]; int palette = 0; // FIXME: assume palette 0 for now cpalTable.GetColor( cpalTable.Palettes[palette] + colrTable.GlyphPalettes[c], //index out r, out g, out b, out a); //----------- _painter.FillColor = new Color(r, g, b);//? a component _painter.Fill(_glyphMeshStore.GetGlyphMesh(gIndex)); } } else { //----------------------------------- //TODO: review here *** //PERFORMANCE revisit here //if we have create a vxs we can cache it for later use? //----------------------------------- _painter.Fill(_glyphMeshStore.GetGlyphMesh(snapToPx.CurrentGlyphIndex)); } } _painter.FillColor = originalFillColor; //restore color } } //restore prev origin _painter.SetOrigin(ox, oy); }
public override void DrawFromGlyphPlans(GlyphPlanSequence glyphPlanSeq, int startAt, int len, float left, float top) { Typeface typeface = _textServices.ResolveTypeface(_font); float scale = typeface.CalculateScaleToPixelFromPointSize(_font.SizeInPoints); int recommendLineSpacing = (int)_font.LineSpacingInPixels; //-------------------------- //TODO: //if (x,y) is left top //we need to adjust y again // TextureKind textureKind = _fontAtlas.TextureKind; float gx = 0; float gy = 0; int baseY = (int)Math.Round(top); float acc_x = 0; float acc_y = 0; int lineHeight = (int)_font.LineSpacingInPixels; //temp PixelBlender32 prevPxBlender = _painter.DestBitmapBlender.OutputPixelBlender; //save _painter.DestBitmapBlender.OutputPixelBlender = _maskPixelBlenderPerCompo; //change to new blender bool fillGlyphByGlyph = true; if (fillGlyphByGlyph) { //test... //fill glyph-by-glyh var aaTech = this.AntialiasTech; int seqLen = glyphPlanSeq.Count; for (int i = 0; i < seqLen; ++i) { UnscaledGlyphPlan unscaledGlyphPlan = glyphPlanSeq[i]; TextureGlyphMapData glyphData; if (!_fontAtlas.TryGetGlyphMapData(unscaledGlyphPlan.glyphIndex, out glyphData)) { //if no glyph data, we should render a missing glyph *** continue; } //-------------------------------------- //TODO: review precise height in float //-------------------------------------- int srcX, srcY, srcW, srcH; glyphData.GetRect(out srcX, out srcY, out srcW, out srcH); float ngx = acc_x + (float)Math.Round(unscaledGlyphPlan.OffsetX * scale); float ngy = acc_y + (float)Math.Round(unscaledGlyphPlan.OffsetY * scale); //NOTE: // -glyphData.TextureXOffset => restore to original pos // -glyphData.TextureYOffset => restore to original pos //-------------------------- //if (glyphData.TextureXOffset != 0) //{ //} gx = (float)(left + (ngx - glyphData.TextureXOffset)); //ideal x gy = (float)(top + (ngy + glyphData.TextureYOffset - srcH + lineHeight)); acc_x += (float)Math.Round(unscaledGlyphPlan.AdvanceX * scale); gy = (float)Math.Floor(gy);// + lineHeight; //clear with solid black color //_maskBufferPainter.Clear(Color.Black); //clear mask buffer at specific pos _maskBufferPainter.FillRect(gx - 1, gy - 1, srcW + 2, srcH + 2, Color.Black); //draw 'stencil' glyph on mask-buffer _maskBufferPainter.DrawImage(_fontBmp, gx, gy, srcX, _fontBmp.Height - (srcY + srcH), srcW, srcH); switch (aaTech) { default: { //select component to render this need to render 3 times for lcd technique //1. B _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.B; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.B; _painter.FillRect(gx + 1, gy, srcW, srcH); //2. G _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.G; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.G; _painter.FillRect(gx + 1, gy, srcW, srcH); //3. R _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.R; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.R; _painter.FillRect(gx + 1, gy, srcW, srcH); } break; case AntialiasTechnique.GreyscaleStencil: { //fill once //we choose greeh channel (middle) _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.G; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.EnableAll; _painter.FillRect(gx + 1, gy, srcW, srcH); } break; } } } else { //clear entire line _maskBufferPainter.FillRect(gx - 1, gy - 1, _maskBufferPainter.Width - gx + 2, lineHeight + 2, Color.Black); bool isFirst = true; int startX = 0, startY = 0; float lenW = 0; float lenH = 0; int seqLen = glyphPlanSeq.Count; for (int i = 0; i < seqLen; ++i) { UnscaledGlyphPlan glyph = glyphPlanSeq[i]; TextureGlyphMapData glyphData; if (!_fontAtlas.TryGetGlyphMapData(glyph.glyphIndex, out glyphData)) { //if no glyph data, we should render a missing glyph *** continue; } //-------------------------------------- //TODO: review precise height in float //-------------------------------------- int srcX, srcY, srcW, srcH; glyphData.GetRect(out srcX, out srcY, out srcW, out srcH); float ngx = acc_x + (float)Math.Round(glyph.OffsetX * scale); float ngy = acc_y + (float)Math.Round(glyph.OffsetY * scale); //NOTE: // -glyphData.TextureXOffset => restore to original pos // -glyphData.TextureYOffset => restore to original pos //-------------------------- gx = (float)(left + (ngx - glyphData.TextureXOffset)); //ideal x gy = (float)(top + (ngy - glyphData.TextureYOffset - srcH + lineHeight)); acc_x += (float)Math.Round(glyph.AdvanceX * scale); gy = (float)Math.Floor(gy) + lineHeight; if (isFirst) { startX = (int)gx; startY = (int)gy; isFirst = false; } _maskBufferPainter.DrawImage(_fontBmp, gx + 1, gy, srcX, _fontBmp.Height - (srcY), srcW + 1, srcH); lenW = gx + srcW; if (srcH > lenH) { lenH = srcH; } } //-------------------------- //fill color on 'stencil' mask { //select component to render this need to render 3 times for lcd technique //1. B _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.B; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.B; _painter.FillRect(startX + 1, startY, lenW, lenH); //2. G _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.G; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.G; _painter.FillRect(startX + 1, startY, lenW, lenH); //3. R _maskPixelBlenderPerCompo.SelectedMaskComponent = PixelBlenderColorComponent.R; _maskPixelBlenderPerCompo.EnableOutputColorComponent = EnableOutputColorComponent.R; _painter.FillRect(startX + 1, startY, lenW, lenH); } } // _painter.DestBitmapBlender.OutputPixelBlender = prevPxBlender;//restore back }
private void button1_Click(object sender, EventArgs e) { selectedTextPrinter = _devVxsTextPrinter; selectedTextPrinter.Typeface = _basicOptions.Typeface; selectedTextPrinter.FontSizeInPoints = _basicOptions.FontSizeInPoints; selectedTextPrinter.ScriptLang = _basicOptions.ScriptLang; selectedTextPrinter.PositionTechnique = _basicOptions.PositionTech; selectedTextPrinter.HintTechnique = HintTechnique.None; selectedTextPrinter.EnableLigature = true; _devVxsTextPrinter.UpdateGlyphLayoutSettings(); //------- var editableTextBlockLayoutEngine = new EditableTextBlockLayoutEngine(); editableTextBlockLayoutEngine.DefaultTypeface = _basicOptions.Typeface; editableTextBlockLayoutEngine.FontSizeInPts = _basicOptions.FontSizeInPoints; editableTextBlockLayoutEngine.LoadText("ABCD\r\n EFGH!"); editableTextBlockLayoutEngine.DoLayout(); //then we render the output to the screen //see UpdateRenderOutput() code //clear previous draw //---------------- //------------- painter.Clear(PixelFarm.Drawing.Color.White); painter.UseSubPixelRendering = false; painter.FillColor = PixelFarm.Drawing.Color.Black; _devVxsTextPrinter.TargetCanvasPainter = painter; List <EditableTextLine> textlines = editableTextBlockLayoutEngine.UnsafeGetEditableTextLine(); //render eachline with painter int lineCount = textlines.Count; float x = 0; int y = 200; int lineSpacing = (int)_devVxsTextPrinter.FontLineSpacingPx; for (int i = 0; i < lineCount; ++i) { EditableTextLine line = textlines[i]; List <IRun> runs = line.UnsageGetTextRunList(); int runCount = runs.Count; for (int r = 0; r < runCount; ++r) { IRun run = runs[r]; TextRun textRun = run as TextRun; if (textRun == null) { continue; } // GlyphPlanSequence seq = textRun.GetGlyphPlanSeq(); _devVxsTextPrinter.DrawFromGlyphPlans( seq.UnsafeGetInteralGlyphPlanList(), seq.startAt, seq.len, x, y); x += run.Width; y -= lineSpacing; //next line? } x = 0; //reset at newline } //---------- //use this util to copy image from Agg actual image to System.Drawing.Bitmap PixelFarm.Agg.Imaging.BitmapHelper.CopyToGdiPlusBitmapSameSize(painter.Graphics.DestActualImage, winBmp); //---------------- //copy from Agg's memory buffer to gdi //PixelFarm.Agg.Imaging.BitmapHelper.CopyToGdiPlusBitmapSameSize(destImg, winBmp); g.Clear(Color.White); g.DrawImage(winBmp, new Point(10, 0)); }
public override void DrawFromGlyphPlans(GlyphPlanSequence glyphPlanList, int startAt, int len, float x, float y) { throw new System.NotImplementedException(); }
public void DrawString(Painter p, char[] buffer, int startAt, int len, double x, double y) { //if (_stencilBmp == null) //{ // //create a stencil bmp // _stencilBmp = new ActualBitmap(p.Width, p.Height); // _stencilBlender = new PixelProcessing.SubBitmapBlender(_stencilBmp, new PixelProcessing.PixelBlenderBGRA()); // _backPainter = AggPainter.Create(_stencilBmp); // //------ //} int j = buffer.Length; //create temp buffer span that describe the part of a whole char buffer TextBufferSpan textBufferSpan = new TextBufferSpan(buffer, startAt, len); //ask text service to parse user input char buffer and create a glyph-plan-sequence (list of glyph-plan) //with specific request font GlyphPlanSequence glyphPlanSeq = _textServices.CreateGlyphPlanSeq(ref textBufferSpan, _font); Typeface typeface = _textServices.ResolveTypeface(_font); float scale = typeface.CalculateScaleToPixelFromPointSize(_font.SizeInPoints); int recommendLineSpacing = (int)_font.LineSpacingInPixels; //-------------------------- //TODO: //if (x,y) is left top //we need to adjust y again y -= _font.LineSpacingInPixels; // float scaleFromTexture = _finalTextureScale; TextureKind textureKind = _fontAtlas.TextureKind; float g_x = 0; float g_y = 0; int baseY = (int)Math.Round(y); //------------------------------------- //load texture //_pcx.LoadTexture1(_glBmp); //------------------------------------- float acc_x = 0; float acc_y = 0; p.DrawImage(_fontBmp, 100, 100); int seqLen = glyphPlanSeq.Count; for (int i = 0; i < seqLen; ++i) { UnscaledGlyphPlan glyph = glyphPlanSeq[i]; TextureGlyphMapData glyphData; if (!_fontAtlas.TryGetGlyphMapData(glyph.glyphIndex, out glyphData)) { //if no glyph data, we should render a missing glyph *** continue; } //-------------------------------------- //TODO: review precise height in float //-------------------------------------- int srcX, srcY, srcW, srcH; glyphData.GetRect(out srcX, out srcY, out srcW, out srcH); float ngx = acc_x + (float)Math.Round(glyph.OffsetX * scale); float ngy = acc_y + (float)Math.Round(glyph.OffsetY * scale); //NOTE: // -glyphData.TextureXOffset => restore to original pos // -glyphData.TextureYOffset => restore to original pos //-------------------------- g_x = (float)(x + (ngx - glyphData.TextureXOffset) * scaleFromTexture); //ideal x g_y = (float)(y + (ngy - glyphData.TextureYOffset + srcH) * scaleFromTexture); acc_x += (float)Math.Round(glyph.AdvanceX * scale); //g_x = (float)Math.Round(g_x); g_y = (float)Math.Floor(g_y); //p.RenderQuality = RenderQuality.Fast; //*** the atlas is inverted so... //p.DrawImage(_fontBmp, g_x, g_y, srcX, _fontBmp.Height - (srcY), srcW, srcH); //p.DrawImage(_fontBmp, g_x, g_y); //1. draw to back buffer //_backPainter.DrawImage(_fontBmp, g_x, g_y, srcX, _fontBmp.Height - (srcY), srcW, srcH); //2. then copy content to this //p.DrawImage(_stencilBmp, 100, 100); p.DrawImage(_fontBmp, g_x, g_y, srcX, _fontBmp.Height - (srcY + srcH), srcW, srcH); switch (textureKind) { default: break; case TextureKind.StencilLcdEffect: { } break; } //copy some part from the bitmap //switch (textureKind) //{ // case TextureKind.Msdf: // _pcx.DrawSubImageWithMsdf(_glBmp, // ref srcRect, // g_x, // g_y, // scaleFromTexture); // break; // case TextureKind.StencilGreyScale: // //stencil gray scale with fill-color // _pcx.DrawGlyphImageWithStecil(_glBmp, // ref srcRect, // g_x, // g_y, // scaleFromTexture); // break; // case TextureKind.Bitmap: // _pcx.DrawSubImage(_glBmp, // ref srcRect, // g_x, // g_y, // scaleFromTexture); // break; // case TextureKind.StencilLcdEffect: // _pcx.WriteVboToList( // _vboBufferList, // _indexList, // ref srcRect, // g_x, // g_y, // scaleFromTexture); // break; //} } //------- //we create vbo first //then render }