public static async Task <ExportResult> ExportSvgAsync( ExportStyle style, InstalledFont selectedFont, FontVariant selectedVariant, Character selectedChar, CanvasTextLayoutAnalysis analysis, CanvasTypography typography) { try { string name = GetFileName(selectedFont, selectedVariant, selectedChar, "svg"); if (await PickFileAsync(name, "SVG", new[] { ".svg" }) is StorageFile file) { CachedFileManager.DeferUpdates(file); CanvasDevice device = Utils.CanvasDevice; Color textColor = style == ExportStyle.Black ? Colors.Black : Colors.White; // If COLR format (e.g. Segoe UI Emoji), we have special export path. if (style == ExportStyle.ColorGlyph && analysis.HasColorGlyphs && !analysis.GlyphFormats.Contains(GlyphImageFormat.Svg)) { NativeInterop interop = Utils.GetInterop(); List <string> paths = new List <string>(); Rect bounds = Rect.Empty; foreach (var thing in analysis.Indicies) { var path = interop.GetPathDatas(selectedVariant.FontFace, thing.ToArray()).First(); paths.Add(path.Path); if (!path.Bounds.IsEmpty) { var left = Math.Min(bounds.Left, path.Bounds.Left); var top = Math.Min(bounds.Top, path.Bounds.Top); var right = Math.Max(bounds.Right, path.Bounds.Right); var bottom = Math.Max(bounds.Bottom, path.Bounds.Bottom); bounds = new Rect( left, top, right - left, bottom - top); } } using (CanvasSvgDocument document = Utils.GenerateSvgDocument(device, bounds, paths, analysis.Colors, invertBounds: false)) { await Utils.WriteSvgAsync(document, file); } return(new ExportResult(true, file)); } var data = GetGeometry(1024, selectedVariant, selectedChar, analysis, typography); async Task SaveMonochromeAsync() { using CanvasSvgDocument document = Utils.GenerateSvgDocument(device, data.Bounds, data.Path, textColor); await Utils.WriteSvgAsync(document, file); } // If the font uses SVG glyphs, we can extract the raw SVG from the font file if (analysis.GlyphFormats.Contains(GlyphImageFormat.Svg)) { string str = null; IBuffer b = GetGlyphBuffer(selectedVariant.FontFace, selectedChar.UnicodeIndex, GlyphImageFormat.Svg); if (b.Length > 2 && b.GetByte(0) == 31 && b.GetByte(1) == 139) { using var stream = b.AsStream(); using var gzip = new GZipStream(stream, CompressionMode.Decompress); using var reader = new StreamReader(gzip); str = reader.ReadToEnd(); } else { using var dataReader = DataReader.FromBuffer(b); dataReader.UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf8; str = dataReader.ReadString(b.Length); } if (str.StartsWith("<?xml")) { str = str.Remove(0, str.IndexOf(">") + 1); } str = str.TrimStart(); try { using (CanvasSvgDocument document = CanvasSvgDocument.LoadFromXml(Utils.CanvasDevice, str)) { // We need to transform the SVG to fit within the default document bounds, as characters // are based *above* the base origin of (0,0) as (0,0) is the Baseline (bottom left) position for a character, // so by default a will appear out of bounds of the default SVG viewport (towards top left). //if (!document.Root.IsAttributeSpecified("viewBox")) // Specified viewbox requires baseline transform? { // We'll regroup all the elements inside a "g" / group tag, // and apply a transform to the "g" tag to try and put in // in the correct place. There's probably a more accurate way // to do this by directly setting the root viewBox, if anyone // can find the correct calculation... List <ICanvasSvgElement> elements = new List <ICanvasSvgElement>(); double minTop = 0; double minLeft = double.MaxValue; double maxWidth = double.MinValue; double maxHeight = double.MinValue; void ProcessChildren(CanvasSvgNamedElement root) { CanvasSvgNamedElement ele = root.FirstChild as CanvasSvgNamedElement; while (true) { CanvasSvgNamedElement next = root.GetNextSibling(ele) as CanvasSvgNamedElement; if (ele.Tag == "g") { ProcessChildren(ele); } else if (ele.Tag == "path") { // Create a XAML geometry to try and find the bounds of each character // Probably more efficient to do in Win2D, but far less code to do with XAML. Geometry gm = XamlBindingHelper.ConvertValue(typeof(Geometry), ele.GetStringAttribute("d")) as Geometry; minTop = Math.Min(minTop, gm.Bounds.Top); minLeft = Math.Min(minLeft, gm.Bounds.Left); maxWidth = Math.Max(maxWidth, gm.Bounds.Width); maxHeight = Math.Max(maxHeight, gm.Bounds.Height); } ele = next; if (ele == null) { break; } } } ProcessChildren(document.Root); double top = minTop < 0 ? minTop : 0; double left = minLeft; document.Root.SetRectangleAttribute("viewBox", new Rect(left, top, data.Bounds.Width, data.Bounds.Height)); } await Utils.WriteSvgAsync(document, file); } } catch { // Certain fonts seem to have their SVG glyphs encoded with... I don't even know what encoding. // for example: https://github.com/adobe-fonts/emojione-color // In these cases, fallback to monochrome black await SaveMonochromeAsync(); } } else { await SaveMonochromeAsync(); } await CachedFileManager.CompleteUpdatesAsync(file); return(new ExportResult(true, file)); } } catch (Exception ex) { await SimpleIoc.Default.GetInstance <IDialogService>() .ShowMessageBox(ex.Message, Localization.Get("SaveImageError")); } return(new ExportResult(false, null)); }
public static async Task ExportSvgAsync( ExportStyle style, InstalledFont selectedFont, FontVariant selectedVariant, Character selectedChar, CanvasTypography typography) { try { string name = GetFileName(selectedFont, selectedVariant, selectedChar, "svg"); if (await PickFileAsync(name, "SVG", new[] { ".svg" }) is StorageFile file) { CachedFileManager.DeferUpdates(file); var device = Utils.CanvasDevice; var textColor = style == ExportStyle.Black ? Colors.Black : Colors.White; /* SVG Exports render at fixed size - but a) they're vectors, and b) they're * inside an auto-scaling viewport. So rendersize is *largely* pointless */ float canvasH = 1024f, canvasW = 1024f, fontSize = 1024f; using (CanvasTextLayout layout = new CanvasTextLayout(device, $"{selectedChar.Char}", new CanvasTextFormat { FontSize = fontSize, FontFamily = selectedVariant.Source, FontStretch = selectedVariant.FontFace.Stretch, FontWeight = selectedVariant.FontFace.Weight, FontStyle = selectedVariant.FontFace.Style, HorizontalAlignment = CanvasHorizontalAlignment.Center }, canvasW, canvasH)) { layout.SetTypography(0, 1, typography); using (CanvasGeometry temp = CanvasGeometry.CreateText(layout)) { var b = temp.ComputeBounds(); double scale = Math.Min(1, Math.Min(canvasW / b.Width, canvasH / b.Height)); Matrix3x2 transform = Matrix3x2.CreateTranslation(new Vector2((float)-b.Left, (float)-b.Top)) * Matrix3x2.CreateScale(new Vector2((float)scale)); using (CanvasGeometry geom = temp.Transform(transform)) { /* * Unfortunately this only constructs a monochrome path, if we want color * Win2D does not yet expose the neccessary API's to get the individual glyph * layers that make up a colour glyph. * * We'll need to handle this in C++/CX if we want to do this at some point. */ SVGPathReciever rc = new SVGPathReciever(); geom.SendPathTo(rc); Rect bounds = geom.ComputeBounds(); using (CanvasSvgDocument document = Utils.GenerateSvgDocument(device, bounds.Width, bounds.Height, rc)) { ((CanvasSvgNamedElement)document.Root.FirstChild).SetColorAttribute("fill", textColor); await Utils.WriteSvgAsync(document, file); } } } } await CachedFileManager.CompleteUpdatesAsync(file); } } catch (Exception ex) { await SimpleIoc.Default.GetInstance <IDialogService>() .ShowMessageBox(ex.Message, Localization.Get("SaveImageError")); } }