From 021f6bab14bb1aefa5ba59c652f9a76e9f5dfd21 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 5 Apr 2026 12:58:35 +1000 Subject: [PATCH 01/21] Migrate to latest docfx --- docfx.json | 71 +++----- templates/modern/layout/_master.tmpl | 172 ++++++++++++++++++ .../public}/android-chrome-192x192.png | Bin .../public}/android-chrome-512x512.png | Bin .../{ => modern/public}/apple-touch-icon.png | Bin .../{ => modern/public}/favicon-16x16.png | Bin .../{ => modern/public}/favicon-32x32.png | Bin templates/{ => modern/public}/favicon.ico | Bin templates/{ => modern/public}/logo.svg | 0 templates/modern/public/main.css | 121 ++++++++++++ templates/modern/public/main.js | 1 + templates_bak/android-chrome-192x192.png | Bin 0 -> 23547 bytes templates_bak/android-chrome-512x512.png | Bin 0 -> 148113 bytes templates_bak/apple-touch-icon.png | Bin 0 -> 21220 bytes templates_bak/favicon-16x16.png | Bin 0 -> 653 bytes templates_bak/favicon-32x32.png | Bin 0 -> 1562 bytes templates_bak/favicon.ico | Bin 0 -> 15406 bytes .../fonts/glyphicons-halflings-regular.eot | Bin .../fonts/glyphicons-halflings-regular.svg | 0 .../fonts/glyphicons-halflings-regular.ttf | Bin .../fonts/glyphicons-halflings-regular.woff | Bin .../fonts/glyphicons-halflings-regular.woff2 | Bin templates_bak/logo.svg | 1 + {templates => templates_bak}/styles/main.css | 0 {templates => templates_bak}/styles/main.js | 0 .../styles/vs2015.css | 0 26 files changed, 317 insertions(+), 49 deletions(-) create mode 100644 templates/modern/layout/_master.tmpl rename templates/{ => modern/public}/android-chrome-192x192.png (100%) rename templates/{ => modern/public}/android-chrome-512x512.png (100%) rename templates/{ => modern/public}/apple-touch-icon.png (100%) rename templates/{ => modern/public}/favicon-16x16.png (100%) rename templates/{ => modern/public}/favicon-32x32.png (100%) rename templates/{ => modern/public}/favicon.ico (100%) rename templates/{ => modern/public}/logo.svg (100%) create mode 100644 templates/modern/public/main.css create mode 100644 templates/modern/public/main.js create mode 100644 templates_bak/android-chrome-192x192.png create mode 100644 templates_bak/android-chrome-512x512.png create mode 100644 templates_bak/apple-touch-icon.png create mode 100644 templates_bak/favicon-16x16.png create mode 100644 templates_bak/favicon-32x32.png create mode 100644 templates_bak/favicon.ico rename {templates => templates_bak}/fonts/glyphicons-halflings-regular.eot (100%) rename {templates => templates_bak}/fonts/glyphicons-halflings-regular.svg (100%) rename {templates => templates_bak}/fonts/glyphicons-halflings-regular.ttf (100%) rename {templates => templates_bak}/fonts/glyphicons-halflings-regular.woff (100%) rename {templates => templates_bak}/fonts/glyphicons-halflings-regular.woff2 (100%) create mode 100644 templates_bak/logo.svg rename {templates => templates_bak}/styles/main.css (100%) rename {templates => templates_bak}/styles/main.js (100%) rename {templates => templates_bak}/styles/vs2015.css (100%) diff --git a/docfx.json b/docfx.json index 04fb1a54a..caaea9535 100644 --- a/docfx.json +++ b/docfx.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", "metadata": [ { "src": [ @@ -8,7 +9,7 @@ ] } ], - "dest": "api/ImageSharp", + "output": "api/ImageSharp", "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { @@ -23,7 +24,7 @@ ] } ], - "dest": "api/ImageSharp.Drawing", + "output": "api/ImageSharp.Drawing", "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { @@ -38,11 +39,11 @@ ] } ], - "dest": "api/Fonts", + "output": "api/Fonts", "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { - + } }, { @@ -53,7 +54,7 @@ ] } ], - "dest": "api/ImageSharp.Web", + "output": "api/ImageSharp.Web", "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { @@ -68,7 +69,7 @@ ] } ], - "dest": "api/ImageSharp.Web.Providers.Azure", + "output": "api/ImageSharp.Web.Providers.Azure", "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { @@ -83,7 +84,7 @@ ] } ], - "dest": "api/ImageSharp.Web.Providers.AWS", + "output": "api/ImageSharp.Web.Providers.AWS", "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { @@ -95,25 +96,14 @@ "xref": [ "xrefmap.yml" ], - "xrefService": [ - "https://xref.docs.microsoft.com/query?uid={uid}" - ], "content": [ { "files": [ - "api/**.yml", - "api/index.md" - ] - }, - { - "files": [ - "articles/**.md", - "articles/**/toc.yml", - "toc.yml", - "*.md" + "**/*.{md,yml}" ], "exclude": [ - "README.md" + "README.md", + "_site/**" ] } ], @@ -126,37 +116,20 @@ ] } ], - "overwrite": [ - { - "files": [ - "apidoc/**.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } + "output": "_site", + "template": [ + "default", + "modern", + "templates/modern" ], - "dest": "_site", - "globalMetadataFiles": [], - "fileMetadataFiles": [], - "fileMetadata": {}, "globalMetadata": { - "_enableSearch": true, "_gitContribute": { "branch": "main" - } - }, - "template": [ - "statictoc", - "templates" - ], - "postProcessors": [ - "ExtractSearchIndex" - ], - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false, - "disableGitFeatures": false + }, + "_enableSearch": true, + "pdf": true, + "_appLogoPath": "public/logo.svg", + "_appFaviconPath": "public/favicon.ico" + } } } \ No newline at end of file diff --git a/templates/modern/layout/_master.tmpl b/templates/modern/layout/_master.tmpl new file mode 100644 index 000000000..df7ac5d0f --- /dev/null +++ b/templates/modern/layout/_master.tmpl @@ -0,0 +1,172 @@ +{{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} +{{!include(/^public/.*/)}} +{{!include(favicon.ico)}} +{{!include(logo.svg)}} + + + + + {{#redirect_url}} + + {{/redirect_url}} + {{^redirect_url}} + {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} + + + {{#_description}}{{/_description}} + {{#description}}{{/description}} + + + + + + {{#_noindex}}{{/_noindex}} + {{#_enableSearch}}{{/_enableSearch}} + {{#_disableNewTab}}{{/_disableNewTab}} + {{#_disableTocFilter}}{{/_disableTocFilter}} + {{#docurl}}{{/docurl}} + + + + + + + + + + + + + + + + + + {{#_googleAnalyticsTagId}} + + + {{/_googleAnalyticsTagId}} + {{/redirect_url}} + + + {{^redirect_url}} + +
+ {{^_disableNavbar}} + + {{/_disableNavbar}} +
+ +
+ {{^_disableToc}} +
+
+
+
Table of Contents
+ +
+
+ +
+
+
+ {{/_disableToc}} + +
+
+ {{^_disableToc}} + + {{/_disableToc}} + + {{^_disableBreadcrumb}} + + {{/_disableBreadcrumb}} +
+ +
+ {{!body}} +
+ + {{^_disableContribution}} +
+ {{#sourceurl}} + {{__global.improveThisDoc}} + {{/sourceurl}} + {{^sourceurl}}{{#docurl}} + {{__global.improveThisDoc}} + {{/docurl}}{{/sourceurl}} +
+ {{/_disableContribution}} + + {{^_disableNextArticle}} + + {{/_disableNextArticle}} + +
+ + {{^_disableAffix}} +
+ +
+ {{/_disableAffix}} +
+ + {{#_enableSearch}} +
+ {{/_enableSearch}} + + + + {{/redirect_url}} + diff --git a/templates/android-chrome-192x192.png b/templates/modern/public/android-chrome-192x192.png similarity index 100% rename from templates/android-chrome-192x192.png rename to templates/modern/public/android-chrome-192x192.png diff --git a/templates/android-chrome-512x512.png b/templates/modern/public/android-chrome-512x512.png similarity index 100% rename from templates/android-chrome-512x512.png rename to templates/modern/public/android-chrome-512x512.png diff --git a/templates/apple-touch-icon.png b/templates/modern/public/apple-touch-icon.png similarity index 100% rename from templates/apple-touch-icon.png rename to templates/modern/public/apple-touch-icon.png diff --git a/templates/favicon-16x16.png b/templates/modern/public/favicon-16x16.png similarity index 100% rename from templates/favicon-16x16.png rename to templates/modern/public/favicon-16x16.png diff --git a/templates/favicon-32x32.png b/templates/modern/public/favicon-32x32.png similarity index 100% rename from templates/favicon-32x32.png rename to templates/modern/public/favicon-32x32.png diff --git a/templates/favicon.ico b/templates/modern/public/favicon.ico similarity index 100% rename from templates/favicon.ico rename to templates/modern/public/favicon.ico diff --git a/templates/logo.svg b/templates/modern/public/logo.svg similarity index 100% rename from templates/logo.svg rename to templates/modern/public/logo.svg diff --git a/templates/modern/public/main.css b/templates/modern/public/main.css new file mode 100644 index 000000000..fa734f098 --- /dev/null +++ b/templates/modern/public/main.css @@ -0,0 +1,121 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swaps"); + +/* ################## + * ### TYPOGRAPHY ### + * ################## + */ + +body{ + font-family: "Inter", sans-serif; +} + +h1, +h2, +h3 { + font-weight: 800; + text-decoration: none; + line-height: 1.15; + word-wrap: none !important; + word-break: normal !important; +} + +h1 { + font-size: 2.5rem; + margin: 1rem 0; +} + +h1[data-uid] { + text-transform: none; + font-size: 2rem; +} + +h2 { + font-size: 2rem; + margin: 1.34rem 0; +} + +h3 { + font-size: 1.5rem; + margin: 1.245rem 0; +} + +h4, +h5 { + font-weight: 600; +} + +a { + color: #e35052; + text-decoration: none; +} + +a:hover, +a:hover .a, +a:focus, +a:focus .a { + color: #0a58ca; + text-decoration: underline; +} + +.a { + text-transform: uppercase; +} + +h1 a, +h2 a, +h3 a { + color: inherit; +} + +.btn-primary { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-primary:hover, +.btn-primary:focus { + color: #fff; + background-color: #0b5ed7; + border-color: #0a58ca; +} + +.btn-primary:active, +.btn-primary:active:focus { + color: #fff; + background-color: #0a58ca; + border-color: #0a53be; +} + +.btn-primary:active:focus { + box-shadow: 0 0 0 0.25rem rgb(49 132 253 / 50%); +} + +/* ####################### + * ### MAIN NAVIGATION ### + * ####################### + */ + +#logo { + width: 160px; + height: 50px; + margin-right: 1rem; +} + +[data-bs-theme="dark"] #logo { + fill: #fff; +} + +body>header, body[data-layout=landing]>header { + box-shadow: 0 0 8px rgb(0 0 0 / 12%); + border-bottom: 0; +} + +[data-bs-theme="dark"] body>header, html[data-bs-theme="dark"] body[data-layout=landing]>header { + box-shadow: 0 0 8px rgb(0 0 0 / 48%); + border-bottom: 0; +} + +header .navbar { + text-transform: uppercase; +} diff --git a/templates/modern/public/main.js b/templates/modern/public/main.js new file mode 100644 index 000000000..b1c6ea436 --- /dev/null +++ b/templates/modern/public/main.js @@ -0,0 +1 @@ +export default {} diff --git a/templates_bak/android-chrome-192x192.png b/templates_bak/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..39fcc99f0cb80af109e77817379d6a9dbdf626db GIT binary patch literal 23547 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 T6Tw4X00000NkvXXu0mjfj~65a diff --git a/templates_bak/favicon-16x16.png b/templates_bak/favicon-16x16.png deleted file mode 100644 index ffd1dec3c451c3768ef3dd346123a3704a8b2a6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 653 zcmV;80&@L{P)22CVE*earI-$rE?DNeL<6@`$66Ct%w2+Sx7 zsfloebYT{i^A>I*kpc}f$|h?v)OX{)JN{rlV-!s##fH zRt$y(aTug52pFsc0t>7-1xMl@XEL@0owKBWMqWXmD5+$9R3B94xwv-USFFu<9+ZX2 zDoW%XZ4%-ZRxY~B{J$V@I7+?VBmE3Hu*C8k)4!DP06oq>oMa^d&23Z&HlI0$O?A~e z9uNpnz*MXcA6pZ~N`^#~*c8t{lw?T2^#wuBh9ZQ9+Awnb0_NYRGPVRX>tNx-_hkT+ zgG~>7;bj0ND7zlT+*AtFy-94pQVS?$Q~>yz7{Pc$_X-fUsmHfJ$z%nztin(kcHL~o zvpsdFh+o7+`ypLB!MBh(p=Uwer0%RQ?bY11wLPE({zjsINRzmrXB9`K627BGBtQh31 nx?sj;iX4esGv8td(LehOgVzu}3A-XH00000NkvXXu0mjf7b+YH diff --git a/templates_bak/favicon-32x32.png b/templates_bak/favicon-32x32.png deleted file mode 100644 index 77c3fa2d30fad65b152bfbfb159ce646f2d63eaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1562 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/templates_bak/fonts/glyphicons-halflings-regular.ttf b/templates_bak/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc609ab6f21774de0cb7e01360095584f65b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45404 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 G`u_lkUM?>H diff --git a/templates_bak/fonts/glyphicons-halflings-regular.woff2 b/templates_bak/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54c3751a6d9adb44c8e3a45ba5a73b77f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18028 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 diff --git a/templates_bak/logo.svg b/templates_bak/logo.svg deleted file mode 100644 index 749c080ea..000000000 --- a/templates_bak/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/templates_bak/styles/main.css b/templates_bak/styles/main.css deleted file mode 100644 index 4eb800905..000000000 --- a/templates_bak/styles/main.css +++ /dev/null @@ -1,936 +0,0 @@ -@import url("https://use.fontawesome.com/releases/v5.15.1/css/all.css"); -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swaps"); -@import url("https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/monokai_sublime.min.css"); -/* ############# - * ### RESET ### - * ############# - */ - -details, -main, -menu { - display: block; -} - -summary { - display: list-item; -} - -progress { - vertical-align: baseline; -} - -audio:not([controls]) { - display: none; - height: 0; -} - -template { - display: none; -} - -a { - -webkit-text-decoration-skip: objects; -} - -a:active, -a:hover { - outline: 0; -} - -abbr[title] { - border-bottom: none; - text-decoration: underline; - text-decoration: underline dotted; - cursor: help; -} - -b, -strong { - font-weight: inherit; - font-weight: 700; -} - -small { - font-size: 80%; -} - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.25rem; -} - -sub { - bottom: -0.5rem; -} - -svg:not(:root) { - overflow: hidden; -} - -button, -input, -optgroup, -select, -textarea { - color: inherit; - font: inherit; - margin: 0; -} - -button { - overflow: visible; -} - -button, -select { - text-transform: none; -} - -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; - cursor: pointer; -} - -button::-moz-focus-inner, -input::-moz-focus-inner { - border-style: none; - padding: 0; -} - -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring, -button:-moz-focusring { - outline: 1px dotted ButtonText; -} - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -[type="search"] { - -webkit-appearance: textfield; - outline-offset: -2px; -} - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; -} - -html { - -webkit-overflow-scrolling: touch; - color: #222; - font-size: 1rem; - box-sizing: border-box; -} - -*, -:after, -:before { - box-sizing: inherit; -} - -[tabindex="-1"]:focus { - outline: none; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - text-rendering: optimizeLegibility; - page-break-after: avoid; -} - -h1 { - font-size: 2rem; - margin: 1.34rem 0; -} - -h2 { - font-size: 1.5rem; - margin: 1.245rem 0; -} - -h3 { - font-size: 1.17rem; - margin: 1.17rem 0; -} - -h4 { - font-size: 1rem; - margin: 1.33rem 0; -} - -h5 { - font-size: 0.83rem; - margin: 1.386rem 0; -} - -h6 { - font-size: 0.67rem; - margin: 1.561rem 0; -} - -::selection { - background: #b3d4fc; - text-shadow: none; -} - -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid silver; - margin: 1rem 0; - padding: 0; -} - -/* Works on Firefox */ -body * { - scrollbar-width: thin; - scrollbar-color: #fff #f5bebf; -} - -/* Works on Chrome, Edge, and Safari */ -body * ::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -body * ::-webkit-scrollbar-track { - background: #fff; -} - -body * ::-webkit-scrollbar-thumb { - background-color: #f5bebf; - border: 3px solid #fff; -} - -/* ################## - * ### TYPOGRAPHY ### - * ################## - */ - -body { - margin: 0; - font-family: "Inter", sans-serif; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - background-color: #fff; - -webkit-text-size-adjust: 100%; - -webkit-tap-highlight-color: transparent; -} - -@media (min-width: 1200px) { - .container { - width: 95%; - max-width: 1400px; - } -} - -h1, -h2, -h3 { - font-weight: 800; - text-decoration: none; - line-height: 1.15; - word-wrap: none !important; - word-break: normal !important; -} - -h1 { - font-size: 2.5rem; - margin: 1rem 0; -} - -h1[data-uid] { - text-transform: none; - font-size: 2rem; -} - -h2 { - font-size: 2rem; - margin: 1.34rem 0; -} - -h3 { - font-size: 1.5rem; - margin: 1.245rem 0; -} - -h4, -h5 { - font-weight: 600; -} - -a, -.a { - color: #e35052; - text-decoration: none; -} - -a:hover, -a:hover .a, -a:focus, -a:focus .a { - color: #0a58ca; - text-decoration: underline; -} - -.a { - text-transform: uppercase; -} - -h1 a, -h2 a, -h3 a { - color: inherit; -} - -.btn-primary { - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} - -.btn-primary:hover, -.btn-primary:focus { - color: #fff; - background-color: #0b5ed7; - border-color: #0a58ca; -} - -.btn-primary:active, -.btn-primary:active:focus { - color: #fff; - background-color: #0a58ca; - border-color: #0a53be; -} - -.btn-primary:active:focus { - box-shadow: 0 0 0 0.25rem rgb(49 132 253 / 50%); -} - -/* ####################### - * ### MAIN NAVIGATION ### - * ####################### - */ - -#logo { - width: 160px; - height: 50px; - margin-right: 1rem; -} - -svg:hover path { - fill: initial; -} - -header .navbar { - text-transform: uppercase; -} - -.navbar-inverse { - background-color: #fff; - border-color: #fff; - box-shadow: 0 0 8px rgb(0 0 0 / 12%); - position: relative; - color: inherit; - z-index: 1; -} - -.navbar-default { - border: none; -} - -.navbar-inverse .navbar-nav > li > a, -.navbar-inverse .navbar-text { - color: inherit; -} - -.navbar-inverse .navbar-nav > li > a:focus, -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:focus, -.navbar-inverse .navbar-nav > .active > a:hover { - color: #e30183; - text-decoration: none; -} - -.navbar-form { - padding: 0; - margin: 8px 0; -} - -.navbar-inverse .navbar-toggle { - border-color: #fff; -} - -.navbar-inverse .navbar-toggle .icon-bar { - background-color: #212529; -} - -.navbar-inverse .navbar-toggle:focus, -.navbar-inverse .navbar-toggle:hover { - background-color: #fff; -} - -.navbar-inverse .navbar-collapse, -.navbar-inverse .navbar-form { - border: none; -} -/* Fix loss of drop shadow by overriding DocFX "static"*/ - -@media only screen and (max-width: 768px) { - header { - position: relative; - } -} - -.icon-bar { - transition: 0.4s; -} - -[aria-expanded="true"] .icon-bar:nth-of-type(2) { - /* Rotate first bar */ - transform: rotate(-45deg) translate(-4px, 5px); -} - -[aria-expanded="true"] .icon-bar:nth-of-type(3) { - /* Fade out the second bar */ - opacity: 0; -} - -[aria-expanded="true"] .icon-bar:nth-of-type(4) { - /* Rotate last bar */ - transform: rotate(45deg) translate(-4px, -5px); -} - -.collapse.in, -.collapsing { - text-align: unset; -} - -/* ####################### - * ### SIDE NAVIGATION ### - * ####################### - */ - -.sidefilter { - background-color: #fff; - top: 106px; - padding: 15px 0; - border: none; -} - -.toc-filter { - border-radius: 0; - background: #fff; - color: inherit; - padding: 0; - position: relative; - margin: 0; -} - -.toc-filter > input { - border: 1px solid #dcdcdc; - color: inherit; - font-size: 0.875rem; - height: 36px; - line-height: 1.8; - padding: 0 10px 0 20px; -} - -.toc-filter > .filter-icon { - top: 8px; -} - -.filter-icon::before { - content: "\f0b0" !important; - font-size: 0.75rem; -} - -#toc_filter_clear::before { - font-size: 0.75rem; -} - -.toc-filter > input:focus { - outline: #3b99fc solid 1px; -} - -.sidetoc { - background-color: #fff; - border: none; - top: 166px; - bottom: auto; - max-height: calc(100% - 166px); - overflow-x: auto !important; -} - -.sidetoc.shiftup { - bottom: auto; -} - -/* Blast the default styles out of the way so the side nav is usable*/ -.sidetoc * { - font-size: 13px !important; - overflow-x: visible !important; -} - -.sidetoc .toc { - width: fit-content; -} - -.toc { - margin: 0; - padding: 0; -} - -.toc ul { - font-size: inherit; - margin: 0; -} - -/* Prevent double scroll bar */ - -body .toc { - background-color: #fff; - overflow-x: initial; -} - -.toc .level1 > li { - font-weight: bold; - margin-top: 2px; - position: relative; - font-size: 15px; -} - -toc .nav > li > a { - font-weight: normal; - color: inherit; - padding: 5px 0 0 15px; - margin: 0; -} - -.toc .nav > li > .expand-stub::before, -.toc .nav > li.active > .expand-stub::before, -.toc .nav > li.filtered > .expand-stub::before { - /*DOCFX selectors far too specific!*/ - content: "\f054" !important; - position: absolute; - z-index: 1; - left: 15px; - top: 9px; - font-size: 0.75rem; - -webkit-transform: rotate(0deg); - transform: rotate(0deg); -} - -.level2 li > .expand-stub::before { - left: 0 !important; -} - -.toc .nav > li.active > .expand-stub::before { - color: #fff; -} - -.toc .nav > li.in > .expand-stub::before, -.toc .nav > li.in.active > .expand-stub::before, -.toc .nav > li > .expand-stub.in::before { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.toc a { - font-weight: normal; -} - -.toc .nav > li > a { - padding: 5px 5px 5px 18px; - margin: 0; -} - -.toc .nav .level2 a { - padding-left: 5px; -} - -.toc .nav > li > a:hover, -.toc .nav > li > a:focus { - color: #23527c; - text-decoration: underline; -} - -.toc .nav > li.in.active > a:hover, -.toc .nav > li.in.active > a:focus { - color: #fff; - background-color: #0b5ed7; - text-decoration: none; -} - -.toc .nav > li.active > a { - color: #fff; - background-color: #0d6efd; -} - -/* ################### - * ### API ARTICLE ### - * ################### - */ - -.article { - margin-top: 94px; -} - -article span.small.pull-right { - /* The styling for these is mental and causes odd offsets all over the place*/ - display: none; -} - -article { - line-height: 1.6; -} - -article h1 { - margin-top: 16px; -} - -article h4 { - border-bottom: none; -} - -.code-like, -code, -kbd, -pre, -samp { - -moz-osx-font-smoothing: auto; - -webkit-font-smoothing: auto; - font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", - "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, - sans-serif; -} - -pre { - word-break: unset; - word-wrap: unset; - overflow-x: auto; - padding: 0; - border: none; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.08); -} - -pre code { - word-wrap: normal; - white-space: pre; -} - -pre { - border-radius: 0.75rem; - position: relative; - z-index: 1; - overflow-x: none; -} - -pre::before, -pre::after, -pre code::before { - content: ""; - position: absolute; - z-index: 1; - pointer-events: none; - top: 1rem; - left: 0.75rem; - border-radius: 100%; - width: 0.75rem; - height: 0.75rem; -} - -pre:before { - background-color: #ef4444; -} - -pre::after { - left: 1.75rem; - background-color: #fbbf24; -} - -pre code::before { - left: 2.75rem; - background-color: #4ade80; -} - -pre code::after { - content: ""; - position: absolute; - z-index: 1; - pointer-events: none; - top: 2.5rem; - left: 0.75rem; - right: 0.75rem; - height: 1px; - background-color: #e30183; - opacity: 0.25; -} - -pre code { - padding: 3.5rem 0.75rem 0.75rem 0.75rem !important; - max-width: 100%; - overflow-x: auto; -} - -/* Fix for bad word break in tables.*/ - -.table tr td:first-child, -.table tr td:first-child * { - word-wrap: unset; - word-break: keep-all; - white-space: nowrap; -} - -/* ######################## - * ### AFFIX NAVIGATION ### - * ######################## - */ - -.sideaffix { - margin-top: 16px; - font-size: 13px; -} - -.contribution a::before { - /*Edit*/ - content: "\f044"; -} - -.contribution li + li a::before { - /*Github*/ - content: "\f09b"; -} - -.sideaffix > div.contribution > ul > li > a.contribution-link { - font-weight: normal; - text-transform: uppercase; -} - -.affix ul > li.active > a { - color: #337ab7; -} - -.affix .level2 > li.active > a { - text-decoration: underline; -} - -.affix ul ul > li > a:before { - content: ""; -} - -.affix > ul > li > a:before { - content: ""; - width: 2px; - background-color: #e35052; - top: 0; - left: 0; - bottom: 0; -} - -.nav > li > a:focus, -.nav > li > a:hover { - text-decoration: underline; -} - -/* ################### - * ### FONTAWESOME ### - * ################### - */ - -.expand-stub, -.contribution a, -.filter-icon { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: inline-block; - font-style: normal; - font-variant: normal; - text-rendering: auto; - line-height: 1; -} - -.expand-stub::before, -.contribution a::before, -.filter-icon::before { - margin-right: 4px; -} - -/*fab*/ - -.contribution a::before { - font-family: "Font Awesome 5 Brands", "Font Awesome 5 Free"; - font-weight: 400; -} - -/*fa, fas*/ - -.expand-stub::before, -.filter-icon::before { - font-family: "Font Awesome 5 Free"; - font-weight: 900; -} - -/* ################### - * ### FONTAWESOME ### - * ################### - */ - -.grad-bottom { - display: none; -} - -.footer { - border-top: none; - background-color: #222; - color: #fff; -} - -.footer a { - color: #fff !important; -} - -/* ################### - * ###### TABS ####### - * ################### - */ - -.tabGroup { - margin-bottom: 1rem; -} - -.tabGroup section[role="tabpanel"] { - border-left: 0; - border-right: 0; - border-bottom: 0; -} - -.tabGroup section[role="tabpanel"] > pre:last-child { - margin-bottom: -8px; -} - -/* ################### - * ##### PRODUCTS #### - * ################### - */ - -.products { - margin-top: 2rem; -} - -.product { - position: relative; - display: flex; - flex-direction: column; - min-width: 0; - word-wrap: break-word; - background-color: #fff; - background-clip: border-box; - text-align: center; - height: 100%; - padding-bottom: 2.3667rem; - margin-bottom: 2rem; -} - -@media (min-width: 992px) { - .products { - display: flex; - } - .product { - padding-bottom: 0; - } -} - -.product img { - max-height: 150px; -} - -.product h5 { - font-size: 1.25rem; -} - -.product h5 a { - text-decoration: none; -} - -.product h5 a::after { - display: none; -} - -.product .btn { - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0 auto; -} - -@media (min-width: 1200px) { - .product .btn { - max-width: 50%; - } -} - -.icon { - max-width: 20px; - margin-right: 0.5rem; -} - -/* ################### - * ####### BUGS ###### - * ################### - */ - -.inheritance .level0:before, -.inheritance .level1:before, -.inheritance .level2:before, -.inheritance .level3:before, -.inheritance .level4:before, -.inheritance .level5:before { - content: "\21B3"; - margin-right: 5px; -} - -/* -* Fix the sidebar so content is not cut off and indicate scroll. -*/ - -.bs-docs-sidebar.affix { - overflow-y: auto; - overflow-x: auto; - height: fit-content; - max-height: calc(100% - 100px); -} - -.bs-docs-sidebar.affix > ul.level1 { - overflow: initial; - max-height: 100%; -} diff --git a/templates_bak/styles/main.js b/templates_bak/styles/main.js deleted file mode 100644 index e5cf705d5..000000000 --- a/templates_bak/styles/main.js +++ /dev/null @@ -1,33 +0,0 @@ -$(function () { - - // The default header breaking function is breaking on camel casing which - // screws up our longer namespace representations. - // Update to add break after keyword only. - function breakPlainText(text) { - if (!text) { - return text; - } - - return text.replace(/(Namespace|Class|Enum|Struct|Type|Interface)/g, '$1'); - } - - $("h1.text-break").each(function () { - const $this = $(this); - - $this.html(breakPlainText($this.text())); - }); - - $(".table tr td:first-child *").each(function () { - const $this = $(this); - - $this.html(breakPlainText($this.text())); - }); - - // Fix the width of the right sidebar so we don't lose content. - const scrollbarWidth = 3.5 * (window.innerWidth - document.body.offsetWidth); - $(".sideaffix").each(function () { - const $this = $(this); - - $this.width($this.parent().outerWidth() + scrollbarWidth); - }); -}); \ No newline at end of file diff --git a/templates_bak/styles/vs2015.css b/templates_bak/styles/vs2015.css deleted file mode 100644 index d1d9be3ca..000000000 --- a/templates_bak/styles/vs2015.css +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Visual Studio 2015 dark style - * Author: Nicolas LLOBERA - */ - -.hljs { - display: block; - overflow-x: auto; - padding: 0.5em; - background: #1E1E1E; - color: #DCDCDC; -} - -.hljs-keyword, -.hljs-literal, -.hljs-symbol, -.hljs-name { - color: #569CD6; -} -.hljs-link { - color: #569CD6; - text-decoration: underline; -} - -.hljs-built_in, -.hljs-type { - color: #4EC9B0; -} - -.hljs-number, -.hljs-class { - color: #B8D7A3; -} - -.hljs-string, -.hljs-meta-string { - color: #D69D85; -} - -.hljs-regexp, -.hljs-template-tag { - color: #9A5334; -} - -.hljs-subst, -.hljs-function, -.hljs-title, -.hljs-params, -.hljs-formula { - color: #DCDCDC; -} - -.hljs-comment, -.hljs-quote { - color: #57A64A; - font-style: italic; -} - -.hljs-doctag { - color: #608B4E; -} - -.hljs-meta, -.hljs-meta-keyword, -.hljs-tag { - color: #9B9B9B; -} - -.hljs-variable, -.hljs-template-variable { - color: #BD63C5; -} - -.hljs-attr, -.hljs-attribute, -.hljs-builtin-name { - color: #9CDCFE; -} - -.hljs-section { - color: gold; -} - -.hljs-emphasis { - font-style: italic; -} - -.hljs-strong { - font-weight: bold; -} - -/*.hljs-code { - font-family:'Monospace'; -}*/ - -.hljs-bullet, -.hljs-selector-tag, -.hljs-selector-id, -.hljs-selector-class, -.hljs-selector-attr, -.hljs-selector-pseudo { - color: #D7BA7D; -} - -.hljs-addition { - background-color: #144212; - display: inline-block; - width: 100%; -} - -.hljs-deletion { - background-color: #600; - display: inline-block; - width: 100%; -} From c2cf6540517f6c58c841fb8b76e788dcaffbfc90 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 May 2026 21:56:50 +1000 Subject: [PATCH 21/21] Disable Prose. --- .github/workflows/build.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 083450eef..b0ee50e3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,22 +9,22 @@ on: - main jobs: - prose: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 + # prose: + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v6 - - name: Vale - uses: errata-ai/vale-action@reviewdog - with: - files: articles/. - env: - # Required - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + # - name: Vale + # uses: errata-ai/vale-action@reviewdog + # with: + # files: articles/. + # env: + # # Required + # GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} deploy: - needs: [prose] + # needs: [prose] runs-on: windows-latest
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 T6Tw4X00000NkvXXu0mjfj~65a literal 0 HcmV?d00001 diff --git a/templates_bak/favicon-16x16.png b/templates_bak/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..ffd1dec3c451c3768ef3dd346123a3704a8b2a6c GIT binary patch literal 653 zcmV;80&@L{P)22CVE*earI-$rE?DNeL<6@`$66Ct%w2+Sx7 zsfloebYT{i^A>I*kpc}f$|h?v)OX{)JN{rlV-!s##fH zRt$y(aTug52pFsc0t>7-1xMl@XEL@0owKBWMqWXmD5+$9R3B94xwv-USFFu<9+ZX2 zDoW%XZ4%-ZRxY~B{J$V@I7+?VBmE3Hu*C8k)4!DP06oq>oMa^d&23Z&HlI0$O?A~e z9uNpnz*MXcA6pZ~N`^#~*c8t{lw?T2^#wuBh9ZQ9+Awnb0_NYRGPVRX>tNx-_hkT+ zgG~>7;bj0ND7zlT+*AtFy-94pQVS?$Q~>yz7{Pc$_X-fUsmHfJ$z%nztin(kcHL~o zvpsdFh+o7+`ypLB!MBh(p=Uwer0%RQ?bY11wLPE({zjsINRzmrXB9`K627BGBtQh31 nx?sj;iX4esGv8td(LehOgVzu}3A-XH00000NkvXXu0mjf7b+YH literal 0 HcmV?d00001 diff --git a/templates_bak/favicon-32x32.png b/templates_bak/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..77c3fa2d30fad65b152bfbfb159ce646f2d63eaa GIT binary patch literal 1562 zcmV+#2IcvQP)U|>t&MCl%@5MFrd!xQZ8Sn7zjA(D zGR-VgU?ik6Mdx%=XlfKFmZnYalnIz0z`gHj=e+m5pYOe(9>)E6@B5zfoZs&`&+qvW zbVhBT`EkJvZ-7`lf%*Xq3s9elu`6+Sk7`rDCsS+TssI5sUART=`;}XsXBbx76!K1 zn|Pj)a4hDw|uT$e!Up?Ynyu12?MBDI z0947*XFoF-k$QSPLK24J)~=(tbKnf1aH)i@KXq5605)p}x~|^oQLMlr0K94eQLd&~ z9-`yO`H(y_Mi%9v>vlWNr7VTg(dk&&?Q}R4Q}21dI>A{fRhlMSz(oqw$-E;G)IS^< zb=weR3PscMEodpNLHPK1#H@W=MnV2Nx!}f!Zac0m+y$lmk^e}D2vnG|R1UalyE};Z zq7U%=LNo4N{0rx^RzXyV)RV;sNsR5057v!GaPyP>o?LJ{qD)}|z$+F=5_%RG0U~tt zFifc33^Eu{H+>D-Pd8xjf*g$6un1k&yJ%jy1H@GIl=&CckQjoaBM|%LyWloE8d3|; zdBv&^f>SA#nKCT|rk(;X5aY{>5IHFY*0NK$lv{|Puux2>--h4;;b>g43D)n9`z3DN zj>Q--FB5H57tlPX#D_B?;t(7-WXiCRB%!a^jNSbi2ALc5uwPa+m7JK zF#Nfw1lI3QNYJM2mk>QC6WzD(;d;S7ne&mBr5G?b1MNpI;qt6v{o0~XDg7LoZedyk zyg-D+4#uQ2yTF2tIA{JCZKoSBWWjVKtY3o8mb++PvlGl{#IR52qwmmYG=I1qEjy1P zJYzJ*R2P8T6hDJ?|7h_)1Zhjr2V#8YDV+QooLy9B(Sqm0gP2ofnEvK zydX1R;<0ZKo;(sazCDav`)lOUWMl*x7#K5vm;r<4W?|r4Sy0ol8nAI)#ncE7LBb!R zQX~mwb4w%UFWMov9h=bmv09}}|PNu_eZE?*n03_?{fQ}Iz^Ti68e<3((2^JqT zKqRqs)HmaD{?>qQS>psmHQX1Sw1q&6AJ=I5s~w(VrfG{Gk0}hvzNp0IoLI?YRWTje z_G@GVc|?WsX>U|Q95F)^I3vFxdg=>$bj!YxV4QKAjg68C+&l2aGH{ydV>o-+0rf5& zo^BdI`K14Tnxz7GHX(*9td2NEJ`D($uk@R9&C<8py7{>5M+eo$M2tNH6Vd@z`LrPP zOpRBSZH0}Qr#w99YRxndj!2X+PBVZ2oalw`*J)lj6sn+Zdw%w#!@kX_ySfNsG8okM zoQw8cO$>uUnog}$3|tAV&~+LE?wqQ)K@iV_kh#wZ#UEGS0fYVxh$ZcH)J zkWGv`CW>gHQHoMT>Aeg^>VSZtU?hMFVuQ2i-1qLxo0<0pCVu;0-j~D3%skHTp7*<_ z-l$Z4RQ*)W&MJHzsq*iyQa!6usYZ?zejn|kQvDa-8#hk;e3VL+?xs?W#_!-Qc!%&l zz9?QJM`~<6-46zhbqh-z=XO;4wA+u`XNUfzectti_C=R)?Rb|+?F8pj+E<-pv{Rkp z5@$G^2^z0Tu%$mp_j(Jxy{E!pyl504Oca4MzE~fzR1-ut# zIK{&($8#{(At9CihNt`CAnWfs3FeUZOW>EL zTY@wSuitH}Ld=GHU$;+(HwR@gTS?bDcT5~yYqob*2D3=H4r>27cNzIz-5#jvkM_|Zv&)+{j9(DExR08YM0BN z5x4V-ms6qT$}RADGy&?$Za_#tIqdN-hvXwyV9A3S(Asndc8o6p;odx0Hc%@(U-20Q zPR@kZpKrsW-zPy`X*+zirUC+=&V}_)=fURZ3Zb>}4t)4hDeBFG<+fT_IiNt`hUEDs z9BqV?doIF1W)?zI^-XB3x(Q8HolN4tx?;F@U?Hp;P%QE)TF;Foqtl?h^)76gpoLXq za$wEqJXkwg3jrewjP6}Mpcn#dN;oHBx}H~waA?~FT3Ze|9^f8He(u+Yg#&+>U9U8FF0nR}`-yCp74XG%vWG)V$;xq4_(? zc-JV+E6%4h=#w;)oTD|9onthUonk_!IL5A-;TZQ=PZ-8L5c-J6utO)&_jHr*`Lp{G z=DScg^Iy*lJ;r>P*N~r>AA4cQN#@J`<{H8L*~>0b%%@Fsj%I#svU4os?=+_~euxW_$J#IUHk#9r8lm&7SU|aDTc_%jt%hj%S&Fo$Zj&Jl8J4ONv2U_kHU|oA;!f z`O0|?NzHR5cJLS$iZ=hhubb;;F#`A1NwNXs+OQs;_s|}mw~J+)&v8gF(!F4C3M{lw z>-HU(B=Y=zxMo>5K5ui~7;6^Ur-83shOozeHP0>UmR+}~n_`{cpbWOZDNdX1F&Uem z*jEF(qBhVKUSm>OfUlxd;cHdlb%@b4aGoo!ThN^eZw$)hz6GAbJ#J}#ub!3$-QCcu zYun6%9(7}m0E-7@GrvZ;kzTjVx?dg|1r-HtO!uh+7h&DhEC`yB4V!1?!j{>2ux+ju zc6b*+;?XAdow|~CSm2z@c+R*cwYhyJgkyz#pfxc_T6vS>cEn2il0ktbJ3q4(@>t|C9`hI5vPxcfnP9pq3g3X=b7&H zld~Zw?keQPH#5nNZ3b@huLCRDcds|!fwv#dWIX53o3EScw#$XLZS%xfqP)Muc2}{# zSB0FL>KY5vUE`p-_&R$}`h#pfA8qdAlmaz{H`qtfA(vTR<~PV-bFQ0mrFT%j7sfKm z^W>@aKunEFH zt!FtB`IfXFuCnhH#kDc6$>;|E0R>|G4hQoXdK$Dz&FpU^pJ)P#p*|xLq3x=mf6vM? zi+e0JOweC&x|QX%OYnIMHfQSw|A7URr?T8ZPB+yh%b!mJvLWDq!BcY?&%X*RXP8op0Yzfo5($Rf!Puup zeshuOt47~8#~JG*mqe&3ZfEcH^dt6*CS~0Ni(s|Qd6qLN=_Z>m8kwly(_gMGXEuKi zbMV5mEl_Z#1+=k(6vVbdA(xm|$d7D=Rl~LBHm7xyZp^hwKg-v!PMGLS>yDQ7Enjb_ zU`uU7|Ks;yss+E6smwKc-V)tw2AqdLn-Z2Q3c6!tbyH0`4{c6$4_$HlFT;+hMX-Hx z5$u>$3?EE74!HK|XfnnjYYF`MsrP4K}C zL=Qhco?L;xMZ$B_b))XJHl?u6rULb2jx5&TVxDTy?P#1koYgdOuwMX6SY|604BB@~cIo1C?={Aog8J7H-u)h`7jr9&}=wA*&{VGGIVQpZh+d|I7 zVu>V%%K9}qu8B6Mb#Lfj0UJqds#slu&%0ayyd^fLy3uUiim{~U=Pj(8bXUTr{<`iU zRW+-Ls4k&emHTtd&lUFA{o9<@jk+PYUlq&0DF>&TXtpEPCD=z%)Xmmy<^I;&TzKB% zx?yv_s^;~o3r2Mq)kIX6P`zzt^ZR<<=DN42bVl_T*?=ypiKs5|8JyZpG1R~{T6dOm z9J9!C!}BJao32~XZ2Xhz5~>X-Kc^gdF^U>2hRJb!J+Ag586pVgpK6kqoVi4($2*gp(hJ6E^&}?IYK+s1v9GdV zibM2s=(`@B?v&!PT?KtP8f#uX9}FAeIV>~)B@(eWVDF;~`#vyEtVyXhq}q(vNY7%e z!0UzQB=yRRL&8}v275v9cdQ$zb{LQSm{%~jpg!|NtkDhqi5Qr|dNZ+I==&RHIG>4} z;TRt<%Qbd{71;YcKKzikhx@?_>|b;ucGkC1!v6Ho9|3#1X4s#{+}mpGPQ0%ahy6{~ zTcViV`I^Joidlmbyj3dbV`&`^Km3E8$MA!jJcb?YK)Zt+_9rEF?+td!p?k7B^kXs=qrF6LkjzP33m zuA-k*j7?IzdrAC^6g#&&`AI2fq_K(lNd4SE>SxX>?p1Dg%3HbJS$`LMtgqW=biHYp zCFBIXw)+V-->{Z5QtVXI8Df*v&lvd5_NvHtDQ67W8Gi7y%NFt(nVi7f?gDmGyU+ep zEJPltg_Nj9puSd0WD}%BG(vJj6C{V@zfn^0->DH-AWf8Xe5HQ!TccBjo^mfagY!1B zJNThs{tR#0X0cq1{G+*#6!IA*|4y8F#XSnX+*$&+JMZ;S`I~=C{lo9KSHnD47MCpY z9rr5DzFFNt_!)MrJKnI(V0k3R&hjxyJ|o30$C>$_@laD{-ZM2vEC0^rst#E0kzo+Bpmugrw8`)rB3*IYZ-hVcpmnwFM*JtGWa^U z9QJLgfc@`R!h!8N`2K@xIKI0MTCWJbO1e2Jq#nGTq_IhEuL`^|!fwa<^lHlADF;@< zE@yYjfmcpUg^n9{_4q@AOWBN0x>u$74EK?9E&G|N4@E5g z86RnEcPaKIIHwTsA6N7IUCJ3HyT9TZ1v~u<^tNxmb{E#-ISQTw2TsU@BOhOYA3m*t zW4mjSFST&&(^@$CQ7vqpu7x>{LTuVLwE%8j7xw+Fj(hOUDjh6xOr!astlb&kMY|LB zx3TZNgxUYNns7_8bH2amD)ir_`TN|*&VV*q=)ZS$b;IfJ>S5MH@hpG$eJl}5GFv${ z%wDUt*J0_X4A#r>dms(YecuT5H|VO!ybde=kfX=0_mRjKh8_7X^wb3G;+%xlt8&=s zj0=erJLO`Ztt*DR)Vs&41<&jRPReBY`!tt0wr6j5cCpXoevr@Hzp)DDI|;ei)(Hjd z%m{tA>s~h;-cl>}W(+>k0(P47uv%NL_TV{zv|gP*CLY?vISc9hDWq0kdo1=uPHV5x z+YC!_atlQLrygRyy#(izTZMTAepao$0V~F2;eHkTOw^uB>>m2jdE*|e^~l%zcgAoi5nnn=hW>h=X@u$bimvLk!;ko(|?XQ9dK|$-Nzu zVC$p;*t@J8_P$dAyB3zhB8N0#){b#?*Ss<|&m;UDc$Vplg_U|A$@i)`_5$#?(X##w zuleNoPS`02{_rg=bfB-Hi|~Kot7Z5p7hCCt`0?5E>)?)RnCm3?8KqoI>SsQhgZRaL z)829yc6(Q{y(-5U!R`gH0(+B$pY@hx{0y&~cn&=4;WLmHd4)rSi{CpW<47|sbIrr~m^t62*lA|8QjcH0 z$04mf$iKh+R0?#WujW^H$VKeW2s!Xlk94R)|40}4lEQ>mIPu9v2;W@~kzX|ECF=7_ z5ViXfocjDSM1R=`+GzB*f;)qbwtKLCR3V$~neJ6n>@;gzX;Xyuk5HTXNaem*931#S z$M6wOnssiRm@Dj4=LFanq|@_&KDXwgd1_ct9pg=Ju;UpuoD&ua{m5SR^n{x4jnTOS!gpSRe@`xlrS``DU5+!W2BO{3_B5-+zCOcmx>tpHt$9z6=9jcr7mY}QvMht0 zOVH*E-BQ?jNPUk(%p;}wyEG?|=3+{IhVYZ0;qzT7_5e0#74Tc)JGVQXL*{#xdIay! z&|=+X$QAaj)0ySKQtXDhNjUR?XD;RP85UQ2vOB{r;-~&Q_mRXGInJ13pN;s*zn}U~ z1LF|=Xs*8nq1$R;|GFypHlPajt;W|?Rj_|mHIx6Yx&RuI)irQn70SD{pjm!L+v5c7vr@D*ezf0Z5}X#=6pCSW9K#StO)O6bIu68U_)<7 zngdJuF6|Ta+U`C08P0bD_NvZJeXH60f~H6`Uu82&6aUV!Q%)e|4CgzYgOFmE^1au4 z)gs?bIfHiZhi6p!S5<6LRT<5MX}*eawS(7u)?)v4b{EdeQ7$Iu-_7l=qcbiYoBLfb zn%@bNJm^F7Rhm(5somu`L%v1{`%+0CCY&uQrRRiCZ?QSuj zv3jrSIyWQV1)FsUJo{eI+K? yAD|uw^$Dofrh1iXCKAehxlkTSxfta$oBHV%P+X \ No newline at end of file diff --git a/templates/styles/main.css b/templates_bak/styles/main.css similarity index 100% rename from templates/styles/main.css rename to templates_bak/styles/main.css diff --git a/templates/styles/main.js b/templates_bak/styles/main.js similarity index 100% rename from templates/styles/main.js rename to templates_bak/styles/main.js diff --git a/templates/styles/vs2015.css b/templates_bak/styles/vs2015.css similarity index 100% rename from templates/styles/vs2015.css rename to templates_bak/styles/vs2015.css From d1c546e73f4bdaa0c1893a9288e84be0b4b6c894 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 5 Apr 2026 14:09:48 +1000 Subject: [PATCH 02/21] Add polgonClipper --- .gitmodules | 4 +++ api/toc.yml | 2 ++ articles/imagesharp/index.md | 2 +- articles/polygonclipper/index.md | 42 ++++++++++++++++++++++++ articles/toc.md | 2 ++ build-dev.ps1 | 55 +++++++++++++++++++++++++++++--- docfx.json | 15 +++++++++ ext/Fonts | 2 +- ext/ImageSharp | 2 +- ext/ImageSharp.Drawing | 2 +- ext/PolygonClipper | 1 + 11 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 articles/polygonclipper/index.md create mode 160000 ext/PolygonClipper diff --git a/.gitmodules b/.gitmodules index 167cc17e4..0dfbf77c7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ path = ext/Fonts url = https://github.com/SixLabors/Fonts ignore = dirty +[submodule "ext/PolygonClipper"] + path = ext/PolygonClipper + url = https://github.com/SixLabors/PolygonClipper + ignore = dirty diff --git a/api/toc.yml b/api/toc.yml index f2065e69f..ff7b86df2 100644 --- a/api/toc.yml +++ b/api/toc.yml @@ -10,3 +10,5 @@ href: ImageSharp.Web.Providers.AWS/SixLabors.ImageSharp.Web.Providers.AWS.yml - name: Fonts href: Fonts/SixLabors.Fonts.yml +- name: PolygonClipper + href: PolygonClipper/SixLabors.PolygonClipper.yml diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index 0b6a35ab2..a39048f1f 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -6,7 +6,7 @@ Designed to simplify image processing, ImageSharp brings you an incredibly power ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations. -Built against [.NET 6](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-6), ImageSharp can be used in device, cloud, and embedded/IoT scenarios. +Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview), ImageSharp can be used in device, cloud, and embedded/IoT scenarios. ### License ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md new file mode 100644 index 000000000..4e57eeef9 --- /dev/null +++ b/articles/polygonclipper/index.md @@ -0,0 +1,42 @@ +# Introduction + +### What is PolygonClipper? +PolygonClipper is a high-performance polygon clipping and stroking in C#. + +Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview), PolygonClipper can be used in device, cloud, and embedded/IoT scenarios. + +### License +PolygonClipper is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. + +### Installation + +PolygonClipper is installed via [NuGet](https://www.nuget.org/packages/SixLabors.PolygonClipper) with nightly builds available on [MyGet](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.PolygonClipper). + +# [Package Manager](#tab/tabid-1) + +```bash +PM > Install-Package SixLabors.PolygonClipper -Version VERSION_NUMBER +``` + +# [.NET CLI](#tab/tabid-2) + +```bash +dotnet add package SixLabors.PolygonClipper --version VERSION_NUMBER +``` + +# [PackageReference](#tab/tabid-3) + +```xml + +``` + +# [Paket CLI](#tab/tabid-4) + +```bash +paket add SixLabors.PolygonClipper --version VERSION_NUMBER +``` + +*** + +>[!WARNING] +>Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. diff --git a/articles/toc.md b/articles/toc.md index c06e9970b..236f2d690 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -22,3 +22,5 @@ # [Fonts](fonts/index.md) ## [Getting Started](fonts/gettingstarted.md) ## [Custom Rendering](fonts/customrendering.md) + +# [PolygonClipper](polygonclipper/index.md) diff --git a/build-dev.ps1 b/build-dev.ps1 index 81d4fc71e..1bae129f3 100644 --- a/build-dev.ps1 +++ b/build-dev.ps1 @@ -1,8 +1,53 @@ -# Ensure all submodules are checked out with the latest main. (Useful for docs development.) -git submodule foreach git rm --cached -r . -git submodule foreach git reset --hard origin/main +function Invoke-Git { + param( + [Parameter(Mandatory = $true)] + [string]$RepositoryPath, -git submodule foreach git pull -f origin main --recurse-submodules + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]]$Arguments + ) + + & git -C $RepositoryPath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed in $RepositoryPath" + } +} + +# Ensure all submodules are initialized, including nested submodules used by dependencies. +Invoke-Git $PSScriptRoot submodule sync --recursive +Invoke-Git $PSScriptRoot submodule foreach --recursive git reset --hard +Invoke-Git $PSScriptRoot submodule foreach --recursive git clean -ffdx +Invoke-Git $PSScriptRoot submodule update --init --recursive + +# Ensure all top-level dependency submodules are checked out to the latest remote commit +# without leaving stale untracked files from previous checkouts behind. +Get-ChildItem (Join-Path $PSScriptRoot 'ext') -Directory | ForEach-Object { + $path = $_.FullName + + Write-Host "Updating submodule: $path" + + Invoke-Git $path fetch origin --tags --prune + + $defaultRef = (& git -C $path symbolic-ref refs/remotes/origin/HEAD).Trim() + if ($LASTEXITCODE -ne 0) { + throw "Unable to determine the default branch for $path" + } + + $defaultBranch = $defaultRef -replace '^refs/remotes/origin/', '' + + # Clean before and after the reset so older generated files cannot survive a branch move. + Invoke-Git $path clean -ffdx + Invoke-Git $path reset --hard "origin/$defaultBranch" + Invoke-Git $path clean -ffdx + + # Bring nested submodules to the commits referenced by the updated parent checkout, + # then clean their working trees too. + Invoke-Git $path submodule sync --recursive + Invoke-Git $path submodule update --init --recursive + Invoke-Git $path submodule foreach --recursive git reset --hard + Invoke-Git $path submodule foreach --recursive git clean -ffdx + Invoke-Git $path submodule update --init --recursive +} # Ensure deterministic builds do not affect submodule build # TODO: Remove first two values once all projects are updated to latest build props. @@ -11,4 +56,4 @@ $env:GITHUB_ACTIONS = $false $env:SIXLABORS_TESTING = $true -docfx \ No newline at end of file +docfx diff --git a/docfx.json b/docfx.json index caaea9535..cf9074d5b 100644 --- a/docfx.json +++ b/docfx.json @@ -89,6 +89,21 @@ "disableDefaultFilter": false, "properties": { + } + }, + { + "src": [ + { + "files": [ + "ext/PolygonClipper/src/PolygonClipper/**.csproj" + ] + } + ], + "output": "api/PolygonClipper", + "disableGitFeatures": false, + "disableDefaultFilter": false, + "properties": { + } } ], diff --git a/ext/Fonts b/ext/Fonts index eadc8dbc0..6011281ed 160000 --- a/ext/Fonts +++ b/ext/Fonts @@ -1 +1 @@ -Subproject commit eadc8dbc041d1b335ecf790ce400b5e069f075de +Subproject commit 6011281ed8b77c80a3598a6c934438384368d0d2 diff --git a/ext/ImageSharp b/ext/ImageSharp index f4a268473..b10a7eda4 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit f4a2684737732059876c13c481c856ed6b28e2c6 +Subproject commit b10a7eda4b354a5ca65c13090fc8b53a24ee9aed diff --git a/ext/ImageSharp.Drawing b/ext/ImageSharp.Drawing index 9911bc204..6d2010b0b 160000 --- a/ext/ImageSharp.Drawing +++ b/ext/ImageSharp.Drawing @@ -1 +1 @@ -Subproject commit 9911bc204b7eca0cbdc388bf1107958367d32817 +Subproject commit 6d2010b0b94ea7c1172e5feae8185c471b3980d1 diff --git a/ext/PolygonClipper b/ext/PolygonClipper new file mode 160000 index 000000000..96cd7a5ac --- /dev/null +++ b/ext/PolygonClipper @@ -0,0 +1 @@ +Subproject commit 96cd7a5ac4d29ff8119f039ff4acf9f123d541c3 From 1d4db3f11651417cc1ba3aafd76bd68962cdd983 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 5 Apr 2026 17:38:54 +1000 Subject: [PATCH 03/21] Update ImageSharp docs --- articles/fonts/index.md | 5 +- articles/imagesharp.drawing/index.md | 5 +- articles/imagesharp.web/index.md | 7 +- articles/imagesharp/animatedgif.md | 110 ++++++--- articles/imagesharp/colorandeffects.md | 147 ++++++++++++ articles/imagesharp/colorprofiles.md | 129 +++++++++++ articles/imagesharp/configuration.md | 105 +++++++-- articles/imagesharp/cropandcanvas.md | 100 ++++++++ articles/imagesharp/formatconversion.md | 116 ++++++++++ articles/imagesharp/gettingstarted.md | 150 +++++------- articles/imagesharp/gif.md | 132 +++++++++++ articles/imagesharp/identify.md | 78 +++++++ articles/imagesharp/imageformats.md | 176 ++++++++++---- articles/imagesharp/index.md | 55 +++-- articles/imagesharp/interop.md | 216 ++++++++++++++++++ articles/imagesharp/jpeg.md | 78 +++++++ articles/imagesharp/loadingandsaving.md | 141 ++++++++++++ articles/imagesharp/memorymanagement.md | 121 ++++++---- articles/imagesharp/metadata.md | 94 ++++++++ .../imagesharp/migratingfromsystemdrawing.md | 124 ++++++++++ articles/imagesharp/orientation.md | 86 +++++++ articles/imagesharp/pixelbuffers.md | 205 ++++++++--------- articles/imagesharp/pixelformats.md | 53 +++-- articles/imagesharp/png.md | 125 ++++++++++ articles/imagesharp/processing.md | 94 +++++--- articles/imagesharp/quantization.md | 125 ++++++++++ articles/imagesharp/recipes.md | 17 ++ articles/imagesharp/resize.md | 129 ++++++++--- articles/imagesharp/security.md | 121 +++++++++- articles/imagesharp/stripmetadata.md | 50 ++++ articles/imagesharp/thumbnails.md | 82 +++++++ articles/imagesharp/tiff.md | 83 +++++++ articles/imagesharp/troubleshooting.md | 130 +++++++++++ articles/imagesharp/webp.md | 89 ++++++++ articles/polygonclipper/index.md | 5 +- articles/toc.md | 38 ++- docfx.json | 3 +- index.md | 4 +- 38 files changed, 3067 insertions(+), 461 deletions(-) create mode 100644 articles/imagesharp/colorandeffects.md create mode 100644 articles/imagesharp/colorprofiles.md create mode 100644 articles/imagesharp/cropandcanvas.md create mode 100644 articles/imagesharp/formatconversion.md create mode 100644 articles/imagesharp/gif.md create mode 100644 articles/imagesharp/identify.md create mode 100644 articles/imagesharp/interop.md create mode 100644 articles/imagesharp/jpeg.md create mode 100644 articles/imagesharp/loadingandsaving.md create mode 100644 articles/imagesharp/metadata.md create mode 100644 articles/imagesharp/migratingfromsystemdrawing.md create mode 100644 articles/imagesharp/orientation.md create mode 100644 articles/imagesharp/png.md create mode 100644 articles/imagesharp/quantization.md create mode 100644 articles/imagesharp/recipes.md create mode 100644 articles/imagesharp/stripmetadata.md create mode 100644 articles/imagesharp/thumbnails.md create mode 100644 articles/imagesharp/tiff.md create mode 100644 articles/imagesharp/troubleshooting.md create mode 100644 articles/imagesharp/webp.md diff --git a/articles/fonts/index.md b/articles/fonts/index.md index f0079e9a5..da4539d48 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -7,10 +7,13 @@ Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-stan ### License Fonts is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/Fonts/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. + +>[!IMPORTANT] +>Starting with Fonts 3.0.0, projects that directly depend on SixLabors.Fonts require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation -Fonts is installed via [NuGet](https://www.nuget.org/packages/SixLabors.Fonts) with nightly builds available on [MyGet](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.Fonts). +Fonts is installed via [NuGet](https://www.nuget.org/packages/SixLabors.Fonts) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) diff --git a/articles/imagesharp.drawing/index.md b/articles/imagesharp.drawing/index.md index 6229a3809..491c9025a 100644 --- a/articles/imagesharp.drawing/index.md +++ b/articles/imagesharp.drawing/index.md @@ -9,10 +9,13 @@ Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-stan ### License ImageSharp.Drawing is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. + +>[!IMPORTANT] +>Starting with ImageSharp.Drawing 3.0.0, projects that directly depend on ImageSharp.Drawing require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation -ImageSharp.Drawing is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Drawing) with nightly builds available on [MyGet](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.ImageSharp.Drawing). +ImageSharp.Drawing is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Drawing) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 9166c7320..4d512a07f 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -7,10 +7,13 @@ ImageSharp.Web is designed from the ground up to be flexible and extensible. The ### License ImageSharp.Web is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Web/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. + +>[!IMPORTANT] +>Starting with ImageSharp.Web 4.0.0, projects that directly depend on ImageSharp.Web require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation -ImageSharp.Web is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web) with nightly builds available on [MyGet](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.ImageSharp.Web). +ImageSharp.Web is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) @@ -61,4 +64,4 @@ To disable the feature, either remove the property or set it to `false`: false -``` \ No newline at end of file +``` diff --git a/articles/imagesharp/animatedgif.md b/articles/imagesharp/animatedgif.md index c00749f70..8095a5569 100644 --- a/articles/imagesharp/animatedgif.md +++ b/articles/imagesharp/animatedgif.md @@ -1,45 +1,91 @@ -# Create an animated GIF +# Create an Animated GIF -The following example demonstrates how to create a animated gif from several single images. -The different image frames will be images with different colors. +ImageSharp builds animated GIFs by creating a multi-frame [`Image`](xref:SixLabors.ImageSharp.Image`1), configuring GIF metadata, and then saving with [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) when you need encoder-specific control. +When you start from [`Color`](xref:SixLabors.ImageSharp.Color) values, convert them to the target pixel type with `ToPixel()` before passing them to generic image constructors. -```c# -// Image dimensions of the gif. -const int width = 100; -const int height = 100; +For format background and palette tradeoffs, see [GIF and Animation](gif.md). This page focuses on the actual authoring workflow. -// Delay between frames in (1/100) of a second. -const int frameDelay = 100; +## Build a Multi-Frame GIF -// For demonstration: use images with different colors. -Color[] colors = { - Color.Green, - Color.Red +The root frame is the first animation frame. Additional frames are appended through [`ImageFrameCollection.AddFrame()`](xref:SixLabors.ImageSharp.ImageFrameCollection.AddFrame(SixLabors.ImageSharp.ImageFrame)), which clones the source frame and requires it to match the image dimensions. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.PixelFormats; + +Color[] colors = +{ + Color.Orange, + Color.DeepSkyBlue, + Color.MediumSeaGreen }; -// Create empty image. -using Image gif = new(width, height, Color.Blue); +using Image gif = new(120, 120, colors[0].ToPixel()); -// Set animation loop repeat count to 5. -var gifMetaData = gif.Metadata.GetGifMetadata(); -gifMetaData.RepeatCount = 5; +GifMetadata gifMetadata = gif.Metadata.GetGifMetadata(); +gifMetadata.RepeatCount = 0; -// Set the delay until the next image is displayed. -GifFrameMetadata metadata = gif.Frames.RootFrame.Metadata.GetGifMetadata(); -metadata.FrameDelay = frameDelay; -for (int i = 0; i < colors.Length; i++) +GifFrameMetadata rootFrameMetadata = gif.Frames.RootFrame.Metadata.GetGifMetadata(); +rootFrameMetadata.FrameDelay = 10; +rootFrameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground; + +for (int i = 1; i < colors.Length; i++) { - // Create a color image, which will be added to the gif. - using Image image = new(width, height, colors[i]); + using Image frameImage = new(120, 120, colors[i].ToPixel()); - // Set the delay until the next image is displayed. - metadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); - metadata.FrameDelay = frameDelay; + GifFrameMetadata frameMetadata = frameImage.Frames.RootFrame.Metadata.GetGifMetadata(); + frameMetadata.FrameDelay = 10; + frameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground; - // Add the color image to the gif. - gif.Frames.AddFrame(image.Frames.RootFrame); + gif.Frames.AddFrame(frameImage.Frames.RootFrame); } -// Save the final result. -gif.SaveAsGif("output.gif"); -``` \ No newline at end of file +gif.SaveAsGif("output.gif", new GifEncoder +{ + ColorTableMode = FrameColorTableMode.Global +}); +``` + +## Control Looping and Frame Timing + +[`GifMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata) stores image-level animation settings: + +- [`RepeatCount`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata.RepeatCount) controls looping. `0` means loop indefinitely. +- [`ColorTableMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata.ColorTableMode) describes whether the animation uses a global or local palette layout. + +[`GifFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata) stores per-frame settings: + +- [`FrameDelay`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.FrameDelay) is measured in hundredths of a second. +- [`DisposalMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.DisposalMode) controls how the previous frame is treated before the next frame is drawn. +- [`ColorTableMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.ColorTableMode) can override palette behavior for an individual frame. + +The most important disposal modes are: + +- [`DoNotDispose`](xref:SixLabors.ImageSharp.Formats.FrameDisposalMode.DoNotDispose) when later frames should draw over earlier content. +- [`RestoreToBackground`](xref:SixLabors.ImageSharp.Formats.FrameDisposalMode.RestoreToBackground) when a frame should be cleared before the next frame is shown. +- [`RestoreToPrevious`](xref:SixLabors.ImageSharp.Formats.FrameDisposalMode.RestoreToPrevious) when the previous composited state should be restored. + +## Palette Choice Still Matters + +GIF is always palette-based, so saving an animation is always a quantization step. [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) inherits the quantization controls exposed by [`QuantizingImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder): + +- [`Quantizer`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.Quantizer) +- [`PixelSamplingStrategy`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.PixelSamplingStrategy) +- [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder.TransparentColorMode) + +In practice: + +- Use [`FrameColorTableMode.Global`](xref:SixLabors.ImageSharp.Formats.FrameColorTableMode.Global) when you want one shared palette across the animation, often for smaller files and more consistent colors across frames. +- Use [`FrameColorTableMode.Local`](xref:SixLabors.ImageSharp.Formats.FrameColorTableMode.Local) when frames differ enough that per-frame palettes produce better results. +- Choose an explicit quantizer when gradients, UI art, or brand colors matter. + +See [Quantization, Palettes, and Dithering](quantization.md) for the full quantization story. + +## Practical Guidance + +- Keep frame dimensions consistent with the GIF canvas size before adding them. +- Set `FrameDelay` and `DisposalMode` on every frame you care about rather than relying on defaults. +- Prefer a global palette for simple flat-color or UI-style animations. +- Consider [WebP](webp.md) instead of GIF when you need better compression or more modern animation behavior. diff --git a/articles/imagesharp/colorandeffects.md b/articles/imagesharp/colorandeffects.md new file mode 100644 index 000000000..3d019ece2 --- /dev/null +++ b/articles/imagesharp/colorandeffects.md @@ -0,0 +1,147 @@ +# Color and Effects + +ImageSharp includes a wide range of processors for tonal adjustment, color transforms, and simple stylistic effects. Common entry points include `Grayscale()`, `Sepia()`, `Brightness()`, `Contrast()`, `Hue()`, `Saturate()`, and `Opacity()`. Under the hood, many of these effects are expressed as a [`ColorMatrix`](xref:SixLabors.ImageSharp.ColorMatrix) and applied with [`Filter()`](xref:SixLabors.ImageSharp.Processing.FilterExtensions.Filter*). + +## Convert to Grayscale + +Use `Grayscale()` to remove color information: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Grayscale()); +``` + +ImageSharp also supports [`GrayscaleMode`](xref:SixLabors.ImageSharp.Processing.GrayscaleMode) when you need a specific conversion mode. + +## Apply a Sepia Tone + +Use `Sepia()` for a classic warm-tone effect: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Sepia()); +``` + +## Adjust Brightness and Contrast + +Use `Brightness()` and `Contrast()` to tune exposure and punch: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .Brightness(1.1F) + .Contrast(1.2F)); +``` + +Values greater than `1` increase the effect. Values less than `1` reduce it. + +## Shift Hue and Saturation + +Use `Hue()` and `Saturate()` when you want to push color balance or intensity: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .Hue(30) + .Saturate(1.25F)); +``` + +## Adjust Opacity + +Use `Opacity()` to reduce alpha values across the image: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.Opacity(0.5F)); +``` + +This is most useful when working with images that already include transparency. + +## Use ColorMatrix for Custom Filters + +[`ColorMatrix`](xref:SixLabors.ImageSharp.ColorMatrix) is the low-level type for custom channel transforms. It is a 5x4 matrix over the color and alpha channels, and [`Filter()`](xref:SixLabors.ImageSharp.Processing.FilterExtensions.Filter*) applies that matrix to the image. + +That makes `ColorMatrix` the right tool when the built-in processors are close to what you need but not quite exact. The diagonal fields such as `M11`, `M22`, `M33`, and `M44` scale channels, while the last-row fields such as `M51`, `M52`, and `M53` add channel bias. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +ColorMatrix warmTone = new() +{ + M11 = 1.08F, + M22 = 1.00F, + M33 = 0.92F, + M44 = 1F, + M51 = 0.02F +}; + +image.Mutate(x => x.Filter(warmTone)); +``` + +## Reuse Known Filter Matrices + +If you want matrix-based control without building every matrix by hand, use [`KnownFilterMatrices`](xref:SixLabors.ImageSharp.Processing.KnownFilterMatrices). The built-in brightness, contrast, grayscale, hue, saturation, opacity, sepia, and preset camera-look filters all come from this API surface. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +ColorMatrix matrix = + KnownFilterMatrices.CreateHueFilter(20F) + * KnownFilterMatrices.CreateSaturateFilter(1.15F); + +image.Mutate(x => x.Filter(matrix)); +``` + +You can also use the predefined matrices directly for stylized looks such as [`KodachromeFilter`](xref:SixLabors.ImageSharp.Processing.KnownFilterMatrices.KodachromeFilter) and [`PolaroidFilter`](xref:SixLabors.ImageSharp.Processing.KnownFilterMatrices.PolaroidFilter). + +## Chain Effects in a Single Pipeline + +These processors are designed to compose cleanly: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .AutoOrient() + .Resize(800, 800) + .Brightness(1.05F) + .Contrast(1.1F) + .Saturate(1.15F)); +``` + +As with other processors, order matters when combining effects. + +## Related Topics + +- [Processing Images](processing.md) +- [Rotate, Flip, and Auto-Orient](orientation.md) +- [Crop, Pad, and Canvas](cropandcanvas.md) diff --git a/articles/imagesharp/colorprofiles.md b/articles/imagesharp/colorprofiles.md new file mode 100644 index 000000000..ec46121f7 --- /dev/null +++ b/articles/imagesharp/colorprofiles.md @@ -0,0 +1,129 @@ +# Color Profiles and Color Conversion + +ImageSharp exposes color management in two different layers: + +- Embedded color metadata on decoded images, such as [`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) and [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile). +- Value-level conversion APIs in [`SixLabors.ImageSharp.ColorProfiles`](xref:SixLabors.ImageSharp.ColorProfiles). + +Those layers are related, but they are not the same thing. Preserving an embedded ICC profile does not automatically mean pixels were converted, and converting pixels does not automatically mean every output format can store the same profile metadata. + +## Inspect Embedded Color Metadata + +Embedded color metadata is available through [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata): + +```csharp +using SixLabors.ImageSharp; + +using Image image = Image.Load("input.jpg"); + +if (image.Metadata.IccProfile is not null) +{ + Console.WriteLine(image.Metadata.IccProfile.Header.ProfileConnectionSpace); + Console.WriteLine(image.Metadata.IccProfile.Header.RenderingIntent); +} + +if (image.Metadata.CicpProfile is not null) +{ + Console.WriteLine(image.Metadata.CicpProfile.ColorPrimaries); + Console.WriteLine(image.Metadata.CicpProfile.TransferCharacteristics); + Console.WriteLine(image.Metadata.CicpProfile.MatrixCoefficients); + Console.WriteLine(image.Metadata.CicpProfile.FullRange); +} +``` + +[`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) is the richer general-purpose color profile mechanism. [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile) carries standardized color signaling values such as primaries, transfer characteristics, matrix coefficients, and range information. + +## Control ICC Handling During Decode + +By default, [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) preserves embedded ICC profiles in metadata and does not transform the decoded pixels. + +If you need different behavior, use [`DecoderOptions.ColorProfileHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.ColorProfileHandling): + +- [`Preserve`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Preserve) keeps embedded ICC profiles intact. +- [`Compact`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Compact) removes canonical sRGB ICC profiles without changing the pixels. +- [`Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert) converts decoded pixels to sRGB v4 and removes the original ICC profile. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +DecoderOptions options = new() +{ + ColorProfileHandling = ColorProfileHandling.Convert +}; + +using Image image = Image.Load(options, "input-with-icc.jpg"); +``` + +This is useful when you want a decode pipeline that normalizes images into a predictable sRGB output space up front. + +## Color Profile Values Are Not `TPixel` Formats + +The value types used by [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) are not the same thing as ImageSharp pixel formats. + +Types such as [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb), [`Cmyk`](xref:SixLabors.ImageSharp.ColorProfiles.Cmyk), [`Hsl`](xref:SixLabors.ImageSharp.ColorProfiles.Hsl), [`YCbCr`](xref:SixLabors.ImageSharp.ColorProfiles.YCbCr), [`CieLab`](xref:SixLabors.ImageSharp.ColorProfiles.CieLab), and [`CieXyz`](xref:SixLabors.ImageSharp.ColorProfiles.CieXyz) are value types used for color-space conversion. They participate in the [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) pipeline, but they are not [`Image`](xref:SixLabors.ImageSharp.Image`1) storage formats and do not implement [`IPixel`](xref:SixLabors.ImageSharp.PixelFormats.IPixel`1). + +That distinction matters because ImageSharp image processing is built around pixel formats that can move efficiently through RGBA-oriented [`Vector4`](xref:System.Numerics.Vector4) conversion paths. Color profile types model color spaces and profile connection spaces instead. + +## Convert Between Working Spaces + +[`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) converts color values and spans between supported profile types. For RGB-to-RGB conversions, the working spaces come from [`ColorConversionOptions`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions): + +```csharp +using SixLabors.ImageSharp.ColorProfiles; + +ColorProfileConverter converter = new(new ColorConversionOptions +{ + SourceRgbWorkingSpace = KnownRgbWorkingSpaces.SRgb, + TargetRgbWorkingSpace = KnownRgbWorkingSpaces.AdobeRgb1998 +}); + +Rgb source = new(0.25F, 0.5F, 0.75F); +Rgb converted = converter.Convert(source); +``` + +The source and target value types are both [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb) here, but the conversion still changes because the working-space definitions are different. This is value-level color conversion, not a change to the in-memory `TPixel` type of an [`Image`](xref:SixLabors.ImageSharp.Image`1). + +## Choose Working Spaces Explicitly + +[`KnownRgbWorkingSpaces`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces) exposes the built-in RGB working spaces, including: + +- [`SRgb`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.SRgb) +- [`Rec709`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.Rec709) +- [`Rec2020`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.Rec2020) +- [`AdobeRgb1998`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.AdobeRgb1998) +- [`ProPhotoRgb`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.ProPhotoRgb) +- [`WideGamutRgb`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.WideGamutRgb) + +Choose the working spaces explicitly when you are doing color conversion outside the normal decode pipeline and need to be clear about the source and destination assumptions. + +## Use ICC Profiles for Explicit Conversion + +If you have actual ICC profiles for the source and destination, set [`ColorConversionOptions.SourceIccProfile`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.SourceIccProfile) and [`ColorConversionOptions.TargetIccProfile`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.TargetIccProfile). The same [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) API will then use the ICC-based conversion path instead of only the working-space defaults. + +This is the right choice when the embedded or device-specific ICC data matters more than a generic named working space. + +## Advanced Conversion Options + +[`ColorConversionOptions`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions) also lets you tune lower-level conversion details: + +- [`SourceWhitePoint`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.SourceWhitePoint) and [`TargetWhitePoint`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.TargetWhitePoint) +- [`AdaptationMatrix`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.AdaptationMatrix), which defaults to [`KnownChromaticAdaptationMatrices.Bradford`](xref:SixLabors.ImageSharp.ColorProfiles.KnownChromaticAdaptationMatrices.Bradford) +- [`YCbCrTransform`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.YCbCrTransform), which defaults to [`KnownYCbCrMatrices.BT601`](xref:SixLabors.ImageSharp.ColorProfiles.KnownYCbCrMatrices.BT601) + +Most applications do not need to override those defaults, but they are available when you need tighter control over conversion behavior. + +## Practical Guidance + +- Preserve embedded ICC metadata when you need round-tripping or want the original profile to stay attached to the image. +- Use decode-time `ColorProfileHandling.Convert` when you want pixels normalized to sRGB as soon as the image is loaded. +- Use `Compact` when you want to remove redundant canonical sRGB ICC profiles without changing pixel values. +- Do not confuse metadata preservation with pixel conversion. They solve different problems. +- Do not confuse color profile value types with ImageSharp pixel formats. `ColorProfileConverter` works on color-space values, while `Image` works with `IPixel` storage types. + +## Related Topics + +- [Working with Metadata](metadata.md) +- [Loading, Identifying, and Saving](loadingandsaving.md) +- [Convert Between Formats](formatconversion.md) +- [Pixel Formats](pixelformats.md) diff --git a/articles/imagesharp/configuration.md b/articles/imagesharp/configuration.md index 5d6d9f18c..d02ed70d9 100644 --- a/articles/imagesharp/configuration.md +++ b/articles/imagesharp/configuration.md @@ -1,30 +1,101 @@ # Configuration -ImageSharp contains a @"SixLabors.ImageSharp.Configuration" class designed to allow the configuration of application wide settings. -This class provides a range of configuration opportunities that cover format support, memory and parallelization settings and more. +[`Configuration`](xref:SixLabors.ImageSharp.Configuration) controls how ImageSharp discovers formats, allocates memory, reads streams, and runs processor pipelines. -@"SixLabors.ImageSharp.Configuration.Default" is a shared singleton that is used to configure the default behavior of the ImageSharp library but it is possible to provide your own instances depended upon your required setup. +For most applications, [`Configuration.Default`](xref:SixLabors.ImageSharp.Configuration.Default) is the right place for process-wide defaults. When you need different behavior for one workload, clone it and use the cloned instance locally. -### Injection Points. +## Use a Local Configuration for Targeted Overrides -The @"SixLabors.ImageSharp.Configuration" class can be injected in several places within the API to allow overriding global values. This provides you with the means to apply fine grain control over your processing activity to cater for your environment. +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; -- The @"SixLabors.ImageSharp.Image" and @"SixLabors.ImageSharp.Image`1" constructors. -- The @"SixLabors.ImageSharp.Image.Load*" methods and variants. -- The @"SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*" and @"SixLabors.ImageSharp.Processing.ProcessingExtensions.Clone*" methods. +Configuration config = Configuration.Default.Clone(); +config.MaxDegreeOfParallelism = 2; +config.PreferContiguousImageBuffers = true; -### Configuring ImageFormats +DecoderOptions options = new() +{ + Configuration = config +}; -As mentioned in [Image Formats](imageformats.md) it is possible to configure your own format collection for the API to consume. -For example, if you wanted to restrict the library to support a specific collection of formats you would configure the library as follows: +using Image image = Image.Load(options, stream); +``` + +This pattern is usually better than mutating [`Configuration.Default`](xref:SixLabors.ImageSharp.Configuration.Default) when the override only matters for one pipeline. + +## What Configuration Controls + +The main knobs on [`Configuration`](xref:SixLabors.ImageSharp.Configuration) are: + +- [`ImageFormatsManager`](xref:SixLabors.ImageSharp.Configuration.ImageFormatsManager) for format registration, encoders, decoders, and detectors. +- [`MemoryAllocator`](xref:SixLabors.ImageSharp.Configuration.MemoryAllocator) for pooled buffer allocation. +- [`MaxDegreeOfParallelism`](xref:SixLabors.ImageSharp.Configuration.MaxDegreeOfParallelism) for row and processor parallelism. +- [`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) for interop-oriented contiguous buffers. +- [`StreamProcessingBufferSize`](xref:SixLabors.ImageSharp.Configuration.StreamProcessingBufferSize) for stream copy buffer size. +- [`ReadOrigin`](xref:SixLabors.ImageSharp.Configuration.ReadOrigin) for whether decode operations read from the current stream position or from the beginning. +- [`Properties`](xref:SixLabors.ImageSharp.Configuration.Properties) for processor-specific defaults and shared settings. + +## Register a Specific Format Set + +[`Configuration`](xref:SixLabors.ImageSharp.Configuration) can be created with an explicit set of [`IImageFormatConfigurationModule`](xref:SixLabors.ImageSharp.Formats.IImageFormatConfigurationModule) registrations: -```c# -var configuration = new Configuration( +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; + +Configuration config = new( new PngConfigurationModule(), new JpegConfigurationModule(), - new GifConfigurationModule(), - new BmpConfigurationModule(), - new TgaConfigurationModule() - new CustomFormatConfigurationModule()); + new GifConfigurationModule()); +``` + +This is useful when you want a deliberately restricted format set for a service or plugin boundary. For more advanced scenarios, [`ImageFormatManager`](xref:SixLabors.ImageSharp.Formats.ImageFormatManager) also exposes methods such as `SetEncoder(...)`, `SetDecoder(...)`, and `AddImageFormatDetector(...)`. + +## Tune Processor Defaults + +ImageSharp stores some processor defaults through [`Configuration.Properties`](xref:SixLabors.ImageSharp.Configuration.Properties). A common example is [`GraphicsOptions`](xref:SixLabors.ImageSharp.GraphicsOptions): + +```csharp +using SixLabors.ImageSharp; +Configuration config = Configuration.Default.Clone(); +config.SetGraphicsOptions(options => +{ + options.Antialias = false; + options.BlendPercentage = 0.75F; +}); ``` + +Those defaults then flow into processing APIs that read graphics options from the current configuration or processing context. + +## Parallelism and Throughput + +[`MaxDegreeOfParallelism`](xref:SixLabors.ImageSharp.Configuration.MaxDegreeOfParallelism) defaults to the machine processor count. That is often a good default for desktop and batch workloads. + +For server-side applications running many requests at once, lowering this value on a local configuration can improve overall throughput by avoiding excessive per-image parallel work. + +## Stream Behavior + +[`ReadOrigin`](xref:SixLabors.ImageSharp.Configuration.ReadOrigin) controls whether decoding starts at the current stream position or the beginning of a seekable stream. + +[`StreamProcessingBufferSize`](xref:SixLabors.ImageSharp.Configuration.StreamProcessingBufferSize) controls the buffer size used when ImageSharp copies stream data internally. Most applications should leave it alone unless profiling shows a reason to change it. + +## When to Customize Configuration + +Use a custom or cloned configuration when: + +- You want a restricted set of supported formats. +- You need a custom [`MemoryAllocator`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator). +- You want different parallelism settings for a specific workload. +- You need contiguous buffers for interop. +- You need different stream-origin behavior for a pipeline that reads partially consumed streams. + +## Related Topics + +- [Image Formats](imageformats.md) +- [Memory Management](memorymanagement.md) +- [Interop and Raw Memory](interop.md) +- [Troubleshooting](troubleshooting.md) diff --git a/articles/imagesharp/cropandcanvas.md b/articles/imagesharp/cropandcanvas.md new file mode 100644 index 000000000..24004a82c --- /dev/null +++ b/articles/imagesharp/cropandcanvas.md @@ -0,0 +1,100 @@ +# Crop, Pad, and Canvas + +ImageSharp includes several processors for changing the visible region of an image or the size of its canvas. The most commonly used are `Crop()`, `Pad()`, `BackgroundColor()`, and `EntropyCrop()`. + +## Crop to an Explicit Rectangle + +Use `Crop()` when you know the exact rectangle you want to keep: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Crop(new Rectangle(100, 80, 1200, 800))); +``` + +This removes everything outside the requested bounds. + +## Crop by Width and Height + +If the crop should start at the top-left corner, you can pass just width and height: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Crop(800, 600)); +``` + +## Pad to a Larger Canvas + +Use `Pad()` when you want to enlarge the canvas without scaling the image: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.Pad(1200, 1200, Color.White)); +``` + +This is useful when generating square thumbnails, social cards, or export assets that require a fixed output size. + +## Fill Transparent Areas or Flatten Onto a Background + +Use `BackgroundColor()` to fill transparent pixels or composite the current image over a solid color: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.BackgroundColor(Color.White)); +``` + +This is a common step before saving a transparent source image to a format that does not support transparency. + +## Crop Automatically Based on Content + +Use `EntropyCrop()` when you want ImageSharp to trim low-information borders automatically: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.EntropyCrop()); +``` + +This can be useful for removing large flat borders or whitespace-like areas before additional processing. + +## Combine Crop and Resize + +Cropping and resizing are often used together: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .AutoOrient() + .Crop(new Rectangle(200, 120, 1000, 1000)) + .Resize(400, 400)); +``` + +Cropping first can reduce the amount of pixel data that later processors need to touch. + +## Related Topics + +- [Processing Images](processing.md) +- [Resizing Images](resize.md) +- [Rotate, Flip, and Auto-Orient](orientation.md) diff --git a/articles/imagesharp/formatconversion.md b/articles/imagesharp/formatconversion.md new file mode 100644 index 000000000..28bfbaa35 --- /dev/null +++ b/articles/imagesharp/formatconversion.md @@ -0,0 +1,116 @@ +# Convert Between Formats + +Format conversion in ImageSharp is a decode-and-re-encode operation, but it is not a blind one. Once an image is loaded, the processing pipeline works with format-agnostic pixel data, while the metadata layer still carries enough information for the destination format to choose the best representation it supports. + +## How ImageSharp Bridges Formats + +ImageSharp's built-in codec metadata translates through [`FormatConnectingMetadata`](xref:SixLabors.ImageSharp.Formats.FormatConnectingMetadata) and [`FormatConnectingFrameMetadata`](xref:SixLabors.ImageSharp.Formats.FormatConnectingFrameMetadata). Those bridge types carry the common image and frame semantics that can be shared across formats, including: + +- Encoded pixel information through [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo), such as color model, alpha behavior, bit depth, and component precision. +- Encoding intent such as lossy versus lossless output and quality. +- Indexed-color settings such as shared color table mode. +- Animation settings such as background color, repeat count, frame duration, blend mode, and disposal mode. + +That is why ImageSharp's conversion story is more comprehensive than simply decoding everything to one in-memory layout and forgetting how the source was encoded. For example, PNG metadata can derive palette, grayscale, RGB, or RGBA output and choose 1, 2, 4, 8, or 16-bit encoding from bridged pixel information, while GIF metadata can carry indexed color-table mode and repeat-count behavior forward when the target format supports them. + +## Use Identify to Plan the Conversion + +Before converting, [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify*) can tell you how the source is encoded. [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) exposes [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo), including: + +- [`BitsPerPixel`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.BitsPerPixel) +- [`ColorType`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.ColorType) +- [`AlphaRepresentation`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.AlphaRepresentation) +- [`ComponentInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.ComponentInfo) for component count and precision + +This is useful when you need to decide whether to flatten transparency for JPEG, keep higher-precision data in PNG or TIFF, or preserve indexed workflows where the target format supports them. + +## Convert PNG to JPEG + +JPEG does not support alpha transparency, so transparent sources usually need to be flattened onto a background color first: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.BackgroundColor(Color.White)); + +image.Save("output.jpg", new JpegEncoder +{ + Quality = 85 +}); +``` + +## Convert JPEG to WebP + +Use a WebP encoder when you want to move a photographic source to a more modern delivery format: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; + +using Image image = Image.Load("input.jpg"); + +image.Save("output.webp", new WebpEncoder +{ + FileFormat = WebpFileFormatType.Lossy, + Quality = 80 +}); +``` + +## Convert Any Input to PNG + +PNG is a good target when you want lossless output or transparency support: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; + +using Image image = Image.Load("input.bin"); + +image.Save("output.png", new PngEncoder()); +``` + +## Choose the Output Based on Pixel Info + +When you want format conversion to respect the source characteristics, inspect the encoded pixel type first and then choose the encoder accordingly: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; + +ImageInfo info = Image.Identify("input.tiff"); + +bool hasAlpha = info.PixelType.ColorType.HasFlag(PixelColorType.Alpha); +int precision = info.PixelType.ComponentInfo?.GetMaximumComponentPrecision() ?? 8; + +using Image image = Image.Load("input.tiff"); + +if (hasAlpha) +{ + image.Save("output.png", new PngEncoder + { + BitDepth = precision > 8 ? PngBitDepth.Bit16 : PngBitDepth.Bit8 + }); +} +else +{ + image.Save("output.jpg", new JpegEncoder + { + Quality = 85 + }); +} +``` + +## Notes + +- Converting from a lossy format to a lossless format does not restore discarded detail. +- Converting a transparent image to JPEG requires flattening or compositing first. +- ImageSharp uses bridged metadata and pixel-type information to pick good destination settings when the target format can represent them. +- If you care about exact output tradeoffs, use an explicit encoder rather than relying only on the file extension. + +For more on format behavior and encoder options, see [Image Formats](imageformats.md). For more on inspecting pixel types before a conversion, see [Read Image Info Without Decoding](identify.md) and [Pixel Formats](pixelformats.md). diff --git a/articles/imagesharp/gettingstarted.md b/articles/imagesharp/gettingstarted.md index a77252ae1..0d959b836 100644 --- a/articles/imagesharp/gettingstarted.md +++ b/articles/imagesharp/gettingstarted.md @@ -1,137 +1,93 @@ # Getting Started ->[!NOTE] ->The official guide assumes intermediate level knowledge of C# and .NET. If you are totally new to .NET development, it might not be the best idea to jump right into a framework as your first step - grasp the basics then come back. Prior experience with other languages and frameworks helps, but is not required. +ImageSharp centers around a small set of core types: -### ImageSharp Images +- [`Image`](xref:SixLabors.ImageSharp.Image) is the format-agnostic image container used by the main loading, processing, and saving APIs. +- `Image` is the generic image container to use when you know the pixel format and want direct pixel access. See [Pixel Formats](pixelformats.md) for more detail. +- [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame) and `ImageFrame` represent individual frames in multi-frame images such as GIF and WebP. +- [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) gives you dimensions, pixel information, and metadata without fully decoding the image. -ImageSharp provides several classes for storing pixel data: +## Load, Process, and Save an Image -- @"SixLabors.ImageSharp.Image" A pixel format agnostic image container used for general processing operations. -- @"SixLabors.ImageSharp.Image`1" A generic image container that allows per-pixel access. +The most common ImageSharp workflow is to load an image, apply a processing pipeline, and save it again: -In addition there are classes available that represent individual image frames: +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; -- @"SixLabors.ImageSharp.ImageFrame" A pixel format agnostic image frame container. -- @"SixLabors.ImageSharp.ImageFrame`1" A generic image frame container that allows per-pixel access. -- @"SixLabors.ImageSharp.IndexedImageFrame`1" A generic image frame used for indexed image pixel data where each pixel buffer value represents an index in a color palette. +using Image image = Image.Load("input.jpg"); -For more information on pixel formats please see the following [documentation](pixelformats.md). +image.Mutate(x => x + .AutoOrient() + .Resize(800, 600)); -### Loading and Saving Images +image.Save("output.jpg"); +``` -ImageSharp provides several options for loading and saving images to cover different scenarios. The library automatically detects the source image format upon load and it is possible to dictate which image format to save an image pixel data to. +This example shows the core workflow: -```c# -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +- `Image.Load()` detects the input format from the image data. +- `Mutate()` applies processors to the current image in order. +- `Save()` picks an encoder from the output path unless you pass one explicitly. -// Open the file automatically detecting the file type to decode it. -// Our image is now in an uncompressed, file format agnostic, structure in-memory as -// a series of pixels. -// You can also specify the pixel format using a type parameter (e.g. Image image = Image.Load("foo.jpg")) -using (Image image = Image.Load("foo.jpg")) -{ - // Resize the image in place and return it for chaining. - // 'x' signifies the current image processing context. - image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2)); - - // The library automatically picks an encoder based on the file extension then - // encodes and write the data to disk. - // You can optionally set the encoder to choose. - image.Save("bar.jpg"); -} // Dispose - releasing memory into a memory pool ready for the next image you wish to process. -``` +For more detail, see [Loading, Identifying, and Saving](loadingandsaving.md), [Processing Images](processing.md), and [Image Formats](imageformats.md). -In this very basic example you are actually utilizing several core ImageSharp features: -- [Image Formats](imageformats.md) by loading and saving an image. -- [Image Processors](processing.md) by calling `Mutate()` and `Resize()` +## Read Image Information Without Decoding Pixels -### Identify image +If you only need image dimensions, pixel information, or metadata, use `Image.Identify()` instead of `Image.Load()`: -If you are only interested in the image dimensions or metadata of the image, you can achieve this with `Image.Identify`. -This will avoid decoding the complete image and therfore be much faster. +```csharp +using SixLabors.ImageSharp; -For example: +ImageInfo imageInfo = Image.Identify("input.jpg"); -```c# -ImageInfo imageInfo = Image.Identify(@"image.jpg"); Console.WriteLine($"Width: {imageInfo.Width}"); Console.WriteLine($"Height: {imageInfo.Height}"); +Console.WriteLine($"Frames: {imageInfo.FrameCount}"); ``` -### Image metadata +This is usually much faster and allocates less memory because the full pixel buffer is never materialized. -To retrieve image metadata, either load an image with `Image.Load` or use `Image.Identify` (this will not decode the complete image, just the metadata). In both cases you will get the image dimensions and additional the the image -metadata in the `Metadata` property. +## Create a New Image -This will contain the following profiles, if present in the image: +You can also create images directly in memory: -- ExifProfile -- IccProfile -- IptcProfile -- XmpProfile +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; -##### Format specific metadata +using Image image = new(640, 480, Color.White); +``` -Further there are format specific metadata, which can be obtained for example like this: +Use `Image` when the pixel format matters to your workflow, for example when you need direct access to pixel rows or want to interoperate with a known buffer format. -```c# -Image image = Image.Load(@"image.jpg"); -ImageMetadata imageMetaData = image.Metadata; +## Mutate or Clone? -// Syntactic sugar for imageMetaData.GetFormatMetadata(JpegFormat.Instance) -JpegMetadata jpegData = imageMetaData.GetJpegMetadata(); -``` +ImageSharp exposes two primary processing entry points: -And similar for the other supported formats. +- `Mutate()` changes the current image in place. +- `Clone()` creates a deep copy and applies the processors to that copy. -### Initializing New Images +Use `Mutate()` when you want to transform the current image, and `Clone()` when you need to keep the original unchanged. -```c# +```csharp using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -int width = 640; -int height = 480; - -// Creates a new image with empty pixel data. -using(Image image = new(width, height)) -{ - // Do your drawing in here... - -} // Dispose - releasing memory into a memory pool ready for the next image you wish to process. +using Image image = Image.Load("input.jpg"); +using Image thumbnail = image.Clone(x => x.Resize(160, 160)); ``` -In this example you are utilizing the following core ImageSharp feature: -- [Pixel Formats](pixelformats.md) by using `Rgba32` - -### API Cornerstones -The easiest way to work with ImageSharp is to utilize our extension methods: -- @"SixLabors.ImageSharp" for basic operations and primitives. -- @"SixLabors.ImageSharp.Processing" for `Mutate()` and `Clone()`. All the processing extensions (eg. `Resize(...)`) live within this namespace. -### Performance -Achieving near-to-native performance is a major goal for the SixLabors team, and thanks to the improvements brought by the RyuJIT runtime, it's no longer mission impossible. We have made great progress and are constantly working on improvements. +## Dispose Images Promptly -At the moment it's pretty hard to define fair benchmarks comparing GDI+ (aka. `System.Drawing` on Windows) and ImageSharp, because of the differences between the algorithms being used. Generally speaking, we are faster and more feature rich, producing better quality output. +ImageSharp images own pooled memory buffers and should be disposed as soon as you are done with them. Prefer `using` declarations or `using` blocks around `Image` and `Image` instances. -If you are experiencing a significant performance gap between System.Drawing and ImageSharp for basic use-cases, there is a high chance that essential SIMD optimizations are not utilized. +See [Memory Management](memorymanagement.md) for production guidance around pooling, contiguous buffers, and diagnostics. -A few troubleshooting steps to try: - -- Check the value of [Vector.IsHardwareAccelerated](https://docs.microsoft.com/en-us/dotnet/api/system.numerics.vector.ishardwareaccelerated?view=netcore-2.1&viewFallbackFrom=netstandard-2.0#System_Numerics_Vector_IsHardwareAccelerated). If the output is false, it means there is no SIMD support in your runtime! - -#### MAUI Performance -ImageSharp performs well with MAUI on both iOS and Android in release mode when correctly configured. For Android we recommend enabling LLVM and AOT compilation in the project file: - -```xml - - true - true - false - -``` +## Next Steps ->[!NOTE] ->Android performance in Debug mode appears to be significantly slower than in Release mode, this is not due to issues within the library itself rather upstream issues in the .NET Runtime. The following [.NET Runtime issue](https://github.com/dotnet/runtime/issues/71210) contains more information. +- [Loading, Identifying, and Saving](loadingandsaving.md) +- [Working with Metadata](metadata.md) +- [Processing Images](processing.md) +- [Pixel Formats](pixelformats.md) +- [Working with Pixel Buffers](pixelbuffers.md) diff --git a/articles/imagesharp/gif.md b/articles/imagesharp/gif.md new file mode 100644 index 000000000..0d54c7bbd --- /dev/null +++ b/articles/imagesharp/gif.md @@ -0,0 +1,132 @@ +# GIF and Animation + +GIF is a palette-based format commonly used for simple animations. In ImageSharp, GIF encoding is built on a quantizing animated encoder, which means palette generation and frame metadata are both important parts of the workflow. + +## Format Characteristics + +GIF is fundamentally a palette format. Each frame is limited to indexed colors rather than storing full true-color pixel data, which is why quantization and palette choice matter so much. + +A few practical implications: + +- GIF is well known and widely compatible for simple animations. +- GIF is limited compared to modern formats for photographic or high-color imagery. +- GIF transparency is palette/index based rather than full alpha blending. +- GIF is usually chosen for compatibility or simplicity rather than compression efficiency. + +## Save as GIF + +Use [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) when you want to control GIF output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.PixelFormats; + +using Image gif = new(120, 120, Color.Black); + +gif.Metadata.GetGifMetadata().RepeatCount = 0; +gif.Frames.RootFrame.Metadata.GetGifMetadata().FrameDelay = 10; + +using Image frame = new(120, 120, Color.Orange); +frame.Frames.RootFrame.Metadata.GetGifMetadata().FrameDelay = 10; + +gif.Frames.AddFrame(frame.Frames.RootFrame); + +gif.Save("output.gif", new GifEncoder +{ + ColorTableMode = FrameColorTableMode.Global +}); +``` + +## Key GIF Encoder Options + +The main `GifEncoder` option is `ColorTableMode`, which controls whether frames use a shared global palette or per-frame local palettes. + +Because `GifEncoder` inherits from [`QuantizingAnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingAnimatedImageEncoder), it also supports: + +- `RepeatCount` +- `BackgroundColor` +- `AnimateRootFrame` +- `Quantizer` +- `PixelSamplingStrategy` +- `TransparentColorMode` + +## Quantization and Palette Control + +Every GIF encode in ImageSharp is a quantization step, because GIF stores indexed palette entries rather than full true-color pixels. If you do nothing, ImageSharp will still build a palette for you, but for gradients, photographic frames, UI art, or brand colors it is often worth controlling the quantizer explicitly. + +The main knobs are: + +- [`Quantizer`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.Quantizer) to choose the palette-generation algorithm. +- [`PixelSamplingStrategy`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.PixelSamplingStrategy) to control how source pixels are sampled while building the palette. +- [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder.TransparentColorMode) to control how transparent pixels are treated during quantization. + +Common choices include: + +- [`OctreeQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.OctreeQuantizer) for a solid general-purpose adaptive palette. +- [`WuQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.WuQuantizer) when you want a high-quality adaptive palette with configurable [`QuantizerOptions`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.QuantizerOptions). +- [`PaletteQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.PaletteQuantizer) when you need to lock output to a known palette. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +using Image image = Image.Load("input.gif"); + +image.Save("output.gif", new GifEncoder +{ + ColorTableMode = FrameColorTableMode.Global, + Quantizer = new WuQuantizer(new QuantizerOptions + { + MaxColors = 128, + Dither = null, + TransparentColorMode = TransparentColorMode.Preserve + }) +}); +``` + +Reducing `MaxColors` can shrink files, but it also makes banding and contouring more likely. Dithering can hide some of that, at the cost of more visible texture. + +## GIF Metadata + +Use `GetGifMetadata()` to inspect or modify GIF-level metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Gif; + +using Image image = Image.Load("input.gif"); + +GifMetadata gifMetadata = image.Metadata.GetGifMetadata(); +``` + +`GifMetadata` includes values such as `RepeatCount`, `ColorTableMode`, and the global color table. + +Per-frame metadata is available through [`GifFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata), including: + +- `FrameDelay` +- `DisposalMode` +- `ColorTableMode` +- `HasTransparency` +- `TransparencyIndex` + +## GIF Tradeoffs + +GIF is best suited to simple animation and palette-based content. It is usually not the best fit for photographic imagery because the format is palette-constrained and heavily depends on quantization. + +GIF is usually a good fit when: + +- You need a simple looping animation format with broad legacy support. +- The content uses relatively few colors. +- You are comfortable with palette-based tradeoffs. + +GIF is usually a poor fit when: + +- The animation contains gradients, photos, or subtle color transitions. +- You want efficient compression. +- You need modern transparency behavior. + +For a step-by-step recipe, see [Create an animated GIF](animatedgif.md). For a more modern animated format, see [WebP](webp.md). diff --git a/articles/imagesharp/identify.md b/articles/imagesharp/identify.md new file mode 100644 index 000000000..fcd22d2f9 --- /dev/null +++ b/articles/imagesharp/identify.md @@ -0,0 +1,78 @@ +# Read Image Info Without Decoding + +Use `Image.Identify()` and `Image.DetectFormat()` when you need to inspect an image without fully decoding the pixel data. This is useful for upload validation, metadata extraction, and planning later processing work. + +## Read Dimensions, Frame Count, and Pixel Info + +`Image.Identify()` returns an [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) with dimensions, frame count, pixel type, and metadata: + +```csharp +using SixLabors.ImageSharp; + +ImageInfo imageInfo = Image.Identify("input.webp"); + +Console.WriteLine($"{imageInfo.Width}x{imageInfo.Height}"); +Console.WriteLine($"Frames: {imageInfo.FrameCount}"); +Console.WriteLine($"Bits per pixel: {imageInfo.PixelType.BitsPerPixel}"); +``` + +## Inspect the Encoded Pixel Type + +[`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) gives you the encoded pixel characteristics reported by the format metadata. This is more than a single bit-depth number. [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo) can tell you whether the source is indexed, grayscale, RGB, alpha-bearing, or higher precision: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +ImageInfo imageInfo = Image.Identify("input.tiff"); +PixelTypeInfo pixelType = imageInfo.PixelType; + +Console.WriteLine($"Bits per pixel: {pixelType.BitsPerPixel}"); +Console.WriteLine($"Color type: {pixelType.ColorType}"); +Console.WriteLine($"Alpha: {pixelType.AlphaRepresentation}"); + +if (pixelType.ComponentInfo is { } componentInfo) +{ + Console.WriteLine($"Components: {componentInfo.ComponentCount}"); + Console.WriteLine($"Max component precision: {componentInfo.GetMaximumComponentPrecision()}"); +} +``` + +This is especially useful before format conversion, because the same pixel-type information is used by ImageSharp's format-bridging metadata to choose the best destination encoding options the target format can support. + +## Detect the Encoded Format + +If you specifically want to know what encoded format a file contains, use `Image.DetectFormat()`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +IImageFormat format = Image.DetectFormat("input.bin"); + +Console.WriteLine(format.Name); +``` + +This is useful when file extensions are missing or untrustworthy. + +## Use Async APIs + +For asynchronous workflows, use `IdentifyAsync()`: + +```csharp +using SixLabors.ImageSharp; + +ImageInfo imageInfo = await Image.IdentifyAsync("input.webp"); + +Console.WriteLine(imageInfo.Width); +Console.WriteLine(imageInfo.Height); +``` + +## Notes + +- `Image.Identify()` is usually much cheaper than `Image.Load()` for inspection-only workflows. +- `ImageInfo.Metadata` still gives you access to metadata without allocating a full pixel buffer. +- `ImageInfo.PixelType` includes color model, alpha behavior, bit depth, and component precision without decoding the full image. +- `Image.DetectFormat()` is focused on encoded format detection, while `Image.Identify()` returns the broader inspection result. + +For more detail, see [Loading, Identifying, and Saving](loadingandsaving.md), [Working with Metadata](metadata.md), [Convert Between Formats](formatconversion.md), and [Pixel Formats](pixelformats.md). diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index e61f093e7..842541a8a 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -1,82 +1,174 @@ # Image Formats -Out of the box ImageSharp supports the following image formats: +ImageSharp supports a broad set of built-in image formats and is designed so additional formats can be registered through configuration when needed. -- Bmp -- Gif -- Jpeg -- Pbm -- Png -- Tiff -- Tga -- WebP -- Qoi +## Built-In Formats -ImageSharp's API however, is designed to support extension by the registration of additional [`IImageFormat`](xref:SixLabors.ImageSharp.Formats.IImageFormat) implementations. +The source of truth for the built-in format list is [`Configuration`](xref:SixLabors.ImageSharp.Configuration): the default ImageSharp configuration preregisters encoder, decoder, and detector modules for the following public [`IImageFormat`](xref:SixLabors.ImageSharp.Formats.IImageFormat) types: -### Loading and Saving Specific Image Formats +| Format | Public API type | Built in by default | +| --- | --- | --- | +| BMP | [`BmpFormat`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpFormat) | Read and write | +| CUR | [`CurFormat`](xref:SixLabors.ImageSharp.Formats.Cur.CurFormat) | Read and write | +| GIF | [`GifFormat`](xref:SixLabors.ImageSharp.Formats.Gif.GifFormat) | Read and write | +| ICO | [`IcoFormat`](xref:SixLabors.ImageSharp.Formats.Ico.IcoFormat) | Read and write | +| JPEG | [`JpegFormat`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegFormat) | Read and write | +| PBM | [`PbmFormat`](xref:SixLabors.ImageSharp.Formats.Pbm.PbmFormat) | Read and write | +| PNG | [`PngFormat`](xref:SixLabors.ImageSharp.Formats.Png.PngFormat) | Read and write | +| QOI | [`QoiFormat`](xref:SixLabors.ImageSharp.Formats.Qoi.QoiFormat) | Read and write | +| TGA | [`TgaFormat`](xref:SixLabors.ImageSharp.Formats.Tga.TgaFormat) | Read and write | +| TIFF | [`TiffFormat`](xref:SixLabors.ImageSharp.Formats.Tiff.TiffFormat) | Read and write | +| WebP | [`WebpFormat`](xref:SixLabors.ImageSharp.Formats.Webp.WebpFormat) | Read and write | -[`Image`](xref:SixLabors.ImageSharp.Image`1) represents raw pixel data, stored in a contiguous memory block. It does not "remember" the original image format. +ICO and CUR are distinct built-in formats even though detection is handled by a shared icon detector internally. -ImageSharp identifies image formats (Jpeg, Png, Gif etc.) by [`IImageFormat`](xref:SixLabors.ImageSharp.Formats.IImageFormat) instances. Decoded images store the format in the [DecodedImageFormat](xref:SixLabors.ImageSharp.Metadata.ImageMetadata.DecodedImageFormat) within the image metadata. It is possible to pass that value to `image.Save` after performing the operation: +## At a Glance -```C# -using (var image = Image.Load(inputStream)) +If you only need a quick rule of thumb: + +- JPEG is the usual choice for photos when small files matter and transparency does not. +- PNG is the usual choice for lossless graphics, screenshots, and transparency. +- GIF is mainly useful for simple palette-based animation and legacy compatibility. +- WebP covers lossy, lossless, transparency, and animation in one format family. +- TIFF is primarily for archival, print, interchange, and imaging-pipeline workflows. + +Another way to think about it: + +- Lossy formats: JPEG, lossy WebP. +- Lossless formats: PNG, lossless WebP, TIFF, QOI, BMP. +- Transparency-friendly formats: PNG, WebP, TIFF, TGA, QOI. +- Animation-friendly formats: GIF, animated PNG workflows through [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder), and animated WebP. + +No single format is best everywhere. The right choice depends on whether your priority is fidelity, file size, transparency, animation, compatibility, or workflow metadata. + +## Load, Detect, and Preserve Formats + +[`Image`](xref:SixLabors.ImageSharp.Image`1) represents decoded pixel data. Once an image is loaded into memory, it is no longer tied to a specific file format unless you explicitly inspect or preserve that information. + +ImageSharp can detect the encoded format of a source before loading it: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +IImageFormat format = Image.DetectFormat("input.bin"); + +Console.WriteLine(format.Name); +``` + +Decoded images also keep the original format in [`ImageMetadata.DecodedImageFormat`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata.DecodedImageFormat). + +That metadata is useful when you want to explicitly save back to the originally decoded format, especially when writing to a stream where there is no file extension to select an encoder for you: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Resize(1200, 800)); + +if (image.Metadata.DecodedImageFormat is not null) { - image.Mutate(c => c.Resize(30, 30)); + using FileStream outputStream = File.Create("output.jpg"); image.Save(outputStream, image.Metadata.DecodedImageFormat); } ``` -> [!NOTE] -> ImageSharp provides common extension methods to save an image into a stream using a specific format. +When you save by path, `image.Save("output.jpg")` or `image.Save("output.png")` selects the encoder from the destination file extension. + +You can also choose a format explicitly by passing an encoder or by using the `SaveAs...()` helpers. + +## Save with Explicit Encoders + +[`ImageEncoder`](xref:SixLabors.ImageSharp.Formats.ImageEncoder) implementations are lightweight configuration objects. Create one when you want to control how a format is written: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; + +using Image image = Image.Load("input.png"); + +image.Save("output.jpg", new JpegEncoder { Quality = 85 }); +image.Save("output.png", new PngEncoder()); +``` + +ImageSharp also provides format-specific helpers: - `image.SaveAsBmp()` (shortcut for `image.Save(new BmpEncoder())`) +- `image.SaveAsCur()` (shortcut for `image.Save(new CurEncoder())`) - `image.SaveAsGif()` (shortcut for `image.Save(new GifEncoder())`) +- `image.SaveAsIco()` (shortcut for `image.Save(new IcoEncoder())`) - `image.SaveAsJpeg()` (shortcut for `image.Save(new JpegEncoder())`) - `image.SaveAsPbm()` (shortcut for `image.Save(new PbmEncoder())`) - `image.SaveAsPng()` (shortcut for `image.Save(new PngEncoder())`) +- `image.SaveAsQoi()` (shortcut for `image.Save(new QoiEncoder())`) - `image.SaveAsTga()` (shortcut for `image.Save(new TgaEncoder())`) - `image.SaveAsTiff()` (shortcut for `image.Save(new TiffEncoder())`) - `image.SaveAsWebp()` (shortcut for `image.Save(new WebpEncoder())`) -- `image.SaveAsQoi()` (shortcut for `image.Save(new QoiEncoder())`) -### A Deeper Overview of ImageSharp Format Management +## General Decoder Options -Real life image streams are usually stored / transferred in standardized formats like Jpeg, Png, Bmp, Gif etc. An image format is represented by an [`IImageFormat`](xref:SixLabors.ImageSharp.Formats.IImageFormat) implementation. +Use [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) with the general `Load()` APIs when you want to control metadata handling, frame limits, or decode-to-size behavior: -- [`ImageDecoder`](xref:SixLabors.ImageSharp.Formats.ImageDecoder) is responsible for decoding streams (and files) in into [`Image`](xref:SixLabors.ImageSharp.Image`1). ImageSharp can **auto-detect** the image formats of streams/files based on their headers, selecting the correct [`IImageFormat`](xref:SixLabors.ImageSharp.Formats.IImageFormat) (and thus [`ImageDecoder`](xref:SixLabors.ImageSharp.Formats.ImageDecoder)). This logic is implemented by [`IImageFormatDetector`](xref:SixLabors.ImageSharp.Formats.IImageFormatDetector)'s. -- [`ImageEncoder`](xref:SixLabors.ImageSharp.Formats.ImageEncoder) is responsible for writing [`Image`](xref:SixLabors.ImageSharp.Image`1) into a stream using a given format. -- Decoders/encoders and [`IImageFormatDetector`](xref:SixLabors.ImageSharp.Formats.IImageFormatDetector)'s are mapped to image formats in [`ImageFormatsManager`](xref:SixLabors.ImageSharp.Configuration.ImageFormatsManager). It's possible to register new formats, or drop existing ones. See [Configuration](configuration.md) for more details. +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; -### Working with Decoders +DecoderOptions options = new() +{ + MaxFrames = 1, + SkipMetadata = false, + TargetSize = new Size(1600, 1600) +}; -The behavior of the various decoders during the decoding process can be controlled by passing [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) instances to our general `Load` APIs. These options contain means to control metadata handling, the decoded frame count, and properties to allow directly decoding the encoded image to a given target size. +using Image image = Image.Load(options, "input.webp"); +``` -### Specialized Decoding +Format-specific decoder option types also exist for specialized scenarios such as JPEG and PNG. -In addition to the general decoding API we offer additional specialized decoding options [`ISpecializedDecoderOptions`](xref:SixLabors.ImageSharp.Formats.ISpecializedDecoderOptions) that can be accessed directly against [`ISpecializedDecoder`](xref:SixLabors.ImageSharp.Formats.ISpecializedImageDecoder`1) instances which provide further options for decoding. +## Common Encoder Families -### Metadata-only Decoding +Several formats share useful option sets through common encoder base types: -Sometimes it's worth to efficiently decode image metadata ignoring the memory and CPU heavy pixel information inside the stream. ImageSharp allows this by using one of the several [Image.Identify](xref:SixLabors.ImageSharp.Image) overloads: +- [`ImageEncoder`](xref:SixLabors.ImageSharp.Formats.ImageEncoder) exposes `SkipMetadata`. +- [`AlphaAwareImageEncoder`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder) adds `TransparentColorMode`. +- [`QuantizingImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder) adds `Quantizer` and `PixelSamplingStrategy`. +- [`AnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder) adds `RepeatCount`, `BackgroundColor`, and `AnimateRootFrame`. -```C# -ImageInfo imageInfo = Image.Identify(inputStream); -Console.WriteLine($"{imageInfo.Width}x{imageInfo.Height} | BPP: {imageInfo.PixelType.BitsPerPixel}"); -``` +Those inherited options are especially useful when working with GIF, APNG, and animated WebP. +For a format-agnostic guide to palettes and dithered output, see [Quantization, Palettes, and Dithering](quantization.md). + +## Format Guides + +Use the format-specific guides for the common cases: -See [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) for more details about the identification result. +- [JPEG](jpeg.md) for photographic output and quality-focused lossy compression. +- [PNG](png.md) for lossless output, transparency, and APNG metadata. +- [GIF and Animation](gif.md) for palette-based animation workflows. +- [WebP](webp.md) for lossy, lossless, transparent, and animated WebP output. +- [TIFF](tiff.md) for workflows where compression mode, pixel layout, and TIFF metadata matter. -### Working with Encoders +The less commonly used built-in formats still have valid niches: -Image formats are usually defined by complex standards allowing multiple representations for the same image. ImageSharp allows parameterizing the encoding process: -[`ImageEncoder`](xref:SixLabors.ImageSharp.Formats.ImageEncoder) implementations are stateless, lightweight **parametric** objects. This means that if you want to encode a Png in a specific way (eg. changing the compression level), you need to new-up a custom [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder) instance. +- BMP is simple and broadly understood, but usually much larger than modern alternatives. +- ICO stores Windows icon files, often with multiple embedded image sizes. +- CUR stores Windows cursor files and hotspot metadata. +- PBM is useful for Netpbm-family workflows and simple interchange scenarios. +- TGA appears most often in graphics and content-pipeline tooling. +- QOI is a fast, simple lossless format with a much smaller ecosystem than PNG or WebP. -Choosing the right encoder parameters allows to balance between conflicting tradeoffs: +## Custom Format Registration + +Format detectors, decoders, and encoders are registered through ImageSharp configuration. See [Configuration](configuration.md) if you need to customize the set of supported formats for your application. + +## Choosing the Right Encoder + +The right encoder settings depend on the tradeoff you want to make between: - Image file size - Encoder speed - Image quality - -Each encoder offers options specific to the image format it represents. + +The format-specific pages below are the best place to start when you need to tune those tradeoffs. diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index a39048f1f..2ff4ec43e 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -1,19 +1,10 @@ -# Introduction +# ImageSharp -### What is ImageSharp? -ImageSharp is a modern, fully featured, fully managed, cross-platform, 2D graphics library. -Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API. +ImageSharp is a fully managed, cross-platform 2D graphics library for .NET. It provides a format-agnostic in-memory image model, a rich processing pipeline, flexible encoders and decoders, and low-level pixel APIs for advanced workloads. -ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations. +## Install ImageSharp -Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview), ImageSharp can be used in device, cloud, and embedded/IoT scenarios. - -### License -ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. - -### Installation - -ImageSharp is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp) with nightly builds available on [MyGet](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.ImageSharp). +ImageSharp is distributed on [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp) with preview and nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) @@ -44,13 +35,25 @@ paket add SixLabors.ImageSharp --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. -### Implicit Usings +## Start Here -The `UseImageSharp` property controls whether **implicit `global using` directives** for ImageSharp are included in your C# project. This feature is available in projects targeting **.NET 6 or later** with **C# 10 or later**. +- [Getting Started](gettingstarted.md) walks through the core image types and the first end-to-end processing workflow. +- [Loading, Identifying, and Saving](loadingandsaving.md) covers file, stream, and buffer-based APIs plus encoder selection. +- [Working with Metadata](metadata.md) explains how to read and preserve EXIF, ICC, IPTC, XMP, and format-specific metadata. +- [Color Profiles and Color Conversion](colorprofiles.md) covers ICC and CICP metadata, decode-time profile handling, and explicit working-space conversion. +- [Image Formats](imageformats.md) explains format detection, encoders, decoders, and format registration. +- [Processing Images](processing.md) introduces `Mutate()` and `Clone()` pipelines. +- [Quantization, Palettes, and Dithering](quantization.md) explains `Quantize()`, palette-based encoders, and dithering tradeoffs. +- [Pixel Formats](pixelformats.md) and [Working with Pixel Buffers](pixelbuffers.md) cover direct pixel access and advanced processing. +- [Interop and Raw Memory](interop.md) covers `LoadPixelData(...)`, `WrapMemory(...)`, and contiguous-buffer interop. +- [Configuration](configuration.md), [Memory Management](memorymanagement.md), and [Security Considerations](security.md) cover production-focused setup. +- [Troubleshooting](troubleshooting.md) covers the common failure modes around format detection, streams, memory, and disposal. +- [Migrating from System.Drawing](migratingfromsystemdrawing.md) maps common GDI-style workflows to ImageSharp APIs. +- [Recipes](recipes.md) provides copy-pasteable solutions for common tasks. -When enabled, a predefined set of `global using` directives for common ImageSharp namespaces (such as `SixLabors.ImageSharp`, `SixLabors.ImageSharp.Processing`, etc.) is automatically added to the compilation. This eliminates the need to manually add `using` statements in every file. +## Implicit Usings -To enable implicit ImageSharp usings, set the property in your project file: +Set `UseImageSharp` in your project file to automatically import the most common ImageSharp namespaces: ```xml @@ -58,11 +61,17 @@ To enable implicit ImageSharp usings, set the property in your project file: ``` -To disable the feature, either remove the property or set it to `false`: +When enabled, ImageSharp adds implicit `global using` directives for: -```xml - - false - -``` +- `SixLabors.ImageSharp` +- `SixLabors.ImageSharp.PixelFormats` +- `SixLabors.ImageSharp.Processing` + +You can turn this off by removing the property or setting it to `false`. + +## License + +ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. +>[!IMPORTANT] +>Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. diff --git a/articles/imagesharp/interop.md b/articles/imagesharp/interop.md new file mode 100644 index 000000000..25d73bd69 --- /dev/null +++ b/articles/imagesharp/interop.md @@ -0,0 +1,216 @@ +# Interop and Raw Memory + +ImageSharp supports both copy-based and zero-copy workflows for raw pixel data. Which API you choose depends on who owns the memory and whether you need ImageSharp to keep its own copy. + +## Choose the Right API + +| Need | API | Copies pixel data? | Who owns the memory? | +|---|---|---|---| +| Import raw pixels into a normal ImageSharp-owned image | `Image.LoadPixelData(...)` | Yes | ImageSharp | +| Export pixels from an image | `CopyPixelDataTo(...)` | Yes | Caller | +| Wrap existing managed memory | `Image.WrapMemory(...)` with `Memory` or `Memory` | No | Caller | +| Wrap an owned buffer and transfer disposal to ImageSharp | `Image.WrapMemory(...)` with `IMemoryOwner` or `IMemoryOwner` | No | ImageSharp | +| Wrap unmanaged or pinned memory | `Image.WrapMemory(...)` pointer overloads | No | Caller | + +## `WrapMemory(...)` Creates a View, Not a Copy + +[`Image.WrapMemory(...)`](xref:SixLabors.ImageSharp.Image.WrapMemory*) does not decode, convert, or clone the source pixels. It creates an [`Image`](xref:SixLabors.ImageSharp.Image`1) view over memory you already have. + +That makes it ideal for zero-copy interop, but it also means: + +- the wrapped memory must already match the chosen `TPixel` layout; +- the source buffer lifetime rules still matter; +- the image is tied to the shape and stride of that existing buffer. + +## Import Raw Pixels with `LoadPixelData(...)` + +Use `LoadPixelData(...)` when you want ImageSharp to create a normal owned image from existing pixels: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +byte[] rgba = GetRgbaBytes(); +using Image image = Image.LoadPixelData(rgba, width, height); +``` + +There are overloads for: + +- `ReadOnlySpan` +- `ReadOnlySpan` +- stride-aware pixel input +- stride-aware byte input + +This is the safest choice when you do not need zero-copy behavior. + +## Export Raw Pixels with `CopyPixelDataTo(...)` + +Use `CopyPixelDataTo(...)` when you want a flattened copy of the root frame pixels: + +```csharp +using SixLabors.ImageSharp.PixelFormats; + +Rgba32[] pixels = new Rgba32[image.Width * image.Height]; +image.CopyPixelDataTo(pixels); +``` + +There is also a `Span` overload if you need raw bytes instead of `TPixel` values. + +If you need frame-specific access, the same API is available on [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame`1). + +## Wrap Existing Managed Memory Without Copying + +Use `Image.WrapMemory(...)` when you already have raw memory and want ImageSharp to view it in place: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +byte[] bgra = GetBgraBytes(); +using Image image = Image.WrapMemory(bgra, width, height, rowStrideInBytes); +``` + +Important ownership rule: + +- If you pass [`Memory`](xref:System.Memory`1) or `Memory`, ownership stays with you. +- The underlying buffer must remain valid for the entire lifetime of the image. + +That makes `WrapMemory(...)` a good fit for shared buffers, pinned arrays, and memory you already control. + +All `WrapMemory(...)` families also have overloads that accept [`Configuration`](xref:SixLabors.ImageSharp.Configuration) and [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata), so you can attach metadata or use a non-default configuration while still keeping the zero-copy behavior. + +## Choose the Right `WrapMemory(...)` Overload + +Within the `WrapMemory(...)` family, the main choice is what kind of source memory you have: + +- use `Memory` when you already have typed pixel data; +- use `Memory` when the source buffer is raw bytes in a known `TPixel` layout; +- use `IMemoryOwner` or `IMemoryOwner` when you want the wrapped image to take ownership and dispose the backing owner; +- use pointer overloads only for unmanaged or pinned memory that cannot be expressed more safely as `Memory` or `Memory`. + +If the source buffer has row padding, use the stride-aware overload: + +- `rowStride` for typed pixel memory; +- `rowStrideInBytes` for byte or pointer memory. + +## Transfer Ownership with `IMemoryOwner` + +If you want ImageSharp to dispose the wrapped buffer together with the image, use an [`IMemoryOwner`](xref:System.Buffers.IMemoryOwner`1) overload: + +```csharp +using System.Buffers; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +IMemoryOwner owner = MemoryPool.Shared.Rent(bufferSize); +using Image image = Image.WrapMemory(owner, width, height, rowStrideInBytes); +``` + +In that form, the ownership of `owner` is transferred to the image. Do not dispose it yourself after wrapping. + +## Packed vs Strided Wrapped Buffers + +Wrapped buffers can be either tightly packed or strided. + +A packed wrapper uses one logical row immediately after the previous one. A strided wrapper uses extra elements or bytes between row starts, which is common when working with foreign image APIs. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +Rgba32[] source = new Rgba32[8]; + +using Image image = Image.WrapMemory( + source.AsMemory(), + width: 3, + height: 2, + rowStride: 4); + +bool contiguous = image.DangerousTryGetSinglePixelMemory(out _); // false +``` + +Important consequences of a strided wrapper: + +- [`DangerousTryGetSinglePixelMemory(...)`](xref:SixLabors.ImageSharp.Image`1.DangerousTryGetSinglePixelMemory*) will usually return `false`; +- [`CopyPixelDataTo(...)`](xref:SixLabors.ImageSharp.Image`1.CopyPixelDataTo*) uses the backing row layout, so destination length must account for stride, not only `width * height`; +- row padding belongs to the wrapped view contract, so make sure the caller and callee agree on it. + +## Work with Native or Pinned Memory + +`Image.WrapMemory(...)` also has pointer overloads for unmanaged or manually pinned buffers. Those overloads are intended for advanced interop scenarios where you already have a stable pointer and buffer length. + +Use them carefully: + +- The pointer must remain valid for the full lifetime of the wrapped image. +- The buffer size and row stride must match the image dimensions. +- If you have `Memory` or `Memory`, prefer those overloads instead because they are much easier to reason about safely. + +## Wrapped Images Are Best for Fixed-Size Work + +`WrapMemory(...)` is best when you want ImageSharp to operate on an existing fixed-size buffer. + +That means in-place pixel work, analysis, format conversion, and encode/decode bridges are good fits. Operations that need to replace the backing buffer, especially dimension-changing processors like `Resize()`, are not a good fit for a wrapped image and may throw. + +If you need to resize, crop into a new image, pad, or otherwise move into a normal ImageSharp-owned lifecycle, clone first: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image wrapped = Image.WrapMemory(bgra, width, height, rowStrideInBytes); +using Image owned = wrapped.CloneAs(); + +owned.Mutate(x => x.Resize(width / 2, height / 2)); +``` + +## Get a Contiguous Buffer from an ImageSharp Image + +If you need to hand ImageSharp-owned pixels to native code, ask for contiguous allocation up front and then call `DangerousTryGetSinglePixelMemory(...)`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +Configuration config = Configuration.Default.Clone(); +config.PreferContiguousImageBuffers = true; + +using Image image = new(config, width, height); + +if (!image.DangerousTryGetSinglePixelMemory(out Memory pixels)) +{ + throw new InvalidOperationException("The image is not backed by one contiguous buffer."); +} +``` + +From there, you can pin the returned [`Memory`](xref:System.Memory`1) if your native API requires an address. Keep the image alive for the full duration of that native access. + +## Stride Matters + +Several interop APIs take a row stride: + +- `rowStride` for pixel-count-based overloads +- `rowStrideInBytes` for byte-count-based overloads + +Use the stride-aware overloads whenever your source buffer contains padding between rows. Do not assume every foreign buffer is tightly packed. + +## Make a Normal Owned Copy When Needed + +If you wrapped foreign memory only as a temporary bridge, you can switch back to a normal ImageSharp-owned image with `Clone()` or `CloneAs()`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image wrapped = Image.WrapMemory(bgra, width, height, rowStrideInBytes); +using Image owned = wrapped.CloneAs(); +``` + +That is often the right move if the wrapped buffer has awkward lifetime rules, if you want a different working pixel format, or if the next processing steps may need a different backing buffer shape. + +## Related Topics + +- [Working with Pixel Buffers](pixelbuffers.md) +- [Memory Management](memorymanagement.md) +- [Troubleshooting](troubleshooting.md) +- [Migrating from System.Drawing](migratingfromsystemdrawing.md) diff --git a/articles/imagesharp/jpeg.md b/articles/imagesharp/jpeg.md new file mode 100644 index 000000000..de4b9d49d --- /dev/null +++ b/articles/imagesharp/jpeg.md @@ -0,0 +1,78 @@ +# JPEG + +JPEG is typically the right choice for photographic images and other continuous-tone content where smaller file sizes matter more than lossless preservation. + +## Format Characteristics + +JPEG uses lossy compression. That means it reduces file size by permanently discarding some image information, which is usually acceptable for photos but much more noticeable on sharp edges, text, UI assets, or repeated save cycles. + +A few practical implications: + +- JPEG is usually excellent for photos and gradients. +- JPEG is usually poor for logos, diagrams, screenshots, and pixel-precise artwork. +- JPEG does not support alpha transparency. +- Re-encoding a JPEG repeatedly can compound visible artifacts over time. + +## Save as JPEG + +Use [`JpegEncoder`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder) when you want to tune JPEG output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; + +using Image image = Image.Load("input.png"); + +image.Save("output.jpg", new JpegEncoder +{ + Quality = 85, + Progressive = true +}); +``` + +## Key JPEG Encoder Options + +The most commonly used `JpegEncoder` options are: + +- `Quality` controls the quality/compression tradeoff on a 1-100 scale. +- `Progressive` enables progressive JPEG output. +- `ProgressiveScans` controls how progressive data is split into scans. +- `Interleaved` controls interleaved versus non-interleaved output. +- `ColorType` lets you influence the encoded JPEG color model. + +JPEG is a lossy format and does not preserve alpha transparency. If the source image includes transparency, composite it onto a background first. + +## Read JPEG Metadata + +You can inspect format-specific metadata through `GetJpegMetadata()`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; + +using Image image = Image.Load("input.jpg"); + +JpegMetadata jpegMetadata = image.Metadata.GetJpegMetadata(); +``` + +General image metadata such as EXIF and ICC profiles remains available through [Working with Metadata](metadata.md). + +## JPEG-Specific Decode Options + +ImageSharp also exposes [`JpegDecoderOptions`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegDecoderOptions) for specialized JPEG decoding scenarios, including decoder-specific resize behavior. + +## When to Use JPEG + +JPEG is usually a good fit when: + +- The source is photographic rather than flat-color artwork. +- You do not need transparency. +- A smaller file size is more important than exact pixel preservation. + +JPEG is usually a poor fit when: + +- The image contains text, hard UI edges, or line art. +- You need pixel-perfect reproduction. +- You need an alpha channel. + +If you need lossless output or alpha transparency, start with [PNG](png.md) or [WebP](webp.md) instead. diff --git a/articles/imagesharp/loadingandsaving.md b/articles/imagesharp/loadingandsaving.md new file mode 100644 index 000000000..50b671aec --- /dev/null +++ b/articles/imagesharp/loadingandsaving.md @@ -0,0 +1,141 @@ +# Loading, Identifying, and Saving + +ImageSharp provides a consistent set of APIs for working with image files, streams, and in-memory buffers. The main entry points are `Image.Load()`, `Image.Identify()`, `Image.DetectFormat()`, and the corresponding async APIs. + +## Load Images + +You can load images from a file path, a stream, or an in-memory byte buffer: + +```csharp +using SixLabors.ImageSharp; + +using Image fromFile = Image.Load("input.webp"); + +using FileStream stream = File.OpenRead("input.webp"); +using Image fromStream = Image.Load(stream); + +byte[] buffer = File.ReadAllBytes("input.webp"); +using Image fromBytes = Image.Load(buffer); +``` + +All of these overloads inspect the image data to determine which decoder to use. + +If you know the target pixel format you want in memory, use the generic overloads such as `Image.Load()`. + +## Use Async APIs for I/O-Bound Work + +ImageSharp also exposes async load and save methods for file and stream based workflows: + +```csharp +using SixLabors.ImageSharp; + +await using FileStream input = File.OpenRead("input.png"); +using Image image = await Image.LoadAsync(input); + +await image.SaveAsync("output.webp"); +``` + +Use the async overloads when your application already uses asynchronous I/O, for example in ASP.NET Core or background processing pipelines. + +## Identify Without Decoding Pixel Data + +Use `Image.Identify()` when you only need dimensions, pixel information, or metadata: + +```csharp +using SixLabors.ImageSharp; + +ImageInfo imageInfo = Image.Identify("input.jpg"); + +Console.WriteLine($"{imageInfo.Width}x{imageInfo.Height}"); +Console.WriteLine($"Bits per pixel: {imageInfo.PixelType.BitsPerPixel}"); +Console.WriteLine($"Frames: {imageInfo.FrameCount}"); +``` + +This avoids allocating the full pixel buffer and is usually the right choice for validation, metadata extraction, and thumbnail planning. + +## Detect the Encoded Format + +Use `Image.DetectFormat()` when you need to know what encoded format a source contains before loading it: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +IImageFormat format = Image.DetectFormat("input.bin"); + +Console.WriteLine(format.Name); +``` + +This is useful when files arrive without a trustworthy extension or when you want to route work based on the encoded format. + +## Save Images + +When you save by path, ImageSharp selects an encoder from the file extension: + +```csharp +using SixLabors.ImageSharp; + +using Image image = Image.Load("input.jpg"); + +image.Save("output.png"); +``` + +If you want to preserve the original encoded format after processing, reuse the decoded format stored in metadata: +If you save by path, ImageSharp already chooses the encoder from the destination file extension. Use `DecodedImageFormat` when you want to explicitly save to the originally decoded format, especially when writing to a stream: + +```csharp +using SixLabors.ImageSharp; + +using Image image = Image.Load("input.jpg"); + +if (image.Metadata.DecodedImageFormat is not null) +{ + using FileStream output = File.Create("output.jpg"); + image.Save(output, image.Metadata.DecodedImageFormat); +} +``` + +`DecodedImageFormat` is only populated for images that were decoded from an existing source. Images created from scratch do not have an original encoded format to preserve. + +## Choose Encoders Explicitly + +When you need control over output settings, pass an encoder directly: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; + +using Image image = Image.Load("input.jpg"); + +image.Save("output.jpg", new JpegEncoder { Quality = 85 }); +image.Save("output.png", new PngEncoder()); +``` + +See [Image Formats](imageformats.md) for a deeper look at encoder and decoder behavior. + +## Control Decoding with DecoderOptions + +Use [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) to customize decoding behavior: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +DecoderOptions options = new() +{ + MaxFrames = 1, + SkipMetadata = true, + TargetSize = new Size(1200, 1200) +}; + +using Image image = Image.Load(options, "animated.webp"); +``` + +These options let you limit decoded frames, skip metadata work, or decode directly to a target size when the format supports it. + +## Related Topics + +- [Working with Metadata](metadata.md) +- [Image Formats](imageformats.md) +- [Processing Images](processing.md) diff --git a/articles/imagesharp/memorymanagement.md b/articles/imagesharp/memorymanagement.md index 3526dbb3b..d1189421e 100644 --- a/articles/imagesharp/memorymanagement.md +++ b/articles/imagesharp/memorymanagement.md @@ -1,73 +1,102 @@ # Memory Management -Starting with ImageSharp 2.0, the library uses large (~4MB) discontiguous chunks of unmanaged memory to represent multi-megapixel images. Internally, these buffers are heavily pooled to reduce OS allocation overhead. Unlike in ImageSharp 1.0, the pools are automatically trimmed after a certain amount of allocation inactivity, releasing the buffers to the OS, making the library more suitable for applications that do imaging operations in a periodic manner. +ImageSharp stores image pixels in pooled buffers managed by [`MemoryAllocator`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator). In normal use, you should assume large images are not backed by one giant contiguous span. -The buffer allocation and pooling behavior is implemented by @"SixLabors.ImageSharp.Memory.MemoryAllocator" which is being used through @"SixLabors.ImageSharp.Configuration"'s @"SixLabors.ImageSharp.Configuration.MemoryAllocator" property within the library, therefore it's configurable and replaceable by the user. +That design keeps large-image handling practical, but it also means interop-heavy code should be explicit about when it truly needs contiguous memory. -### Configuring the pool size +## Default Behavior -By default, the maximum pool size is platform-specific, defaulting to a portion of the available physical memory on 64 bit coreclr, and to a 128MB constant size on other platforms / runtimes. +[`Configuration.MemoryAllocator`](xref:SixLabors.ImageSharp.Configuration.MemoryAllocator) defaults to [`MemoryAllocator.Default`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator.Default). For most applications, that default allocator is the right choice. -We highly recommend to go with these defaults, however in certain cases it might be desirable to override the pool limit. In such cases the most straightforward solution is to replace the memory allocator globally: +The ImageSharp source explicitly recommends using a single busy allocator per process. If you customize allocation behavior, prefer doing so by replacing the allocator on a shared configuration rather than creating many short-lived allocators. -```C# -Configuration.Default.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions() +## Customize the Allocator + +If you need tighter control over pool size or allocation limits, create a custom allocator with [`MemoryAllocator.Create(...)`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator.Create*) and [`MemoryAllocatorOptions`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions): + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Memory; + +Configuration config = Configuration.Default.Clone(); +config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { - MaximumPoolSizeMegabytes = 64 + MaximumPoolSizeMegabytes = 128, + AllocationLimitMegabytes = 1024 }); ``` -### Enforcing contiguous buffers +[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow. -Certain interop use cases may require multi-megapixel images to be layed out contiguously in memory so a single buffer pointer can be passed to native API-s. This can be enforced by setting @"SixLabors.ImageSharp.Configuration"'s @"SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers" to `true`. Note that this will lead to significantly reduced pooling that may hurt overall processing throughput. We don't recommend to flip this option globally. Instead, you can enable it locally for the image instances that are expected to be contiguous: +## Prefer Contiguous Buffers Only When You Need Them -```C# -Configuration customConfig = Configuration.Default.Clone(); -customConfig.PreferContiguousImageBuffers = true; +[`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) asks ImageSharp to use contiguous image buffers whenever possible: -using (Image image = new(customConfig, 4096, 4096)) -{ - if (!image.DangerousTryGetSinglePixelMemory(out Memory memory)) - { - throw new Exception( - "This can only happen with multi-GB images or when PreferContiguousImageBuffers is not set to true."); - } - - using (MemoryHandle pinHandle = memory.Pin()) - { - void* ptr = pinHandle.Pointer; - - // You can now pass 'ptr' to native API-s. - // Make sure to keep 'pinHandle', and 'image' alive while native resource work with the pointer. - // Make sure to Dispose() them afterwards. - } -} +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +Configuration config = Configuration.Default.Clone(); +config.PreferContiguousImageBuffers = true; + +using Image image = new(config, 2048, 2048); + +bool contiguous = image.DangerousTryGetSinglePixelMemory(out Memory pixels); ``` -### Wrapping existing buffers as `Image` +This is primarily for interop scenarios. It is not something to enable globally without a reason, because it reduces ImageSharp's flexibility around large pooled allocations and can hurt throughput. + +For more on that workflow, see [Interop and Raw Memory](interop.md). + +## Dispose Images Promptly + +`Image` and `Image` own unmanaged resources. Always dispose them with `using` or `await using` patterns where appropriate. -It's also possible to do the other way around, and wrap an existing native buffer to process it as an `Image`. You can use one of the @"SixLabors.ImageSharp.Image.WrapMemory*" overloads for this. Note that the resulting image is not suitable for operations that would change the dimensions of the image, such an attempt will lead to an @"SixLabors.ImageSharp.Memory.InvalidMemoryOperationException". +ImageSharp includes finalizer-based safety nets, but relying on finalization instead of deterministic disposal can still create avoidable memory pressure and latency. -### Troubleshooting memory leaks +## Track Undisposed Allocations -Strictly speaking, ImageSharp is safe against memory leaks, because finalizers will take care of the native memory resources leaked by omitting `Dispose()` or `using` blocks. However, letting the memory leak to finalizers may lead to performance issues and if GC execution can't keep up with the leaks, to `OutOfMemoryException`. Application code should take care of disposing any @"SixLabors.ImageSharp.Image`1" allocated. +[`MemoryDiagnostics`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics) exposes two useful diagnostics: -In complex and large apps, this might be hard to verify. ImageSharp 2.0+ exposes some code-first diagnostic API-s that may help detecting leaks. +- [`TotalUndisposedAllocationCount`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics.TotalUndisposedAllocationCount) +- [`UndisposedAllocation`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics.UndisposedAllocation) -Query and log @"SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics.TotalUndisposedAllocationCount" to track if the number of undisposed allocations is increasing during your application's lifetime: +```csharp +using SixLabors.ImageSharp.Diagnostics; -```C# -myLogger.Log(@"Number of undisposed ImageSharp buffers: {MemoryDiagnostics.TotalUndisposedAllocationCount}"); +Console.WriteLine(MemoryDiagnostics.TotalUndisposedAllocationCount); ``` -For troubleshooting you can also subscribe to the event @"SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics.UndisposedAllocation". When the event fires, it will report the stack trace of leaking allocations, which may help tracking down bugs. Subscribing to this event has *significant* performance overhead, so avoid it in the final production deployment of your app. +For troubleshooting, you can subscribe to `UndisposedAllocation` to capture allocation stack traces for resources that leaked to the finalizer. That event is intended for diagnostics and carries significant overhead, so it should not stay enabled in normal production traffic. -```C# -#if TROUBLESHOOTING_TESTING_NOT_PRODUCTION -MemoryDiagnostics.UndisposedAllocation += allocationStackTrace => +## Releasing Retained Resources + +If you create a custom allocator and later retire it, dispose all associated images first and then call [`ReleaseRetainedResources()`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator.ReleaseRetainedResources): + +```csharp +using SixLabors.ImageSharp.Memory; + +MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions { - Console.WriteLine($@"Undisposed allocation detected at:{Environment.NewLine}{allocationStackTrace}"); - Environment.Exit(1); -}; -#endif + MaximumPoolSizeMegabytes = 64 +}); + +allocator.ReleaseRetainedResources(); ``` + +That tells the allocator to drop retained pooled buffers that are no longer needed. + +## Practical Guidance + +- Keep [`MemoryAllocator.Default`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator.Default) unless profiling shows a real need to customize it. +- Use one shared allocator per process rather than many temporary allocators. +- Avoid forcing contiguous buffers unless you truly need a single `Memory` or pointer. +- Use [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) and [`DecoderOptions.MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) when you want to limit decode cost up front. +- Track leaked images with [`MemoryDiagnostics`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics) if disposal bugs are suspected. + +## Related Topics + +- [Configuration](configuration.md) +- [Working with Pixel Buffers](pixelbuffers.md) +- [Interop and Raw Memory](interop.md) +- [Troubleshooting](troubleshooting.md) diff --git a/articles/imagesharp/metadata.md b/articles/imagesharp/metadata.md new file mode 100644 index 000000000..51d42c1cb --- /dev/null +++ b/articles/imagesharp/metadata.md @@ -0,0 +1,94 @@ +# Working with Metadata + +ImageSharp exposes image metadata through [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata). You can access it from a fully decoded image through `image.Metadata`, or from [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) when using `Image.Identify()`. + +## Read Metadata from a File + +Use `Image.Identify()` when you only need metadata and dimensions: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Metadata; + +ImageInfo imageInfo = Image.Identify("photo.jpg"); +ImageMetadata metadata = imageInfo.Metadata; + +Console.WriteLine(metadata.DecodedImageFormat?.Name); +Console.WriteLine(metadata.HorizontalResolution); +Console.WriteLine(metadata.VerticalResolution); +``` + +If you are already loading the image for processing, use `image.Metadata` instead. + +## Common Metadata Profiles + +Depending on the source format, `ImageMetadata` can expose several common profiles: + +- `ExifProfile` for camera, orientation, and capture metadata. +- `IccProfile` for embedded color profile data. +- `IptcProfile` for editorial and descriptive metadata. +- `XmpProfile` for extensible structured metadata. +- `CicpProfile` for coding-independent code points metadata when present. + +These profile properties are nullable because not every image carries every kind of metadata. + +## Work with Format-Specific Metadata + +In addition to the common profiles, ImageSharp exposes format-specific metadata helpers: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; + +using Image image = Image.Load("photo.jpg"); + +JpegMetadata jpegMetadata = image.Metadata.GetJpegMetadata(); +PngMetadata pngMetadata = image.Metadata.GetPngMetadata(); +``` + +Similar helpers exist for other built-in formats, including GIF, TIFF, and WebP. + +## Access Frame Metadata + +Multi-frame formats can also expose per-frame metadata: + +```csharp +using SixLabors.ImageSharp; + +ImageInfo imageInfo = Image.Identify("animation.webp"); + +Console.WriteLine($"Frame count: {imageInfo.FrameMetadataCollection.Count}"); +``` + +This is useful when inspecting animated formats without decoding every frame into pixel memory. + +## Strip Metadata Before Saving + +If you do not want to preserve the original metadata, clear the profiles before saving: + +```csharp +using SixLabors.ImageSharp; + +using Image image = Image.Load("photo.jpg"); + +image.Metadata.ExifProfile = null; +image.Metadata.IccProfile = null; +image.Metadata.IptcProfile = null; +image.Metadata.XmpProfile = null; +image.Metadata.CicpProfile = null; + +image.Save("photo-stripped.jpg"); +``` + +This is a common step when reducing file size, removing personal data, or normalizing exported assets. + +## Preserve Metadata Intentionally + +ImageSharp preserves metadata by default when the decoder and encoder both support that metadata. If metadata is important to your workflow, keep these points in mind: + +- `Image.Identify()` lets you inspect metadata without paying for a full decode. +- `DecodedImageFormat` tells you which encoded format was originally loaded. +- Saving to a different format may change which metadata can be represented in the output. + +For deeper guidance on loading and saving workflows, see [Loading, Identifying, and Saving](loadingandsaving.md). For ICC and CICP-specific guidance, see [Color Profiles and Color Conversion](colorprofiles.md). diff --git a/articles/imagesharp/migratingfromsystemdrawing.md b/articles/imagesharp/migratingfromsystemdrawing.md new file mode 100644 index 000000000..0948cb5a3 --- /dev/null +++ b/articles/imagesharp/migratingfromsystemdrawing.md @@ -0,0 +1,124 @@ +# Migrating from System.Drawing + +ImageSharp is not a one-for-one clone of `System.Drawing`, but most common workflows map cleanly once you shift from GDI-style APIs to ImageSharp's generic image and processing model. + +## Core Type Mapping + +| `System.Drawing` concept | ImageSharp equivalent | +|---|---| +| `Image` / `Bitmap` | [`Image`](xref:SixLabors.ImageSharp.Image) or [`Image`](xref:SixLabors.ImageSharp.Image`1) | +| `Color` | [`Color`](xref:SixLabors.ImageSharp.Color) or a specific pixel type such as [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32) | +| `PixelFormat` | generic `TPixel` plus [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo) | +| `GetPixel` / `SetPixel` | indexers or `ProcessPixelRows(...)` | +| `LockBits` / `UnlockBits` | `ProcessPixelRows(...)`, `CopyPixelDataTo(...)`, `LoadPixelData(...)`, `WrapMemory(...)`, `DangerousTryGetSinglePixelMemory(...)` | +| `Image.Save(...)` with codec choices | `Save(...)`, `SaveAsJpeg(...)`, `SaveAsPng(...)`, or explicit encoder types | +| `Graphics.DrawImage(...)` | `Mutate(...)` with `DrawImage(...)` | + +## Loading, Processing, and Saving + +A typical `System.Drawing` workflow translates to: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(context => context + .AutoOrient() + .Resize(400, 300)); + +image.SaveAsPng("output.png"); +``` + +Instead of mutating through a separate `Graphics` object, ImageSharp uses processing pipelines built with `Mutate(...)` or `Clone(...)`. + +## Pixels: Prefer Row Access Over Per-Pixel APIs + +If you used `Bitmap.GetPixel()` or `Bitmap.SetPixel()` heavily, the closest ImageSharp equivalent is the indexer: + +```csharp +using SixLabors.ImageSharp.PixelFormats; + +Rgba32 pixel = image[10, 20]; +image[10, 20] = Rgba32.White; +``` + +For real throughput, move to `ProcessPixelRows(...)` instead. That is the ImageSharp replacement for most `LockBits`-driven loops: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = Image.Load("input.png"); + +image.ProcessPixelRows(accessor => +{ + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + row.Reverse(); + } +}); +``` + +## `Color` and `TPixel` Are Different + +This is one of the biggest mental shifts. + +[`Color`](xref:SixLabors.ImageSharp.Color) in ImageSharp is a general color value that can convert to any [`IPixel`](xref:SixLabors.ImageSharp.PixelFormats.IPixel`1) type. It is not the same thing as the `TPixel` storage type used by [`Image`](xref:SixLabors.ImageSharp.Image`1). + +That means: + +- use [`Color`](xref:SixLabors.ImageSharp.Color) when you want a pixel-agnostic color value; +- use [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32), [`Bgra32`](xref:SixLabors.ImageSharp.PixelFormats.Bgra32), [`L8`](xref:SixLabors.ImageSharp.PixelFormats.L8), and similar types when you care about actual in-memory layout. + +## Replace `PixelFormat` with `TPixel` + +Instead of storing a runtime `PixelFormat` enum and branching on it later, ImageSharp encourages you to choose a generic working type: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image rgba = Image.Load("input.tiff"); +using Image bgra = rgba.CloneAs(); +``` + +If you only need metadata about the decoded source layout, [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) exposes [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo). + +## Replace `LockBits` with the Right Raw-Pixel API + +If your old code used `LockBits`, the best ImageSharp replacement depends on what the code was really trying to do: + +- use `ProcessPixelRows(...)` for most in-place managed algorithms; +- use `CopyPixelDataTo(...)` when you need a copied export buffer; +- use `LoadPixelData(...)` when you want to import raw bytes or pixels into a normal owned image; +- use `WrapMemory(...)` when you need a zero-copy bridge to existing memory; +- use `DangerousTryGetSinglePixelMemory(...)` only when you truly need one contiguous ImageSharp-owned buffer. + +## Compositing vs Drawing APIs + +If your `System.Drawing` code mainly used `Graphics.DrawImage(...)`, the closest ImageSharp equivalent is `DrawImage(...)` inside a processing pipeline. + +If the old code also draws shapes, paths, or text, you will usually want the companion packages documented elsewhere in this repo: + +- `SixLabors.ImageSharp.Drawing` +- `SixLabors.Fonts` + +## Practical Migration Strategy + +For most migrations, the least painful path is: + +1. Keep the old high-level workflow the same. +2. Replace `Bitmap` with `Image`. +3. Replace `Graphics` operations with `Mutate(...)` or `Clone(...)`. +4. Replace `LockBits` loops with `ProcessPixelRows(...)`. +5. Standardize on a working pixel format such as [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32) unless you have a reason not to. + +## Related Topics + +- [Getting Started](gettingstarted.md) +- [Working with Pixel Buffers](pixelbuffers.md) +- [Interop and Raw Memory](interop.md) +- [Pixel Formats](pixelformats.md) diff --git a/articles/imagesharp/orientation.md b/articles/imagesharp/orientation.md new file mode 100644 index 000000000..b0407d763 --- /dev/null +++ b/articles/imagesharp/orientation.md @@ -0,0 +1,86 @@ +# Rotate, Flip, and Auto-Orient + +ImageSharp provides several processors for correcting orientation and applying geometric transforms. The most common are `AutoOrient()`, `Rotate()`, `Flip()`, and `RotateFlip()`. + +## Correct Orientation from EXIF Metadata + +Use `AutoOrient()` early in your pipeline to normalize images captured by cameras and phones: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.AutoOrient()); +``` + +`AutoOrient()` uses embedded EXIF orientation metadata when it is present. This is often the right first processing step for user-uploaded photos. + +## Rotate by a Known Angle + +Use `Rotate()` to rotate by a specific number of degrees or a known rotate mode: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Rotate(90)); +``` + +ImageSharp also supports [`RotateMode`](xref:SixLabors.ImageSharp.Processing.RotateMode) when you want a predefined quarter-turn rotation. + +## Flip Horizontally or Vertically + +Use `Flip()` to mirror the image: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Flip(FlipMode.Horizontal)); +``` + +See [`FlipMode`](xref:SixLabors.ImageSharp.Processing.FlipMode) for the available options. + +## Combine Rotation and Flipping + +Use `RotateFlip()` when you need both operations together: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.RotateFlip(RotateMode.Rotate90, FlipMode.Vertical)); +``` + +This can make intent clearer than chaining separate calls when the final transformation is a single orientation step. + +## Normalize Orientation Before Resizing + +In most workflows, orient the image before cropping or resizing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .AutoOrient() + .Resize(1200, 800)); +``` + +That keeps downstream dimensions and crop coordinates aligned with the final visual orientation. + +## Related Topics + +- [Processing Images](processing.md) +- [Crop, Pad, and Canvas](cropandcanvas.md) +- [Working with Metadata](metadata.md) diff --git a/articles/imagesharp/pixelbuffers.md b/articles/imagesharp/pixelbuffers.md index 8200c43cb..0b73a749c 100644 --- a/articles/imagesharp/pixelbuffers.md +++ b/articles/imagesharp/pixelbuffers.md @@ -1,159 +1,154 @@ # Working with Pixel Buffers -### Setting individual pixels using indexers -A very basic and readable way for manipulating individual pixels is to use the indexer either on `Image` or `ImageFrame`: -```C# -using (Image image = new Image(400, 400)) -{ - image[200, 200] = Rgba32.White; // also works on ImageFrame -} -``` +ImageSharp gives you several ways to work directly with pixel data. The right one depends on whether you care most about simplicity, throughput, pixel-format independence, or interop. + +## Choose the Right Access Pattern -The indexer is an order of magnitude faster than the `.GetPixel(x, y)` and `.SetPixel(x, y)` methods of `System.Drawing`, but individual `[x, y]` indexing has inherent overhead compared to more sophisticated approaches demonstrated below. +Use: -### Efficient pixel manipulation -If you want to achieve killer speed in your pixel manipulation routines, you should utilize the per-row methods. These methods take advantage of the [`Span`-based memory manipulation primitives](https://www.codemag.com/Article/1807051/Introducing-.NET-Core-2.1-Flagship-Types-Span-T-and-Memory-T) from [System.Memory](https://www.nuget.org/packages/System.Memory/), providing a fast, yet safe low-level solution to manipulate pixel data. +- indexers for occasional pixel reads or writes; +- `ProcessPixelRows(...)` for fast row-by-row work in a known `TPixel`; +- `ProcessPixelRowsAsVector4(...)` for reusable pixel-format-agnostic processing; +- `CopyPixelDataTo(...)`, `LoadPixelData(...)`, and `WrapMemory(...)` when exchanging raw data with other systems. -This is how you can implement efficient row-by-row pixel manipulation. This API receives a @"SixLabors.ImageSharp.PixelAccessor`1" which ensures that the span is never [transferred to the heap](#spant-limitations) making the operation safe. +## Use Indexers for Simple Cases -> [!Note] -> The pixel manipulation APIs have been changed in ImageSharp 2.0. -> If you are interested about the background of these changes, see the [API discussion on GitHub](https://github.com/SixLabors/ImageSharp/issues/1739). +If you only need to touch a few pixels, the indexer is the simplest option: -```C# +```csharp using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = new(400, 400); +image[200, 200] = Rgba32.White; +``` + +That is fine for small amounts of work, but repeated random pixel access has more overhead than processing full rows. + +## Use `ProcessPixelRows(...)` for Fast Known-Format Access + +[`Image`](xref:SixLabors.ImageSharp.Image`1) and [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame`1) expose `ProcessPixelRows(...)` so you can work with row spans directly: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = Image.Load("input.png"); -// ... -using Image image = Image.Load("my_file.png"); image.ProcessPixelRows(accessor => { - // Color is pixel-agnostic, but it's implicitly convertible to the Rgba32 pixel type - Rgba32 transparent = Color.Transparent; - for (int y = 0; y < accessor.Height; y++) { - Span pixelRow = accessor.GetRowSpan(y); + Span row = accessor.GetRowSpan(y); - // pixelRow.Length has the same value as accessor.Width, - // but using pixelRow.Length allows the JIT to optimize away bounds checks: - for (int x = 0; x < pixelRow.Length; x++) + for (int x = 0; x < row.Length; x++) { - // Get a reference to the pixel at position x - ref Rgba32 pixel = ref pixelRow[x]; + ref Rgba32 pixel = ref row[x]; if (pixel.A == 0) { - // Overwrite the pixel referenced by 'ref Rgba32 pixel': - pixel = transparent; + pixel = Rgba32.Transparent; } } } }); ``` -It's possible to simplify the part dealing with `pixelRow` using C# 7.3 `foreach ref`: +This is the usual replacement for `LockBits`-style workflows when your algorithm already knows the working pixel format. -```C# -foreach (ref Rgba32 pixel in pixelRow) -{ - if (pixel.A == 0) - { - // overwrite the pixel referenced by 'ref Rgba32 pixel': - pixel = transparent; - } -} -``` +## Process Multiple Images Row by Row -Need to process two images simultaneously? Sure! +There are overloads for processing multiple images together: -```C# -// Extract a sub-region of sourceImage as a new image -private static Image Extract(Image sourceImage, Rectangle sourceArea) -{ - Image targetImage = new(sourceArea.Width, sourceArea.Height); - int height = sourceArea.Height; - sourceImage.ProcessPixelRows(targetImage, (sourceAccessor, targetAccessor) => - { - for (int i = 0; i < height; i++) - { - Span sourceRow = sourceAccessor.GetRowSpan(sourceArea.Y + i); - Span targetRow = targetAccessor.GetRowSpan(i); +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; - sourceRow.Slice(sourceArea.X, sourceArea.Width).CopyTo(targetRow); - } - }); +using Image source = Image.Load("source.png"); +using Image target = new(source.Width, source.Height); - return targetImage; -} +source.ProcessPixelRows(target, (sourceAccessor, targetAccessor) => +{ + for (int y = 0; y < sourceAccessor.Height; y++) + { + Span sourceRow = sourceAccessor.GetRowSpan(y); + Span targetRow = targetAccessor.GetRowSpan(y); + sourceRow.CopyTo(targetRow); + } +}); ``` -### Parallel, pixel-format agnostic image manipulation -There is a way to process image data in a pixel-agnostic floating-point format that has the advantage of working on images of any underlying pixel-format, in a completely transparent way: using the @"SixLabors.ImageSharp.Processing.PixelRowDelegateExtensions.ProcessPixelRowsAsVector4(SixLabors.ImageSharp.Processing.IImageProcessingContext,SixLabors.ImageSharp.Processing.PixelRowOperation)" APIs. +This is a good fit for compositing, comparisons, or custom copy/merge logic. + +## Use `ProcessPixelRowsAsVector4(...)` for Pixel-Format-Agnostic Logic -This is how you can use this extension to manipulate an image: +If you want one processor that can run on many `TPixel` formats, use `ProcessPixelRowsAsVector4(...)`: -```C# -// ... +```csharp +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; -image.Mutate(c => c.ProcessPixelRowsAsVector4(row => +image.Mutate(context => context.ProcessPixelRowsAsVector4(row => { for (int x = 0; x < row.Length; x++) { - // We can apply any custom processing logic here row[x] = Vector4.SquareRoot(row[x]); } })); ``` -This API receives a @"SixLabors.ImageSharp.Processing.PixelRowOperation" instance as input, and uses it to modify the pixel data of the target image. It does so by automatically executing the input operation in parallel, on multiple pixel rows at the same time, to fully leverage the power of modern multi-core CPUs. The `ProcessPixelRowsAsVector4` extension also takes care of converting the pixel data to/from the `Vector4` format, which means the same operation can be used to easily process images of any existing pixel-format, without having to implement the processing logic again for each of them. +This is extremely useful for reusable processing logic, but remember that it introduces conversion work to and from `Vector4`. It is often a great tradeoff for flexibility, but it is not always the fastest possible path for a hot server-side workload. -This extension offers fast and flexible way to implement custom image processors in ImageSharp. In certain cases (typically desktop apps running on multi-core CPU) the processor-level parallelism might be faster and desirable, but in case of high-load server-side applications it usually hurts throughput. To address this, the level of parallelism can be customized via @"SixLabors.ImageSharp.Configuration"'s @"SixLabors.ImageSharp.Configuration.MaxDegreeOfParallelism" property. +## Convert to a Working Pixel Format -### `Span` limitations -Please be aware that **`Span` has a very specific limitation**: it is a stack-only type! Read the *Is There Anything Span Can’t Do?!* section in [this article](https://www.codemag.com/Article/1807051/Introducing-.NET-Core-2.1-Flagship-Types-Span-T-and-Memory-T) for more details. -A short summary of the limitations: -- Span can only live on the execution stack. -- Span cannot be boxed or put on the heap. -- Span cannot be used as a generic type argument. -- Span cannot be an instance field of a type that itself is not stack-only. -- Span cannot be used within asynchronous methods. +Sometimes the cleanest approach is to convert the image into a known working format first: -### Exporting raw pixel data from an `Image` -You can use @"SixLabors.ImageSharp.Image`1.CopyPixelDataTo*" to copy the pixel data to a user buffer. Note that the following sample code leads to to significant extra GC allocation in case of large images, which can be avoided by processing the image row-by row instead. -```C# -Rgba32[] pixelArray = new Rgba32[image.Width * image.Height] -image.CopyPixelDataTo(pixelArray); -``` +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; -Or: -```C# -byte[] pixelBytes = new byte[image.Width * image.Height * Unsafe.SizeOf()] -image.CopyPixelDataTo(pixelBytes); +using Image source = Image.Load("input.tiff"); +using Image working = source.CloneAs(); ``` -### Loading raw pixel data into an `Image` +[`CloneAs()`](xref:SixLabors.ImageSharp.Image.CloneAs*) is especially useful when you want to standardize a pipeline on [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32), [`Bgra32`](xref:SixLabors.ImageSharp.PixelFormats.Bgra32), or another specific working format. -```C# -int width = ...; -int height = ...; -Rgba32[] rgbaData = GetMyRgbaArray(); -using (var image = Image.LoadPixelData(rgbaData, width, height)) -{ - // Work with the image -} +## Copy Raw Pixels In and Out + +Use `CopyPixelDataTo(...)` when you need a flattened copy of the root frame pixel buffer: + +```csharp +using SixLabors.ImageSharp.PixelFormats; + +Rgba32[] pixels = new Rgba32[image.Width * image.Height]; +image.CopyPixelDataTo(pixels); ``` -```C# -int width = ...; -int height = ...; -byte[] rgbaBytes = GetMyRgbaBytes(); -using (var image = Image.LoadPixelData(rgbaBytes, width, height)) -{ - // Work with the image -} +Use `LoadPixelData(...)` when you want ImageSharp to create an owned image from raw input: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +byte[] rgba = GetRgbaBytes(); +using Image image = Image.LoadPixelData(rgba, width, height); ``` -### OK nice, but how do you get a single pointer or span to the underlying pixel buffer? +There are stride-aware overloads for both pixel and byte input. For zero-copy interop, see [Interop and Raw Memory](interop.md). + +## `Span` Rules Still Apply + +The row spans you get from pixel accessors are [`Span`](xref:System.Span`1) values. That means they are stack-only: + +- They cannot be stored on the heap. +- They cannot cross `await` boundaries. +- They cannot be captured and used after the callback returns. + +Keep all row work inside the callback that received the accessor. -That's the neat part, you don't. 🙂 Well, normally. +## Related Topics -For custom image processing code written in C#, we highly recommend to use the methods introduced above, since ImageSharp buffers are discontiguous by default. However, certain interop use-cases may require to overcome this limitation, and we support that. Please read the [Memory Management](memorymanagement.md) section for more information. \ No newline at end of file +- [Pixel Formats](pixelformats.md) +- [Interop and Raw Memory](interop.md) +- [Memory Management](memorymanagement.md) +- [Migrating from System.Drawing](migratingfromsystemdrawing.md) diff --git a/articles/imagesharp/pixelformats.md b/articles/imagesharp/pixelformats.md index 22ba97d5b..991c608d2 100644 --- a/articles/imagesharp/pixelformats.md +++ b/articles/imagesharp/pixelformats.md @@ -1,25 +1,52 @@ # Pixel Formats -### Why is @"SixLabors.ImageSharp.Image`1" a generic class? +[`Image`](xref:SixLabors.ImageSharp.Image`1) is generic because the in-memory pixel type is part of the image contract. An [`Image`](xref:SixLabors.ImageSharp.Image`1) and an [`Image`](xref:SixLabors.ImageSharp.Image`1) can represent the same picture, but they differ in channel layout, precision, memory usage, and what direct pixel access means for your code. -We support multiple pixel formats just like _System.Drawing_ does. However, unlike their closed [PixelFormat](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.imaging.pixelformat) enumeration, our solution is extensible. -A pixel is basically a small value object (struct), describing the color at a given point according to a pixel model we call Pixel Format. `Image` represents a pixel graphic bitmap stored as a **generic, discontiguous memory block** of pixels, of total size `image.Width * image.Height`. Note that while the image memory should be considered discontiguous by default, if the image is small enough (less than ~4MB in memory, on 64-bit), it will be stored in a single, contiguous memory block. In addition to memory optimization advantages, discontigous buffers also enable us to load images at super high resolution, which couldn't otherwise be loaded due to limitations to the maximum size of `Span` in the .NET runtime, even on 64-bit systems. Please read the [Memory Management](memorymanagement.md) section for more information. +Image memory is usually treated as discontiguous, even though smaller images may fit in a single backing buffer. See [Memory Management](memorymanagement.md) for more detail on how ImageSharp stores large images efficiently. -In the case of multi-frame images multiple bitmaps are stored in `image.Frames` as `ImageFrame` instances. +For multi-frame images, the individual bitmaps live in `image.Frames` as [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame`1) instances. -### Choosing Pixel Formats +## What Counts as a Pixel Format -Take a look at the various pixel formats available under @"SixLabors.ImageSharp.PixelFormats#structs" After picking the pixel format of your choice, use it as a generic argument for @"SixLabors.ImageSharp.Image`1", for example, by instantiating `Image`. +A pixel format in ImageSharp is not just any color-related struct. To be used as `TPixel`, a type must implement [`IPixel`](xref:SixLabors.ImageSharp.PixelFormats.IPixel`1). -### Defining Custom Pixel Formats +That contract includes conversion members such as: -Creating your own pixel format is a case of defining a struct implementing @"SixLabors.ImageSharp.PixelFormats.IPixel`1" and using it as a generic argument for @"SixLabors.ImageSharp.Image`1". -Baseline batched pixel-conversion primitives are provided via @"SixLabors.ImageSharp.PixelFormats.PixelOperations`1" but it is possible to override those baseline versions with your own optimized implementation. +- [`ToRgba32()`](xref:SixLabors.ImageSharp.PixelFormats.IPixel.ToRgba32) +- [`ToScaledVector4()`](xref:SixLabors.ImageSharp.PixelFormats.IPixel.ToScaledVector4) +- [`ToVector4()`](xref:SixLabors.ImageSharp.PixelFormats.IPixel.ToVector4) +- `FromScaledVector4(...)` +- `FromVector4(...)` +- conversions to and from canonical pixel types such as [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32), [`Rgb24`](xref:SixLabors.ImageSharp.PixelFormats.Rgb24), [`Bgra32`](xref:SixLabors.ImageSharp.PixelFormats.Bgra32), [`L8`](xref:SixLabors.ImageSharp.PixelFormats.L8), and [`Rgba64`](xref:SixLabors.ImageSharp.PixelFormats.Rgba64) -### Is it possible to store a pixel on a single bit for monochrome images? +This is what keeps the image processing pipeline practical. Many operations and batched conversion paths assume pixels can move efficiently through RGBA-oriented [`Vector4`](xref:System.Numerics.Vector4) representations, and some optimized paths are specifically designed for [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32)-compatible pixel types where `ToVector4()` and `FromVector4(...)` are not expensive. -No. Our architecture does not allow sub-byte pixel formats at the moment. This feature is incredibly complex to implement, and you are going to pay the price of the low memory footprint in processing speed / CPU load. +## Pixel Formats Are Not Color Profile Types -### It is possible to decode into pixel formats like [CMYK](https://en.wikipedia.org/wiki/CMYK_color_model) or [CIELAB](https://en.wikipedia.org/wiki/Lab_color_space)? +This is separate from the color-profile conversion APIs described in [Color Profiles and Color Conversion](colorprofiles.md). -Unfortunately it's not possible and is unlikely to be in the future. Many image processing operations expect the pixels to be laid out in-memory in RGBA format. To manipulate images in exotic colorspaces we would have to translate each pixel to-and-from the colorspace multiple times, which would result in unusable performance and a loss of color information. +Types such as [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb), [`Cmyk`](xref:SixLabors.ImageSharp.ColorProfiles.Cmyk), [`Hsl`](xref:SixLabors.ImageSharp.ColorProfiles.Hsl), [`YCbCr`](xref:SixLabors.ImageSharp.ColorProfiles.YCbCr), [`CieLab`](xref:SixLabors.ImageSharp.ColorProfiles.CieLab), and [`CieXyz`](xref:SixLabors.ImageSharp.ColorProfiles.CieXyz) are color value types used by [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter). They are not `TPixel` implementations for [`Image`](xref:SixLabors.ImageSharp.Image`1). + +That means ImageSharp can convert color values between spaces like RGB, CMYK, Lab, and XYZ without treating those color models as general-purpose in-memory image storage formats. ImageSharp pixel formats are intentionally limited to types that fit the RGBA-oriented processing pipeline without expensive per-pixel translation on every operation. + +## Choosing Pixel Formats + +Choose a `TPixel` based on the kind of in-memory work you need to do: + +- Use [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32) as the general-purpose default. +- Use lower-memory formats such as [`Rgb24`](xref:SixLabors.ImageSharp.PixelFormats.Rgb24) or [`L8`](xref:SixLabors.ImageSharp.PixelFormats.L8) when you know you do not need the extra channels or precision. +- Use higher-precision formats such as [`Rgb48`](xref:SixLabors.ImageSharp.PixelFormats.Rgb48), [`Rgba64`](xref:SixLabors.ImageSharp.PixelFormats.Rgba64), or [`RgbaVector`](xref:SixLabors.ImageSharp.PixelFormats.RgbaVector) when your pipeline benefits from more precision. + +If you want to inspect pixel characteristics before a full decode, [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) exposes [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo). See [Read Image Info Without Decoding](identify.md) for more on that workflow. + +## Defining Custom Pixel Formats + +You can define a custom pixel format by creating a struct that implements [`IPixel`](xref:SixLabors.ImageSharp.PixelFormats.IPixel`1) and using it as the generic argument for [`Image`](xref:SixLabors.ImageSharp.Image`1). + +Baseline batched conversion primitives are provided by [`PixelOperations`](xref:SixLabors.ImageSharp.PixelFormats.PixelOperations`1), and you can override those implementations if you have a more efficient specialization. + +In practice, custom `TPixel` types should still fit the same RGBA-compatible conversion model as the built-in formats. Many of the packed and vector-style pixel types are deliberately in the same family as graphics-oriented packed color representations, and [`IPackedVector`](xref:SixLabors.ImageSharp.PixelFormats.IPackedVector`1) follows the same packed-value shape used by MonoGame and XNA types, which allows signature compatibility with them. + +## Single-Bit Monochrome Pixels + +ImageSharp does not currently support sub-byte `TPixel` formats such as a true 1-bit pixel type. That trade-off keeps the processing model and API surface much simpler, and it avoids paying a heavy CPU cost across the rest of the pipeline for a niche storage optimization. diff --git a/articles/imagesharp/png.md b/articles/imagesharp/png.md new file mode 100644 index 000000000..c312ff29c --- /dev/null +++ b/articles/imagesharp/png.md @@ -0,0 +1,125 @@ +# PNG + +PNG is the standard lossless choice for UI assets, screenshots, icons, and any workflow where transparency or exact pixel preservation matters. ImageSharp also supports animated PNG metadata and encoding scenarios. + +## Format Characteristics + +PNG is a lossless format. It preserves pixel data exactly, which makes it a strong fit for graphics where edges, text, and flat-color regions need to stay crisp. + +A few practical implications: + +- PNG is excellent for screenshots, icons, logos, diagrams, and UI assets. +- PNG supports alpha transparency. +- PNG is often much larger than JPEG for photographic content. +- PNG can also carry animated PNG data, though ecosystem support is not as universal as static PNG support. + +## Save as PNG + +Use [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder) when you want to tune PNG output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; + +using Image image = Image.Load("input.png"); + +image.Save("output.png", new PngEncoder +{ + CompressionLevel = PngCompressionLevel.BestCompression, + FilterMethod = PngFilterMethod.Adaptive, + ColorType = PngColorType.RgbWithAlpha +}); +``` + +PNG encoding is lossless. The main tradeoffs are encoder speed, file size, and how the pixel data is represented. + +## Key PNG Encoder Options + +The most commonly used `PngEncoder` options are: + +- `CompressionLevel` controls deflate compression effort. +- `FilterMethod` controls how scanlines are filtered before compression. +- `ColorType` and `BitDepth` control how pixel data is represented. +- `InterlaceMethod` lets you write an Adam7 interlaced image. +- `ChunkFilter` and `TextCompressionThreshold` control which ancillary data is written and how text chunks are compressed. +- `Gamma` lets you write a gamma value into the output metadata. + +Because `PngEncoder` inherits from [`QuantizingAnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingAnimatedImageEncoder), it also supports `Quantizer`, `PixelSamplingStrategy`, and `TransparentColorMode` when you are writing palette-based PNG data. + +## Quantization and Palette PNGs + +PNG does not always quantize. Quantization is only part of the encode path when you target a palette PNG by setting [`PngColorType.Palette`](xref:SixLabors.ImageSharp.Formats.Png.PngColorType.Palette). For RGB, RGBA, grayscale, or grayscale-with-alpha PNG output, ImageSharp writes the image in those representations without first reducing it to a palette. + +Palette PNGs can be a very good fit for icons, diagrams, pixel art, and other flat-color assets where a smaller indexed palette is acceptable. They are usually a poor fit for photos, gradients, and other images with subtle transitions. + +When you choose palette PNG output, ImageSharp uses the same quantization building blocks as the GIF encoder: + +- [`Quantizer`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.Quantizer) selects the palette-generation algorithm. +- [`PixelSamplingStrategy`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.PixelSamplingStrategy) controls how pixels are sampled when building the palette. +- [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder.TransparentColorMode) controls how fully transparent pixels are normalized during encoding. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +using Image image = Image.Load("input.png"); + +image.Save("output-indexed.png", new PngEncoder +{ + ColorType = PngColorType.Palette, + Quantizer = new WuQuantizer(new QuantizerOptions + { + MaxColors = 64, + Dither = null, + TransparentColorMode = TransparentColorMode.Preserve + }) +}); +``` + +If you need a fixed output palette instead of an adaptive one, use [`PaletteQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.PaletteQuantizer). If you keep `ColorType` as `Rgb`, `RgbWithAlpha`, `Grayscale`, or `GrayscaleWithAlpha`, the quantizer settings are not the main control surface because the encoder is not writing palette-indexed PNG data. + +## Read PNG and APNG Metadata + +Use `GetPngMetadata()` to inspect PNG-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; + +using Image image = Image.Load("input.png"); + +PngMetadata pngMetadata = image.Metadata.GetPngMetadata(); +``` + +`PngMetadata` includes values such as: + +- `ColorType` +- `BitDepth` +- `InterlaceMethod` +- `Gamma` +- `RepeatCount` +- `AnimateRootFrame` + +For animated PNGs, frame-level metadata is available through [`PngFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Png.PngFrameMetadata), including `FrameDelay`, `DisposalMode`, and `BlendMode`. + +## PNG-Specific Decode Options + +[`PngDecoderOptions`](xref:SixLabors.ImageSharp.Formats.Png.PngDecoderOptions) exposes `MaxUncompressedAncillaryChunkSizeBytes`, which can be useful when controlling how much memory decompressed ancillary chunks are allowed to occupy. + +## When to Use PNG + +PNG is usually a good fit when: + +- You need lossless output. +- The image uses transparency. +- You are working with screenshots, logos, diagrams, or UI assets. +- You need APNG-style animation metadata and frame control. + +PNG is usually a poor fit when: + +- The content is photographic and file size is a major concern. +- You only need a web-first animated format and modern browser-oriented compression matters more than static PNG compatibility. + +If you want a lossy photographic format, start with [JPEG](jpeg.md). If you want a modern alternative that supports both lossy and lossless output, see [WebP](webp.md). diff --git a/articles/imagesharp/processing.md b/articles/imagesharp/processing.md index c18625ce1..29b3f2f1c 100644 --- a/articles/imagesharp/processing.md +++ b/articles/imagesharp/processing.md @@ -1,50 +1,82 @@ -# Processing Image Operations +# Processing Images -The ImageSharp processing API is imperative. This means that the order in which you supply the individual processing operations is the order in which they are are compiled and applied. This allows the API to be very flexible, allowing you to combine processes in any order. Details of built in processing extensions can be found in the @"SixLabors.ImageSharp.Processing" documentation. +ImageSharp processing pipelines are imperative and ordered. The processors you add inside `Mutate()` or `Clone()` run in the same order you write them, which makes the pipeline easy to reason about and compose. -Processing operations are implemented using one of two available method calls. -[`Mutate`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*?displayProperty=name) and [`Clone`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Clone*?displayProperty=name) +The main entry points are [`Mutate`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*?displayProperty=name) and [`Clone`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Clone*?displayProperty=name): -The difference being that the former applies the given processing operations to the current image whereas the latter applies the operations to a deep copy of the original image. +- `Mutate()` applies processors to the current image. +- `Clone()` creates a deep copy and applies the processors to that copy. -For example: +## Mutate the Current Image -**Mutate** +Use `Mutate()` when you want to transform the current image in place: -```c# +```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -using (Image image = Image.Load(inPath)) -{ - // Resize the given image in place and return it for chaining. - // 'x' signifies the current image processing context. - image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2)); +using Image image = Image.Load("input.jpg"); - image.Save(outPath); -} // Dispose - releasing memory into a memory pool ready for the next image you wish to process. +image.Mutate(x => x + .AutoOrient() + .Resize(1200, 800) + .Grayscale()); + +image.Save("output.jpg"); ``` -**Clone** +This is the most common choice for request processing, thumbnails, and one-way export workflows. + +## Clone When You Need to Preserve the Original + +Use `Clone()` when the original image must remain unchanged: -```c# +```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Formats.Png; - -using (Image image = Image.Load(inStream)) -{ - // Create a deep copy of the given image, resize it, and return it for chaining. - using (Image copy = image.Clone(x => x.Resize(image.Width / 2, image.Height / 2))) - { - copy.Save(outStream, new PngEncoder()); - } -} // Dispose - releasing memory into a memory pool ready for the next image you wish to process. + +using Image image = Image.Load("input.jpg"); +using Image thumbnail = image.Clone(x => x + .Resize(160, 160) + .Sepia()); + +thumbnail.Save("thumbnail.jpg"); ``` -### Common Examples +This is useful when you need multiple derived outputs from the same source image. + +## Build Ordered Pipelines + +Processor order matters. For example, auto-orienting before resizing usually produces more predictable results than resizing first and correcting orientation later: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .AutoOrient() + .Crop(new Rectangle(200, 100, 1200, 800)) + .Resize(600, 400) + .BackgroundColor(Color.White)); +``` + +As a rule of thumb: + +- Normalize orientation early. +- Crop before expensive down-stream work when the crop meaningfully reduces the pixel area. +- Apply output-specific effects near the end of the pipeline. + +## Common Processing Topics + +- [Resizing Images](resize.md) covers `Resize()` and [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions). +- [Crop, Pad, and Canvas](cropandcanvas.md) covers `Crop()`, `Pad()`, `BackgroundColor()`, and `EntropyCrop()`. +- [Rotate, Flip, and Auto-Orient](orientation.md) covers `AutoOrient()`, `Rotate()`, `Flip()`, and `RotateFlip()`. +- [Color and Effects](colorandeffects.md) covers `Grayscale()`, `Sepia()`, `Brightness()`, `Contrast()`, `Hue()`, `Saturate()`, and `Opacity()`. +- [Quantization, Palettes, and Dithering](quantization.md) covers `Quantize()`, palette selection, encoder quantizers, and dithering algorithms. +- [Create an animated GIF](animatedgif.md) covers a multi-frame workflow. -Examples of common operations can be found in the following documentation pages. +## Related APIs -- [Resizing](resize.md) images using different options. -- Create [animated gif](animatedgif.md). \ No newline at end of file +Most built-in processors live under the [`SixLabors.ImageSharp.Processing`](xref:SixLabors.ImageSharp.Processing) namespace. Import that namespace in files where you build processing pipelines. diff --git a/articles/imagesharp/quantization.md b/articles/imagesharp/quantization.md new file mode 100644 index 000000000..3870d3bd0 --- /dev/null +++ b/articles/imagesharp/quantization.md @@ -0,0 +1,125 @@ +# Quantization, Palettes, and Dithering + +Quantization reduces an image to a limited set of colors. In ImageSharp, that matters both as an explicit processing step and as part of formats that write indexed or palette-constrained output. + +## Where Quantization Shows Up + +Quantization is relevant in a few common places: + +- [`Quantize()`](xref:SixLabors.ImageSharp.Processing.QuantizeExtensions) when you want to reduce colors as part of a processing pipeline. +- [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder), because GIF output is palette based. +- [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder) when you target [`PngColorType.Palette`](xref:SixLabors.ImageSharp.Formats.Png.PngColorType.Palette). +- [`IcoEncoder`](xref:SixLabors.ImageSharp.Formats.Ico.IcoEncoder) and [`CurEncoder`](xref:SixLabors.ImageSharp.Formats.Cur.CurEncoder) for icon and cursor workflows. + +Use quantization when you want smaller palette-based outputs, fixed-color branding palettes, retro or posterized looks, or more control over indexed formats. + +## Quantize as a Processing Step + +The default [`Quantize()`](xref:SixLabors.ImageSharp.Processing.QuantizeExtensions.Quantize*) overload uses [`KnownQuantizers.Octree`](xref:SixLabors.ImageSharp.Processing.KnownQuantizers.Octree), which is a fast, good general-purpose adaptive quantizer. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.Quantize(new WuQuantizer(new QuantizerOptions +{ + MaxColors = 64 +}))); + +image.Save("output.png"); +``` + +This remaps the image content to a smaller palette before you save it. That can be useful when you want the palette reduction to be part of the visible processing result rather than only an encoder detail. + +## Choose the Quantizer + +[`KnownQuantizers`](xref:SixLabors.ImageSharp.Processing.KnownQuantizers) exposes reusable built-in choices: + +- `KnownQuantizers.Octree` for a fast adaptive quantizer with solid general results. +- `KnownQuantizers.Wu` for high-quality adaptive palette generation. +- `KnownQuantizers.WebSafe` for the fixed web-safe palette. +- `KnownQuantizers.Werner` for the fixed Werner palette. + +When you need more control, create a quantizer directly: + +- [`WuQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.WuQuantizer) for adaptive palette generation with configurable [`QuantizerOptions`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.QuantizerOptions). +- [`OctreeQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.OctreeQuantizer) for fast adaptive quantization. +- [`PaletteQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.PaletteQuantizer) when you want to force output to a known palette. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +using Image image = Image.Load("input.png"); + +Color[] brandPalette = +{ + Color.Black, + Color.White, + Color.ParseHex("0057B8"), + Color.ParseHex("FFD100") +}; + +image.Mutate(x => x.Quantize(new PaletteQuantizer(brandPalette))); +``` + +## Dithering Choices + +[`QuantizerOptions`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.QuantizerOptions) controls the main quantization tradeoffs: + +- `MaxColors` limits the palette size. +- `Dither` selects the dithering algorithm. +- `DitherScale` adjusts how strongly dithering is applied. +- `TransparencyThreshold` and `TransparentColorMode` affect how transparent pixels are reduced into the palette. + +[`KnownDitherings`](xref:SixLabors.ImageSharp.Processing.KnownDitherings) exposes the built-in dithering algorithms, including ordered Bayer variants and error-diffusion algorithms such as Floyd-Steinberg, Atkinson, Burks, Jarvis-Judice-Ninke, and Stucki. + +Set `Dither = null` when you want flatter output with no dithering pattern. Keep dithering enabled when you want to hide banding in gradients or other smooth transitions. + +ImageSharp also has a separate [`Dither()`](xref:SixLabors.ImageSharp.Processing.DitherExtensions) processing extension. Its default overload reduces the image to the web-safe palette using [`KnownDitherings.Bayer8x8`](xref:SixLabors.ImageSharp.Processing.KnownDitherings.Bayer8x8), and other overloads let you dither against a palette you provide. + +## Encoder-Time Quantization + +Many palette-sensitive exports are better controlled at save time by configuring the encoder directly: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +using Image image = Image.Load("input.png"); + +image.Save("output-indexed.png", new PngEncoder +{ + ColorType = PngColorType.Palette, + Quantizer = new WuQuantizer(new QuantizerOptions + { + MaxColors = 128, + Dither = KnownDitherings.FloydSteinberg, + DitherScale = 0.75F, + TransparentColorMode = TransparentColorMode.Preserve + }), + PixelSamplingStrategy = new ExtensivePixelSamplingStrategy() +}); +``` + +This approach is usually the right choice when you want format-specific palette output without permanently changing the in-memory image first. + +## Sampling and Transparency + +Encoders that implement quantizing behavior also expose a pixel-sampling strategy. The default strategy samples a subset of pixels on large inputs to keep palette generation practical. [`ExtensivePixelSamplingStrategy`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.ExtensivePixelSamplingStrategy) scans all pixels instead, which can improve results when rare colors matter, at the cost of more work. + +Transparency handling matters most for GIF, palette PNG, ICO, and CUR output. [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.TransparentColorMode) controls how transparency is represented in the reduced palette, while `TransparencyThreshold` controls when partially transparent pixels are treated as transparent during quantization. + +## Related Topics + +- [GIF and Animation](gif.md) +- [PNG](png.md) +- [Convert Between Formats](formatconversion.md) +- [Read Image Info Without Decoding](identify.md) diff --git a/articles/imagesharp/recipes.md b/articles/imagesharp/recipes.md new file mode 100644 index 000000000..4ad6e6cf7 --- /dev/null +++ b/articles/imagesharp/recipes.md @@ -0,0 +1,17 @@ +# Recipes + +This section collects common ImageSharp tasks into small, copy-pasteable examples. These recipes are intentionally practical and build on the broader guidance in the conceptual docs. + +## Common Tasks + +- [Generate Thumbnails](thumbnails.md) for fit-within-box and square-crop thumbnail workflows. +- [Convert Between Formats](formatconversion.md) for common re-encode scenarios such as PNG to JPEG or JPEG to WebP. +- [Strip Metadata](stripmetadata.md) for removing EXIF, ICC, IPTC, XMP, and related metadata before export. +- [Read Image Info Without Decoding](identify.md) for dimensions, frame count, pixel info, and format detection without a full decode. + +## Related Topics + +- [Loading, Identifying, and Saving](loadingandsaving.md) +- [Working with Metadata](metadata.md) +- [Image Formats](imageformats.md) +- [Processing Images](processing.md) diff --git a/articles/imagesharp/resize.md b/articles/imagesharp/resize.md index 3b449e16c..0b98d60a2 100644 --- a/articles/imagesharp/resize.md +++ b/articles/imagesharp/resize.md @@ -1,51 +1,126 @@ # Resizing Images -Resizing an image is probably the most common processing operation that applications use. ImageSharp offers an incredibly flexible collection of resize options that allow developers to choose sizing algorithms, sampling algorithms, and gamma handling as well as other options. +Resizing is one of the most common ImageSharp operations. The simple `Resize()` overloads are good for direct width and height changes, while [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) gives you control over fit mode, anchor position, background padding, sampler choice, alpha handling, and manual target rectangles. -### The Basics +## Basic Resize -Resizing an image involves the process of creating and iterating through the pixels of a target image and sampling areas of a source image to choose what color to implement for each pixel. The sampling algorithm chosen affects the target color and can dramatically alter the result. Different samplers are usually chosen based upon the use case - For example `NearestNeigbor` is often used for fast, low quality thumbnail generation, `Lanczos3` for high quality thumbnails due to it's sharpening effect, and `Spline` for high quality enlargement due to it's smoothing effect. +Use the basic overloads when you already know the destination size: -With ImageSharp we default to `Bicubic` as it is a very robust algorithm offering good quality output when both reducing and enlarging images but you can easily set the algorithm when processing. +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Resize(1200, 800)); +image.Save("output.jpg"); +``` -A full list of out-of-the-box sampling algorithms can be found [here](xref:SixLabors.ImageSharp.Processing.KnownResamplers): +ImageSharp defaults to [`KnownResamplers.Bicubic`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Bicubic), which is a solid general-purpose choice for both downscaling and upscaling. -**Resize the given image using the default `Bicubic` sampler.** +If either `width` or `height` is `0`, ImageSharp calculates the missing dimension to preserve aspect ratio: -```c# +```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -using (Image image = Image.Load(inStream)) -{ - int width = image.Width / 2; - int height = image.Height / 2; - image.Mutate(x => x.Resize(width, height)); +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Resize(600, 0)); +``` + +## Choose the Right Resampler - image.Save(outPath); -} +Resampler choice affects sharpness, smoothness, and aliasing: + +- [`Bicubic`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Bicubic) is the balanced default. +- [`Lanczos3`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos3) is a strong choice for high-quality downscaling. +- [`Spline`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Spline) is often a good fit for enlargement. +- [`NearestNeighbor`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.NearestNeighbor) is useful for pixel art and hard-edged imagery. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.Resize(320, 240, KnownResamplers.Lanczos3)); ``` -**Resize the given image using the `Lanczos3` sampler:** +## Use ResizeOptions for Real-World Layout Rules + +[`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) is the main API for fit-and-fill workflows. When you use it, set [`Mode`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.Mode) explicitly; its default is [`Crop`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Crop). -```c# +```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Formats.Png; //used only for the PNG encoder below -using (Image image = Image.Load(inStream)) +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x.Resize(new ResizeOptions { - int width = image.Width / 2; - int height = image.Height / 2; - image.Mutate(x => x.Resize(width, height, KnownResamplers.Lanczos3)); + Size = new Size(300, 300), + Mode = ResizeMode.Crop, + Position = AnchorPositionMode.Center, + Sampler = KnownResamplers.Lanczos3, + Compand = true +})); +``` - image.Save(outStream, new PngEncoder());//Replace Png encoder with the file format of choice -} +The resize modes are: + +- [`Crop`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Crop): fills the target box and crops overflow. +- [`Pad`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Pad): fits within the target box and fills the remainder. +- [`BoxPad`](xref:SixLabors.ImageSharp.Processing.ResizeMode.BoxPad): pads without upscaling the original image; when downscaling it behaves like `Pad`. +- [`Max`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Max): fits within the box without cropping. +- [`Min`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Min): scales until the shortest side reaches the target, without upscaling. +- [`Stretch`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Stretch): ignores aspect ratio and forces the exact size. +- [`Manual`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Manual): uses [`TargetRectangle`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.TargetRectangle) to place the resized result explicitly. + +## Position, Padding, and Manual Placement + +`ResizeOptions` also controls where the result lands inside the output canvas: + +- [`Position`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.Position) sets the anchor for crop and pad operations. +- [`CenterCoordinates`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.CenterCoordinates) lets you bias crop focus more precisely. +- [`PadColor`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.PadColor) fills the background for padded results. +- [`TargetRectangle`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.TargetRectangle) is required for [`ResizeMode.Manual`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Manual). + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.Resize(new ResizeOptions +{ + Size = new Size(1200, 1200), + Mode = ResizeMode.Pad, + Position = AnchorPositionMode.Center, + PadColor = Color.White +})); ``` -> [!NOTE] -> If you pass `0` as any of the values for `width` and `height` dimensions then ImageSharp will automatically determine the correct opposite dimensions size to preserve the original aspect ratio. +## Companding and Alpha Handling + +[`Compand`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.Compand) enables gamma-companded resizing, which can improve the visual quality of some photographic resizes. It is not always necessary, but it is worth testing when color accuracy matters. + +[`PremultiplyAlpha`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.PremultiplyAlpha) defaults to `true` and should usually stay enabled for transparent images, because interpolation behaves better when alpha is handled in premultiplied form. + +## Decode Smaller When That Is Enough + +If you only need a bounded preview or thumbnail, consider decoding directly to a smaller size with [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) instead of fully decoding and then resizing. ImageSharp treats that target as a fit-within box equivalent to [`ResizeMode.Max`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Max). + +See [Loading, Identifying, and Saving](loadingandsaving.md) and [Security Considerations](security.md) for examples. + +## WrapMemory Caveat + +`Resize()` changes image dimensions and therefore needs a new backing buffer. Images created with `WrapMemory(...)` are best suited to fixed-size interop workflows, so resize them only after copying or cloning into a regular ImageSharp-owned image. + +See [Interop and Raw Memory](interop.md) for the full wrapped-memory guidance. -### Advanced Resizing +## Related Topics -In addition to basic resizing operations ImageSharp also offers more advanced features. Check out the @"SixLabors.ImageSharp.Processing.ResizeOptions" class for details. +- [Processing Images](processing.md) +- [Crop, Pad, and Canvas](cropandcanvas.md) +- [Generate Thumbnails](thumbnails.md) diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index 19b0ab9c0..6f97aad07 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -1,19 +1,118 @@ # Security Considerations -Image processing is a memory-intensive application. Most image processing libraries (including ImageSharp, SkiaSharp, and Magick.NET) decode images into in-memory buffers for further processing. Without additional measures, any publicly facing service that consumes images coming from untrusted sources might be vulnerable to DoS attacks attempting to deplete process memory. +Image processing is resource-intensive by nature. Public or semi-public systems that accept untrusted images should treat decode and processing as potentially expensive work and put explicit limits around what is accepted. -Such measures can be: -- Authentication, for example by using HMAC. See [Securing Processing Commands in ImageSharp.Web](../imagesharp.web/processingcommands.md#securing-processing-commands). -- Offloading to separate services/containers. -- Placing the solution behind a reverse proxy. -- Rate Limiting. -- Imposing conservative allocation limits by configuring a custom `MemoryAllocator`: +ImageSharp gives you several practical controls for doing that. + +## Preflight with Identify When Possible + +If you only need to validate dimensions, frame count, metadata presence, or pixel information, use [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify(System.String)) instead of a full decode. + +```csharp +using SixLabors.ImageSharp; + +ImageInfo info = Image.Identify("upload.bin"); + +Console.WriteLine($"{info.Width}x{info.Height}"); +Console.WriteLine($"Frames: {info.FrameCount}"); +Console.WriteLine($"Bits per pixel: {info.PixelType.BitsPerPixel}"); +``` + +This lets you reject obviously unsuitable files before allocating the full decoded image buffers. + +## Reduce Decode Cost with DecoderOptions + +[`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) is the main place to constrain decode behavior: + +- [`TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) decodes to a bounded fit-within size equivalent to [`ResizeMode.Max`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Max). +- [`MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) limits how many frames are decoded from animated formats. +- [`SkipMetadata`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SkipMetadata) avoids loading encoded metadata when you do not need it. +- [`SegmentIntegrityHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SegmentIntegrityHandling) controls how tolerant decoding is of damaged segments. ```csharp -Configuration.Default.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions() +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +DecoderOptions options = new() { - // Note that this limits the maximum image size to 64 megapixels of Rgba32. - // Any attempt to create a larger image will throw. + MaxFrames = 1, + SkipMetadata = true, + TargetSize = new Size(1600, 1600), + SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreNone +}; + +using Image image = Image.Load(options, stream); +``` + +For public upload endpoints, `MaxFrames = 1` is often appropriate when you only need a poster frame or preview. Likewise, `SkipMetadata = true` is a straightforward win when EXIF, ICC, IPTC, and XMP data are irrelevant to the workflow. + +## Be Deliberate About Error Tolerance + +[`SegmentIntegrityHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SegmentIntegrityHandling) is a tradeoff between strictness and recovery: + +- [`IgnoreNone`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreNone) rejects files on any segment validation error. +- [`IgnoreNonCritical`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreNonCritical) is the library default. +- [`IgnoreData`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreData) and [`IgnoreAll`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreAll) are better suited to recovery tools than to public-facing ingest paths. + +That recommendation is an inference from the enum semantics: the more errors you ignore, the more "best effort" your decode path becomes. + +## Restrict the Supported Format Set + +If your service only needs a small number of formats, build a dedicated [`Configuration`](xref:SixLabors.ImageSharp.Configuration) instead of exposing every registered codec: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; + +Configuration config = new( + new PngConfigurationModule(), + new JpegConfigurationModule()); + +DecoderOptions options = new() +{ + Configuration = config +}; +``` + +This reduces the amount of format detection and decoder surface area involved in that pipeline. + +## Limit Memory Use + +You can impose conservative allocation limits with a custom [`MemoryAllocator`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator): + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Memory; + +Configuration config = Configuration.Default.Clone(); +config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions +{ + // Roughly limits the workload to about 64 megapixels of Rgba32 data. AllocationLimitMegabytes = 256 }); -``` \ No newline at end of file +``` + +This is one of the most important safeguards for services that handle arbitrary uploads. For broader guidance on allocator behavior and tradeoffs, see [Memory Management](memorymanagement.md). + +## Put Outer Limits Around Streams and Requests + +ImageSharp requires a readable stream, and for non-seekable streams it copies the input into an internal seekable memory stream before decoding. In practice that means request-body limits, upload-size limits, and outer buffering rules still matter even before pixel buffers are allocated. + +Use your hosting layer to enforce: + +- maximum request body size; +- authentication or signed commands when appropriate; +- rate limiting; +- reverse proxy limits; and +- service or container isolation for expensive workloads. + +For ImageSharp.Web command signing, see [Securing Processing Commands in ImageSharp.Web](../imagesharp.web/processingcommands.md#securing-processing-commands). + +## Practical Security Defaults + +- Use `Identify()` first whenever a full decode is not necessary. +- Use `TargetSize`, `MaxFrames`, and `SkipMetadata` to shrink decode cost up front. +- Prefer `IgnoreNone` or the default `IgnoreNonCritical` over broader error ignoring on untrusted inputs. +- Restrict the enabled format modules when your workload only needs a few codecs. +- Use allocator limits and host-level request limits together rather than relying on only one layer. diff --git a/articles/imagesharp/stripmetadata.md b/articles/imagesharp/stripmetadata.md new file mode 100644 index 000000000..771c34627 --- /dev/null +++ b/articles/imagesharp/stripmetadata.md @@ -0,0 +1,50 @@ +# Strip Metadata + +Stripping metadata is useful when reducing file size, removing personal information, or normalizing exported assets. + +## Strip Metadata with the Encoder + +The simplest approach, when you control the output encoder, is to set [`ImageEncoder.SkipMetadata`](xref:SixLabors.ImageSharp.Formats.ImageEncoder.SkipMetadata) to `true`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; + +using Image image = Image.Load("input.jpg"); + +image.Save("output.jpg", new JpegEncoder +{ + Quality = 85, + SkipMetadata = true +}); +``` + +The same pattern works with other encoders that derive from `ImageEncoder`, such as `PngEncoder` and `WebpEncoder`. + +## Clear Common Metadata Profiles Manually + +If you want to clear metadata directly on the decoded image before saving, remove the common profiles from `image.Metadata`: + +```csharp +using SixLabors.ImageSharp; + +using Image image = Image.Load("input.jpg"); + +image.Metadata.ExifProfile = null; +image.Metadata.IccProfile = null; +image.Metadata.IptcProfile = null; +image.Metadata.XmpProfile = null; +image.Metadata.CicpProfile = null; + +image.Save("output.jpg"); +``` + +This approach is useful when you want to inspect or edit metadata before deciding what to keep. + +## Notes + +- `SkipMetadata = true` is usually the easiest option when you are already choosing an explicit encoder. +- Manual profile clearing gives you more control over which metadata survives. +- Saving to a different format can also change which metadata can be represented in the output. + +For more detail, see [Working with Metadata](metadata.md). diff --git a/articles/imagesharp/thumbnails.md b/articles/imagesharp/thumbnails.md new file mode 100644 index 000000000..7a49d8f8f --- /dev/null +++ b/articles/imagesharp/thumbnails.md @@ -0,0 +1,82 @@ +# Generate Thumbnails + +Thumbnail generation is one of the most common ImageSharp workflows. The two usual patterns are: + +- fit the image within a bounding box while preserving aspect ratio, and +- create a fixed-size thumbnail that fills the target area by cropping. + +## Fit Within a Bounding Box + +Use [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) with [`ResizeMode.Max`](xref:SixLabors.ImageSharp.Processing.ResizeMode) when you want the full image to fit inside a target box: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .AutoOrient() + .Resize(new ResizeOptions + { + Size = new Size(300, 300), + Mode = ResizeMode.Max + })); + +image.Save("thumbnail.jpg", new JpegEncoder { Quality = 85 }); +``` + +This keeps the whole image visible and preserves aspect ratio. + +## Create a Square Center-Crop Thumbnail + +Use [`ResizeMode.Crop`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Crop) to fill the target bounds and crop the overflow: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(x => x + .AutoOrient() + .Resize(new ResizeOptions + { + Size = new Size(256, 256), + Mode = ResizeMode.Crop, + Position = AnchorPositionMode.Center + })); + +image.Save("avatar.jpg"); +``` + +This is the usual pattern for avatars, cards, and tile-based UI. + +## Keep Transparency in Thumbnails + +If the source image uses transparency and you want to preserve it, save the thumbnail to a format that supports alpha, such as PNG or WebP: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.png"); + +image.Mutate(x => x.Resize(new ResizeOptions +{ + Size = new Size(256, 256), + Mode = ResizeMode.Max +})); + +image.Save("thumbnail.png", new PngEncoder()); +``` + +## Notes + +- `AutoOrient()` is usually the right first step for user-uploaded photos. +- `ResizeMode.Max` is for fit-within-box results. +- `ResizeMode.Crop` is for fixed output dimensions that must be fully filled. + +For more detail on resizing behavior, see [Resizing Images](resize.md). diff --git a/articles/imagesharp/tiff.md b/articles/imagesharp/tiff.md new file mode 100644 index 000000000..489c5da46 --- /dev/null +++ b/articles/imagesharp/tiff.md @@ -0,0 +1,83 @@ +# TIFF + +TIFF is useful in workflows where compression mode, pixel layout, and metadata fidelity matter more than broad browser support. ImageSharp exposes a range of TIFF-specific encoder and metadata options for those cases. + +## Format Characteristics + +TIFF is best thought of as a flexible imaging container with multiple possible encodings and metadata conventions rather than a single narrow web format. + +A few practical implications: + +- TIFF is common in archival, print, scanning, publishing, and professional imaging workflows. +- TIFF can represent different compression schemes and pixel layouts. +- TIFF can carry rich format-specific metadata. +- TIFF is usually not the best choice for browser-facing delivery. + +## Save as TIFF + +Use [`TiffEncoder`](xref:SixLabors.ImageSharp.Formats.Tiff.TiffEncoder) when you want to control how TIFF data is written: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; + +using Image image = Image.Load("input.png"); + +image.Save("output.tiff", new TiffEncoder +{ + Compression = TiffCompression.Lzw, + HorizontalPredictor = TiffPredictor.Horizontal, + BitsPerPixel = TiffBitsPerPixel.Bit24, + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb +}); +``` + +## Key TIFF Encoder Options + +The most commonly used `TiffEncoder` options are: + +- `Compression` controls the TIFF compression algorithm. +- `CompressionLevel` controls deflate compression effort when deflate is used. +- `BitsPerPixel` controls the encoded pixel depth. +- `PhotometricInterpretation` controls how pixel data is interpreted. +- `HorizontalPredictor` can improve compression ratios for deflate or LZW output. + +Some compression and photometric values are defined by the TIFF specification but are not currently supported by the encoder. In those cases, the encoder falls back rather than emitting unsupported output. + +## Read TIFF Metadata + +Use `GetTiffMetadata()` to inspect TIFF-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Tiff; + +using Image image = Image.Load("input.tiff"); + +TiffMetadata tiffMetadata = image.Metadata.GetTiffMetadata(); +``` + +`TiffMetadata` includes values such as: + +- `ByteOrder` +- `FormatType` +- `Compression` +- `BitsPerPixel` +- `PhotometricInterpretation` +- `Predictor` + +## When to Use TIFF + +TIFF is usually worth considering when: + +- You need TIFF-specific compression or pixel layout options. +- You care about byte order, predictor behavior, or TIFF format metadata. +- The workflow is archival, interchange, print, or imaging-pipeline oriented rather than browser-first. + +TIFF is usually a poor fit when: + +- The output is primarily intended for browser delivery. +- You just need a simple photo or web asset format. + +For more typical application and web workloads, [PNG](png.md), [JPEG](jpeg.md), and [WebP](webp.md) are usually better starting points. diff --git a/articles/imagesharp/troubleshooting.md b/articles/imagesharp/troubleshooting.md new file mode 100644 index 000000000..5e0d376c0 --- /dev/null +++ b/articles/imagesharp/troubleshooting.md @@ -0,0 +1,130 @@ +# Troubleshooting + +Most ImageSharp problems fall into a few common categories: format detection, invalid content, stream positioning, memory pressure, interop assumptions, and disposal bugs. + +## "Image format is unknown" + +[`UnknownImageFormatException`](xref:SixLabors.ImageSharp.UnknownImageFormatException) means ImageSharp could not match the input to a registered format detector or decoder. + +Common causes: + +- the input is empty; +- the stream is positioned incorrectly; +- the format is not registered in the current [`Configuration`](xref:SixLabors.ImageSharp.Configuration); +- the input is not an image at all. + +Useful first checks: + +```csharp +using SixLabors.ImageSharp; + +var format = Image.DetectFormat(bytes); +ImageInfo? info = Image.Identify(bytes); +``` + +If `DetectFormat(...)` fails, focus on the source bytes or the active configuration before debugging anything else. + +## "The format is known, but loading still fails" + +[`InvalidImageContentException`](xref:SixLabors.ImageSharp.InvalidImageContentException) means the decoder recognized the format but the encoded data was invalid, truncated, or unsupported in some way. + +That usually points to corrupted input, partial downloads, damaged metadata blocks, or malformed animation/frame data rather than a registration issue. + +`Identify(...)` is often useful here because it lets you confirm whether basic header parsing works before you commit to a full decode. + +## Stream Position Problems + +By default, ImageSharp respects the current position of a seekable stream. If your stream has already been read from, loading may fail even though the underlying data is valid. + +You can fix that either by resetting the stream manually or by using [`ReadOrigin.Begin`](xref:SixLabors.ImageSharp.ReadOrigin.Begin) on a custom configuration: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +Configuration config = Configuration.Default.Clone(); +config.ReadOrigin = ReadOrigin.Begin; + +DecoderOptions options = new() +{ + Configuration = config +}; + +using Image image = Image.Load(options, stream); +``` + +## Large Images or Animations Use Too Much Memory + +If decoding fails with an [`InvalidImageContentException`](xref:SixLabors.ImageSharp.InvalidImageContentException) that wraps an [`InvalidMemoryOperationException`](xref:SixLabors.ImageSharp.Memory.InvalidMemoryOperationException), the requested image size or frame set may be beyond the allocator limits or practical memory budget. + +Ways to reduce decode cost: + +- use [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) when a smaller decode is acceptable; +- use [`DecoderOptions.MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) to cap animated formats; +- use [`DecoderOptions.SkipMetadata`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SkipMetadata) when metadata is not needed; +- adjust [`MemoryAllocatorOptions.AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) if you truly need a larger allocator budget. + +Also avoid turning on [`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) unless you explicitly need contiguous memory for interop. + +## `DangerousTryGetSinglePixelMemory(...)` Returns `false` + +That means the image is not backed by one contiguous buffer. This is normal for ImageSharp. + +If you truly need a single [`Memory`](xref:System.Memory`1), create or load the image with a local configuration that has [`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) set to `true`. Even then, very large images may still be unable to satisfy a contiguous allocation request. + +## Raw Memory Import Fails + +`LoadPixelData(...)` and `WrapMemory(...)` validate: + +- width and height; +- row stride; +- byte-stride divisibility by pixel size; +- required buffer length. + +If you get [`ArgumentException`](xref:System.ArgumentException) or [`ArgumentOutOfRangeException`](xref:System.ArgumentOutOfRangeException), double-check: + +- whether the buffer is tightly packed or padded; +- whether you passed pixel stride or byte stride to the correct overload; +- whether the `TPixel` matches the actual input layout. + +## Transform Operations Throw `DegenerateTransformException` + +[`DegenerateTransformException`](xref:SixLabors.ImageSharp.Processing.Processors.Transforms.DegenerateTransformException) means a transform matrix or builder input collapsed into an invalid transform. + +This usually happens when a perspective or affine transform is built from duplicate points, zero-area geometry, or other mathematically degenerate inputs. + +When that happens, validate the source geometry before building the transform rather than treating it as a decoder or encoder problem. + +## Memory Keeps Growing + +The first question to ask is whether images are being disposed promptly. + +Use: + +- [`MemoryDiagnostics.TotalUndisposedAllocationCount`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics.TotalUndisposedAllocationCount) for a low-overhead signal; +- [`MemoryDiagnostics.UndisposedAllocation`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics.UndisposedAllocation) when you need stack traces for leaked allocations. + +```csharp +using SixLabors.ImageSharp.Diagnostics; + +Console.WriteLine(MemoryDiagnostics.TotalUndisposedAllocationCount); +``` + +If that number trends upward in a steady-state workload, start looking for missing `Dispose()` or `using` blocks. + +## A Good Debugging Order + +When an image pipeline misbehaves, this order is usually productive: + +1. Run `DetectFormat(...)` or `Identify(...)`. +2. Confirm the stream position and active configuration. +3. Check whether the problem is a format-registration issue or an invalid-content issue. +4. Reduce decode cost with `TargetSize`, `MaxFrames`, or `SkipMetadata` if memory is the problem. +5. Only then investigate deeper processing or interop assumptions. + +## Related Topics + +- [Loading, Identifying, and Saving](loadingandsaving.md) +- [Configuration](configuration.md) +- [Memory Management](memorymanagement.md) +- [Interop and Raw Memory](interop.md) diff --git a/articles/imagesharp/webp.md b/articles/imagesharp/webp.md new file mode 100644 index 000000000..531eb4f0d --- /dev/null +++ b/articles/imagesharp/webp.md @@ -0,0 +1,89 @@ +# WebP + +WebP supports lossy and lossless compression, transparency, and animation. In ImageSharp, it is one of the most flexible general-purpose web output formats. + +## Format Characteristics + +WebP is a modern format family rather than a single narrow use case. It can be used as: + +- a lossy alternative to JPEG, +- a lossless alternative to PNG in many workflows, +- a transparency-capable web format, +- and an animation format. + +A few practical implications: + +- WebP is often the most flexible web-oriented output option. +- WebP supports both alpha transparency and animation. +- Lossy and lossless modes have different tuning behavior. +- Compatibility is generally strong in modern environments, but not identical to long-established formats like JPEG or PNG everywhere. + +## Save as WebP + +Use [`WebpEncoder`](xref:SixLabors.ImageSharp.Formats.Webp.WebpEncoder) when you want to tune WebP output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; + +using Image image = Image.Load("input.png"); + +image.Save("output.webp", new WebpEncoder +{ + FileFormat = WebpFileFormatType.Lossless, + Quality = 75, + Method = WebpEncodingMethod.BestQuality, + UseAlphaCompression = true +}); +``` + +Set `FileFormat` to choose between lossy and lossless output. + +## Key WebP Encoder Options + +The most commonly used `WebpEncoder` options are: + +- `FileFormat` chooses lossy or lossless encoding. +- `Quality` controls quality or compression effort, depending on the mode. +- `Method` controls the speed/quality tradeoff. +- `UseAlphaCompression` controls how the alpha plane is compressed. +- `NearLossless` and `NearLosslessQuality` tune near-lossless workflows. +- `EntropyPasses`, `SpatialNoiseShaping`, and `FilterStrength` expose more advanced tuning. + +Because `WebpEncoder` inherits from [`AnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder), it also supports `RepeatCount`, `BackgroundColor`, and `AnimateRootFrame`. + +## Read WebP Metadata + +Use `GetWebpMetadata()` to inspect WebP-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; + +using Image image = Image.Load("input.webp"); + +WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); +``` + +`WebpMetadata` includes values such as: + +- `FileFormat` +- `ColorType` +- `BitsPerPixel` +- `RepeatCount` +- `BackgroundColor` + +For animated WebP, frame-level metadata is available through [`WebpFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Webp.WebpFrameMetadata), including `FrameDelay`, `BlendMode`, and `DisposalMode`. + +## When to Use WebP + +WebP is a strong choice when you want: + +- Lossy or lossless output from the same family of encoders. +- Transparency support. +- Animation support. +- More control over size/quality tradeoffs than a simple save-by-extension workflow provides. + +WebP is often the best first alternative to compare against both JPEG and PNG when optimizing for delivery size. + +If you need strict lossless preservation with a more traditional workflow, see [PNG](png.md). If you specifically need TIFF-style metadata and pixel layout control, see [TIFF](tiff.md). diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index 4e57eeef9..bc01232ae 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -7,10 +7,13 @@ Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/d ### License PolygonClipper is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. + +>[!IMPORTANT] +>Starting with PolygonClipper 1.0.0, projects that directly depend on SixLabors.PolygonClipper require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation -PolygonClipper is installed via [NuGet](https://www.nuget.org/packages/SixLabors.PolygonClipper) with nightly builds available on [MyGet](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.PolygonClipper). +PolygonClipper is installed via [NuGet](https://www.nuget.org/packages/SixLabors.PolygonClipper) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) diff --git a/articles/toc.md b/articles/toc.md index 236f2d690..75e57f55d 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -1,14 +1,34 @@ # [ImageSharp](imagesharp/index.md) ## [Getting Started](imagesharp/gettingstarted.md) -### [Pixel Formats](imagesharp/pixelformats.md) -### [Image Formats](imagesharp/imageformats.md) -### [Processing Images](imagesharp/processing.md) -#### [Resizing Images](imagesharp/resize.md) -#### [Create an animated GIF](imagesharp/animatedgif.md) -### [Working with Pixel Buffers](imagesharp/pixelbuffers.md) -### [Configuration](imagesharp/configuration.md) -### [Memory Management](imagesharp/memorymanagement.md) -### [Security Considerations](imagesharp/security.md) +## [Loading, Identifying, and Saving](imagesharp/loadingandsaving.md) +## [Working with Metadata](imagesharp/metadata.md) +## [Color Profiles and Color Conversion](imagesharp/colorprofiles.md) +## [Pixel Formats](imagesharp/pixelformats.md) +## [Image Formats](imagesharp/imageformats.md) +### [JPEG](imagesharp/jpeg.md) +### [PNG](imagesharp/png.md) +### [GIF and Animation](imagesharp/gif.md) +### [WebP](imagesharp/webp.md) +### [TIFF](imagesharp/tiff.md) +## [Processing Images](imagesharp/processing.md) +### [Resizing Images](imagesharp/resize.md) +### [Crop, Pad, and Canvas](imagesharp/cropandcanvas.md) +### [Rotate, Flip, and Auto-Orient](imagesharp/orientation.md) +### [Color and Effects](imagesharp/colorandeffects.md) +### [Quantization, Palettes, and Dithering](imagesharp/quantization.md) +### [Create an animated GIF](imagesharp/animatedgif.md) +## [Working with Pixel Buffers](imagesharp/pixelbuffers.md) +## [Interop and Raw Memory](imagesharp/interop.md) +## [Configuration](imagesharp/configuration.md) +## [Memory Management](imagesharp/memorymanagement.md) +## [Security Considerations](imagesharp/security.md) +## [Troubleshooting](imagesharp/troubleshooting.md) +## [Migrating from System.Drawing](imagesharp/migratingfromsystemdrawing.md) +## [Recipes](imagesharp/recipes.md) +### [Generate Thumbnails](imagesharp/thumbnails.md) +### [Convert Between Formats](imagesharp/formatconversion.md) +### [Strip Metadata](imagesharp/stripmetadata.md) +### [Read Image Info Without Decoding](imagesharp/identify.md) # [ImageSharp.Drawing](imagesharp.drawing/index.md) ## [Getting Started](imagesharp.drawing/gettingstarted.md) diff --git a/docfx.json b/docfx.json index cf9074d5b..50b77f1a3 100644 --- a/docfx.json +++ b/docfx.json @@ -118,7 +118,8 @@ ], "exclude": [ "README.md", - "_site/**" + "_site/**", + "**/codecov.yml" ] } ], diff --git a/index.md b/index.md index e157d7dc0..a11b235fd 100644 --- a/index.md +++ b/index.md @@ -1,11 +1,11 @@ # Six Labors Documentation -We aim to provide modern, cross-platform, incredibly powerful yet beautifully simple graphics libraries. Built against .NET Standard, our libraries can be used in device, cloud, and embedded/IoT scenarios. +We aim to provide modern, cross-platform, incredibly powerful yet beautifully simple graphics libraries. Built against .NET, our libraries can be used in device, cloud, and embedded/IoT scenarios. You can find tutorials, examples and API details covering all Six Labors projects. >[!NOTE] ->Documentation for previous releases can be found at . +>Documentation for previous releases can be found at . ### [API documentation](api/index.md) From ec84393f6834b6f1b2c97b7334b661f1c3142309 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 5 Apr 2026 21:56:15 +1000 Subject: [PATCH 04/21] Add Fonts docs --- articles/fonts/checkglyphcoverage.md | 42 +++++ articles/fonts/colorfonts.md | 105 ++++++++++++ articles/fonts/customrendering.md | 193 +++++++++++---------- articles/fonts/fallbackfonts.md | 108 ++++++++++++ articles/fonts/fittexttowidth.md | 48 ++++++ articles/fonts/fontmetadata.md | 116 +++++++++++++ articles/fonts/fontmetrics.md | 231 ++++++++++++++++++++++++++ articles/fonts/gettingstarted.md | 137 +++++++++++---- articles/fonts/hintingandshaping.md | 160 ++++++++++++++++++ articles/fonts/index.md | 33 +++- articles/fonts/inspectfontfiles.md | 39 +++++ articles/fonts/listsystemfonts.md | 45 +++++ articles/fonts/measuringtext.md | 107 ++++++++++++ articles/fonts/opentypefeatures.md | 123 ++++++++++++++ articles/fonts/recipes.md | 11 ++ articles/fonts/systemfonts.md | 104 ++++++++++++ articles/fonts/textlayout.md | 174 +++++++++++++++++++ articles/fonts/troubleshooting.md | 105 ++++++++++++ articles/fonts/unicode.md | 146 ++++++++++++++++ articles/fonts/useopentypefeatures.md | 55 ++++++ articles/fonts/variablefonts.md | 121 ++++++++++++++ articles/imagesharp/index.md | 14 +- articles/toc.md | 20 ++- ext/Fonts | 2 +- index.md | 32 ++-- 25 files changed, 2122 insertions(+), 149 deletions(-) create mode 100644 articles/fonts/checkglyphcoverage.md create mode 100644 articles/fonts/colorfonts.md create mode 100644 articles/fonts/fallbackfonts.md create mode 100644 articles/fonts/fittexttowidth.md create mode 100644 articles/fonts/fontmetadata.md create mode 100644 articles/fonts/fontmetrics.md create mode 100644 articles/fonts/hintingandshaping.md create mode 100644 articles/fonts/inspectfontfiles.md create mode 100644 articles/fonts/listsystemfonts.md create mode 100644 articles/fonts/measuringtext.md create mode 100644 articles/fonts/opentypefeatures.md create mode 100644 articles/fonts/recipes.md create mode 100644 articles/fonts/systemfonts.md create mode 100644 articles/fonts/textlayout.md create mode 100644 articles/fonts/troubleshooting.md create mode 100644 articles/fonts/unicode.md create mode 100644 articles/fonts/useopentypefeatures.md create mode 100644 articles/fonts/variablefonts.md diff --git a/articles/fonts/checkglyphcoverage.md b/articles/fonts/checkglyphcoverage.md new file mode 100644 index 000000000..9a2edba08 --- /dev/null +++ b/articles/fonts/checkglyphcoverage.md @@ -0,0 +1,42 @@ +# Check Glyph Coverage Before Choosing Fallbacks + +This recipe is useful when you want to know whether a font can cover the text you plan to render before you choose fallback families. + +### Check individual code points + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; + +Font font = SystemFonts.CreateFont("Segoe UI", 16); + +bool hasLatinA = font.TryGetGlyphs(new CodePoint('A'), out _); +bool hasOmega = font.TryGetGlyphs(new CodePoint(0x03A9), out _); +bool hasEmoji = font.TryGetGlyphs(new CodePoint(0x1F600), out _); +``` + +### Scan a whole string for missing glyphs + +```csharp +using System.Collections.Generic; +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; + +string text = "Hello 123 \u0645\u0631\u062D\u0628\u0627 \uD83D\uDE00"; +Font font = SystemFonts.CreateFont("Segoe UI", 16); +List missing = new(); + +foreach (CodePoint codePoint in text.AsSpan().EnumerateCodePoints()) +{ + if (!font.TryGetGlyphs(codePoint, out _)) + { + missing.Add(codePoint); + } +} +``` + +This is a simple way to decide whether you need `FallbackFontFamilies` before you measure or render the text. + +If you want a broader face-level view instead of checking a specific string, use `Font.FontMetrics.GetAvailableCodePoints()`. + +For the conceptual fallback guidance, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). For face-level coverage inspection, see [Font Metrics](fontmetrics.md). diff --git a/articles/fonts/colorfonts.md b/articles/fonts/colorfonts.md new file mode 100644 index 000000000..a66cd07ff --- /dev/null +++ b/articles/fonts/colorfonts.md @@ -0,0 +1,105 @@ +# Color Fonts + +Color fonts let glyphs carry paint information instead of only a monochrome outline. + +Fonts has comprehensive support for the major OpenType color-font technologies it exposes publicly: + +- `ColorFontSupport.ColrV0` for layered solid-color glyphs defined by COLR and CPAL tables +- `ColorFontSupport.ColrV1` for paint-graph glyphs with gradients, transforms, and richer composition +- `ColorFontSupport.Svg` for color glyphs stored in the OpenType SVG table + +### Enable or restrict color-font support + +`TextOptions.ColorFontSupport` controls which color-font technologies are honored during layout and rendering. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); +Font font = family.CreateFont(32); + +TextOptions options = new(font) +{ + ColorFontSupport = ColorFontSupport.ColrV1 | ColorFontSupport.ColrV0 | ColorFontSupport.Svg +}; +``` + +`TextOptions` enables all three by default, so you usually only need to set this property when you want to disable color glyphs or restrict the allowed formats. + +### Force monochrome output + +Set `ColorFontSupport.None` when you want color-font-capable text to fall back to monochrome outline rendering. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); +Font font = family.CreateFont(32); + +TextOptions options = new(font) +{ + ColorFontSupport = ColorFontSupport.None +}; +``` + +### What happens in custom renderers + +When a resolved glyph is a painted color glyph, Fonts streams it through `IGlyphRenderer` as one or more layers. + +That means custom renderers should pay attention to: + +- `GlyphRendererParameters.GlyphType` +- `BeginLayer(...)` +- `Paint` +- `FillRule` +- `ClipQuad` + +Depending on the font technology in use, the `Paint` passed to `BeginLayer(...)` may be: + +- `SolidPaint` +- `LinearGradientPaint` +- `RadialGradientPaint` +- `SweepGradientPaint` + +If your renderer ignores paint information, the glyph can still be drawn, but it will no longer preserve the font's intended color presentation. + +### Inspect color glyphs directly + +If you need to inspect a glyph without running full text layout, use `Font.TryGetGlyphs(...)` with explicit color support. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); +Font font = family.CreateFont(32); + +if (font.TryGetGlyphs( + new CodePoint(0x1F600), + ColorFontSupport.ColrV1 | ColorFontSupport.ColrV0 | ColorFontSupport.Svg, + out Glyph? glyph)) +{ + bool isPainted = glyph.GlyphMetrics.GlyphType == GlyphType.Painted; +} +``` + +### COLR vs SVG in practice + +At a high level: + +- COLR v0 uses layered shapes with palette colors +- COLR v1 extends that model with richer paint graphs, gradients, transforms, and clipping +- SVG glyphs carry SVG-authored painted content + +Fonts resolves those technologies into a common painted-glyph rendering flow, which is why custom renderers can consume them through the same layer and paint callbacks. + +### Measurement and rendering stay aligned + +Color-font support is part of text layout, not just final painting. If you measure text with one `ColorFontSupport` configuration and render with another, you can create drift between the measured and rendered result. + +Use the same `TextOptions` instance for both `TextMeasurer` and `TextRenderer` when you want a guaranteed match. + +For renderer implementation details, see [Custom Rendering](customrendering.md). For fallback across multiple families, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). diff --git a/articles/fonts/customrendering.md b/articles/fonts/customrendering.md index 33b07504c..656eb50a9 100644 --- a/articles/fonts/customrendering.md +++ b/articles/fonts/customrendering.md @@ -1,134 +1,143 @@ # Custom Rendering ->[!WARNING] ->Fonts is still considered BETA quality and we still reserve the rights to change the API shapes. - >[!NOTE] ->ImageSharp.Drawing already implements the glyph rendering for you unless you are rendering on other platforms we would recommend using the version provided by that library.. This is a more advanced topic. +>If you want to draw text onto images, [ImageSharp.Drawing](../imagesharp.drawing/index.md) already provides the rendering layer for you. This page is for cases where you want to render glyphs to your own surface or extract geometry for another system. + +Custom rendering in Fonts is built around `IGlyphRenderer`. `TextRenderer.RenderTextTo(...)` performs layout and shaping, then sends the result to your renderer as glyphs, layers, figures, and path commands. + +### When to use it + +Custom rendering is useful when you want to: + +- draw text into a game engine or UI toolkit +- export outlines to SVG, PDF, or another vector format +- capture glyph geometry for hit testing or diagnostics +- consume color-font layers and paints yourself + +### Rendering flow + +The callbacks are delivered in this order: + +1. `BeginText(...)` +2. `BeginGlyph(...)` +3. `BeginLayer(...)` +4. `BeginFigure()`, `MoveTo(...)`, `LineTo(...)`, `QuadraticBezierTo(...)`, `CubicBezierTo(...)`, `ArcTo(...)`, `EndFigure()` +5. `EndLayer()` +6. `EndGlyph()` +7. `SetDecoration(...)` for any decorations requested by `EnabledDecorations()` +8. `EndText()` -### Implementing a glyph renderer +`BeginGlyph(...)` receives `GlyphRendererParameters`, which identify the glyph instance being rendered, including the glyph ID, the glyph's `CodePoint` value, font style, point size, DPI, layout mode, and active `TextRun`. Return `false` from `BeginGlyph(...)` if you want to skip rendering that glyph. -The abstraction used by `Fonts` to allow implementing glyph rendering is the `IGlyphRenderer` and its brother `IColoredGlypheRenderer` (for colored emoji support). +### A minimal renderer +```csharp +using System.Collections.Generic; +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; -```c# - // `IColoredGlyphRenderer` implements `IGlyphRenderer` so if you don't want colored font support just implement `IGlyphRenderer`. -public class CustomGlyphRenderer : IColoredGlyphRenderer +public sealed class RecordingGlyphRenderer : IGlyphRenderer { + public List Points { get; } = new(); - /// - /// Called before any glyphs have been rendered. - /// - /// The bounds the text will be rendered at and at whats size. - void IGlyphRenderer.BeginText(FontRectangle bounds) + public void BeginText(in FontRectangle bounds) { - // called before any thing else to provide access to the total required size to redner the text } - /// - /// Begins the glyph. - /// - /// The bounds the glyph will be rendered at and at what size. - /// The set of paramaters that uniquely represents a version of a glyph in at particular font size, font family, font style and DPI. - /// Returns true if the glyph should be rendered othersie it returns false. - bool IGlyphRenderer.BeginGlyph(FontRectangle bounds, GlyphRendererParameters paramaters) + public void EndText() { - // called before each glyph/glyph layer is rendered. - // The paramaters can be used to detect the exact details - // of the glyph so that duplicate glyphs could optionally - // be cached to reduce processing. - - // You can return false to skip all the figures within the glyph (if you return false EndGlyph will still be called) } - /// - /// Sets the color to use for the current glyph. - /// - /// The color to override the renders brush with. - void IColorGlyphRenderer.SetColor(GlyphColor color) - { - // from the IColorGlyphRenderer version, onlt called if the current glyph should override the forgound color of current glyph/layer - } + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) => true; - /// - /// Begins the figure. - /// - void IGlyphRenderer.BeginFigure() + public void EndGlyph() { - // called at the start of the figure within the single glyph/layer - // glyphs are rendered as a serise of arcs, lines and movements - // which together describe a complex shape. } - /// - /// Sets a new start point to draw lines from - /// - /// The point. - void IGlyphRenderer.MoveTo(Vector2 point) + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) { - // move current point to location marked by point without describing a line; } - /// - /// Draw a quadratic bezier curve connecting the previous point to . - /// - /// The second control point. - /// The point. - void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + public void EndLayer() { - // describes Quadratic Bezier curve from the 'current point' using the - // 'second control point' and final 'point' leaving the 'current point' - // at 'point' } - /// - /// Draw a Cubics bezier curve connecting the previous point to . - /// - /// The second control point. - /// The third control point. - /// The point. - void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + public void BeginFigure() { - // describes Cubic Bezier curve from the 'current point' using the - // 'second control point', 'third control point' and final 'point' - // leaving the 'current point' at 'point' } - /// - /// Draw a straight line connecting the previous point to . - /// - /// The point. - void IGlyphRenderer.LineTo(Vector2 point) + public void MoveTo(Vector2 point) => this.Points.Add(point); + + public void LineTo(Vector2 point) => this.Points.Add(point); + + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { - // describes straight line from the 'current point' to the final 'point' - // leaving the 'current point' at 'point' + this.Points.Add(secondControlPoint); + this.Points.Add(point); } - /// - /// Ends the figure. - /// - void IGlyphRenderer.EndFigure() + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { - // Called after the figure has completed denoting a straight line should - // be drawn from the current point to the first point + this.Points.Add(secondControlPoint); + this.Points.Add(thirdControlPoint); + this.Points.Add(point); } - /// - /// Ends the glyph. - /// - void IGlyphRenderer.EndGlyph() + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + => this.Points.Add(point); + + public void EndFigure() { - // says the all figures have completed for the current glyph/layer. - // NOTE this will be called even if BeginGlyph return false. } + public TextDecorations EnabledDecorations() => TextDecorations.None; - /// - /// Called once all glyphs have completed rendering - /// - void IGlyphRenderer.EndText() + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { - //once all glyphs/layers have been drawn this is called. } } ``` + +Render text to that surface with `TextRenderer`. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + ColorFontSupport = ColorFontSupport.ColrV1 | ColorFontSupport.Svg +}; + +RecordingGlyphRenderer renderer = new(); +TextRenderer.RenderTextTo(renderer, "Hello world", options); +``` + +Replace `"Segoe UI"` with any installed family that exists on your machine. + +### Layers, paints, and color fonts + +`BeginLayer(...)` is where Fonts communicates how the current glyph layer should be filled: + +- `paint` may be `null` for outline-only content +- `SolidPaint` represents a single color +- `LinearGradientPaint`, `RadialGradientPaint`, and `SweepGradientPaint` are used for richer color-font layers +- `fillRule` tells you how the path should be filled +- `clipBounds` provides an optional clip quad for the layer + +If your renderer only supports monochrome output, you can ignore `paint` and render every layer with your own brush. If you want color-font output, honor both `ColorFontSupport` in `TextOptions` and the `Paint` information delivered to `BeginLayer(...)`. + +See [Color Fonts](colorfonts.md) for a fuller guide to `ColorFontSupport`, painted glyphs, and the different color-font technologies that Fonts can surface. + +### Decorations + +Decorations are opt-in. Return the decorations you care about from `EnabledDecorations()`, and Fonts will call `SetDecoration(...)` after the glyph geometry has been emitted. + +```csharp +public TextDecorations EnabledDecorations() + => TextDecorations.Underline | TextDecorations.Strikeout; +``` + +This makes it possible to render underline, overline, or strikeout using the same backend as the glyph outlines. diff --git a/articles/fonts/fallbackfonts.md b/articles/fonts/fallbackfonts.md new file mode 100644 index 000000000..59467f078 --- /dev/null +++ b/articles/fonts/fallbackfonts.md @@ -0,0 +1,108 @@ +# Fallback Fonts and Multilingual Text + +Modern text often mixes scripts, emoji, and symbols that do not all exist in a single font. Fonts handles that through `TextOptions.FallbackFontFamilies`. + +When the primary `Font` does not contain a glyph for part of the text, the layout engine searches the fallback families in order and uses the first family that can supply the missing glyphs. + +### Use families, not fonts + +Fallback is configured with `FontFamily` instances, not `Font` instances. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily latin = collection.Add("fonts/NotoSans-Regular.ttf"); +FontFamily arabic = collection.Add("fonts/NotoSansArabic-Regular.ttf"); +FontFamily emoji = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); + +TextOptions options = new(latin.CreateFont(16)) +{ + FallbackFontFamilies = new[] { arabic, emoji } +}; +``` + +The primary font still controls the default point size and layout options. When a fallback family is selected, Fonts creates the matching font instance for that run automatically. + +### Order matters + +Fallback families are searched in the order you provide them. + +- Put script-specific fonts before more general fallback fonts. +- Put emoji fonts after your normal text families unless you explicitly want them to win earlier. +- Keep the fallback list as small and intentional as possible so the selection stays predictable. + +### Mixed-script example + +This pattern works well for text that mixes Latin, Arabic, and emoji: + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily latin = collection.Add("fonts/NotoSans-Regular.ttf"); +FontFamily arabic = collection.Add("fonts/NotoSansArabic-Regular.ttf"); +FontFamily emoji = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); + +string text = "Status: ready \U0001F600 \u0645\u0631\u062D\u0628\u0627"; + +TextOptions options = new(latin.CreateFont(18)) +{ + FallbackFontFamilies = new[] { arabic, emoji }, + TextDirection = TextDirection.Auto, + ColorFontSupport = ColorFontSupport.ColrV1 | ColorFontSupport.Svg +}; +``` + +`TextDirection.Auto` lets the layout engine determine whether a run should flow left-to-right or right-to-left. `ColorFontSupport` matters when one of your fallback families is a color emoji font. + +### Fallback is not the same as explicit styling + +Use fallback fonts when the goal is "use another family if the current one cannot render this text". + +Use `TextRuns` when the goal is "this specific range should use a different font even if the base font could render it". + +```csharp +using SixLabors.Fonts; + +const string text = "Latin title \u0627\u0644\u0639\u0631\u0628\u064A\u0629"; + +FontCollection collection = new(); +FontFamily latin = collection.Add("fonts/NotoSans-Regular.ttf"); +FontFamily arabic = collection.Add("fonts/NotoSansArabic-Regular.ttf"); + +TextOptions options = new(latin.CreateFont(18)) +{ + FallbackFontFamilies = new[] { arabic }, + TextRuns = new[] + { + new TextRun + { + Start = 12, + End = 19, + Font = arabic.CreateFont(18) + } + } +}; +``` + +The fallback list helps with missing glyphs. `TextRuns` gives you deliberate control over which grapheme ranges use which fonts. + +### Wrapping and script behavior + +Multilingual text often benefits from layout settings beyond just fallback families: + +- `TextDirection.Auto` for mixed LTR and RTL content +- `WordBreaking.KeepAll` or `WordBreaking.BreakWord` for CJK-heavy text +- `LayoutMode` for vertical scripts or mixed vertical presentation + +If a script needs shaping support, make sure the selected font actually supports that script. Fallback can only help if one of the supplied families contains the needed glyphs and shaping data. + +### Common pitfalls + +- A fallback family will not be used if the primary font already has a glyph for that Unicode scalar value, even if you would prefer the fallback font's design. +- `TextRuns` use grapheme indices, not UTF-16 code-unit indices. +- Emoji color layers are only used if `ColorFontSupport` allows them. +- Mixing many broad-coverage fonts can make fallback order hard to reason about. + +If layout still looks wrong after fallback is configured, see [Troubleshooting](troubleshooting.md). diff --git a/articles/fonts/fittexttowidth.md b/articles/fonts/fittexttowidth.md new file mode 100644 index 000000000..c34e824c8 --- /dev/null +++ b/articles/fonts/fittexttowidth.md @@ -0,0 +1,48 @@ +# Fit Text to a Target Width + +This recipe is useful when you need a heading, label, or short line of text to fit inside a fixed width. + +For single-line text, the usual pattern is: + +1. start with a candidate font size +2. measure with `TextMeasurer.MeasureAdvance(...)` +3. reduce the size until the width fits + +```csharp +using SixLabors.Fonts; + +const string text = "SixLabors.Fonts"; +const float targetWidth = 240; + +FontFamily family = SystemFonts.Get("Segoe UI"); +float fontSize = 32; +FontRectangle bounds = default; + +while (fontSize > 6) +{ + Font font = family.CreateFont(fontSize, FontStyle.Bold); + TextOptions options = new(font) + { + WrappingLength = -1 + }; + + bounds = TextMeasurer.MeasureAdvance(text, options); + if (bounds.Width <= targetWidth) + { + break; + } + + fontSize -= 1; +} + +Font fittedFont = family.CreateFont(fontSize, FontStyle.Bold); +``` + +This is a simple and predictable approach for titles and short labels. If you need more control, you can reduce in larger steps first and then refine more precisely near the final size. + +For multiline text, also set `WrappingLength` and measure with the same layout options you plan to render with. + +>[!NOTE] +>This example is intentionally naive. It remeasures from scratch on each iteration to keep the recipe easy to follow. Production layout engines would usually cache measurements, font instances, or intermediate fit results instead of doing a full linear probe every time. + +See [Measuring Text](measuringtext.md) and [Text Layout and Options](textlayout.md) for the fuller discussion. diff --git a/articles/fonts/fontmetadata.md b/articles/fonts/fontmetadata.md new file mode 100644 index 000000000..bf75c2c0d --- /dev/null +++ b/articles/fonts/fontmetadata.md @@ -0,0 +1,116 @@ +# Font Metadata and Inspection + +You do not need to fully load a font into a `FontCollection` just to inspect its names and basic face metadata. + +Fonts exposes `FontDescription` for that purpose. + +### Read metadata without loading the font for layout + +Use `FontDescription.LoadDescription(...)` when you only need descriptive information from a single font file or stream. + +```csharp +using System.Globalization; +using SixLabors.Fonts; +using SixLabors.Fonts.WellKnownIds; + +FontDescription description = FontDescription.LoadDescription("fonts/SourceSans3-Regular.ttf"); + +string family = description.FontFamilyInvariantCulture; +string fullName = description.FontNameInvariantCulture; +string subfamily = description.FontSubFamilyNameInvariantCulture; +string version = description.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.Version); +``` + +This is a better fit than `FontCollection.Add(...)` when you are building font pickers, diagnostics, import tools, or metadata listings. + +### Work with localized names + +`FontDescription` exposes both invariant and culture-aware name accessors: + +- `FontNameInvariantCulture` +- `FontFamilyInvariantCulture` +- `FontSubFamilyNameInvariantCulture` +- `FontName(culture)` +- `FontFamily(culture)` +- `FontSubFamilyName(culture)` + +```csharp +using System.Globalization; +using SixLabors.Fonts; + +FontDescription description = FontDescription.LoadDescription("fonts/SourceSans3-Regular.ttf"); +CultureInfo english = CultureInfo.GetCultureInfo("en-US"); + +string familyName = description.FontFamily(english); +``` + +### Read additional name-table entries + +Use `GetNameById(...)` with `KnownNameIds` when you need more than the basic family and subfamily fields. + +Common values include: + +- `KnownNameIds.Version` +- `KnownNameIds.PostscriptName` +- `KnownNameIds.Designer` +- `KnownNameIds.Manufacturer` +- `KnownNameIds.LicenseDescription` +- `KnownNameIds.LicenseInfoUrl` +- `KnownNameIds.SampleText` + +```csharp +using System.Globalization; +using SixLabors.Fonts; +using SixLabors.Fonts.WellKnownIds; + +FontDescription description = FontDescription.LoadDescription("fonts/SourceSans3-Regular.ttf"); + +string designer = description.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.Designer); +string sample = description.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.SampleText); +``` + +### Inspect font collections + +Use `FontDescription.LoadFontCollectionDescriptions(...)` when a file contains multiple faces, such as a `.ttc` collection. + +```csharp +using SixLabors.Fonts; + +FontDescription[] descriptions = + FontDescription.LoadFontCollectionDescriptions("fonts/NotoSansCJK-Regular.ttc"); +``` + +If you are loading a collection into a `FontCollection`, the `AddCollection(...)` overloads can also return the descriptions that were discovered during the load. + +### Inspect loaded families and fonts + +Once a family has been loaded, there are a few additional inspection helpers worth knowing about: + +- `FontFamily.GetAvailableStyles()` lists the styles currently available for that family in the collection +- `FontFamily.TryGetPaths(...)` returns source file paths when the family came from filesystem-backed fonts +- `Font.TryGetPath(...)` returns the backing file path for a concrete font instance when one exists +- `Font.FontMetrics.Description` exposes the same `FontDescription` for the resolved face + +```csharp +using System; +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/SourceSans3-Regular.ttf"); + +foreach (FontStyle style in family.GetAvailableStyles()) +{ + Console.WriteLine(style); +} + +Font font = family.CreateFont(16); +FontDescription description = font.FontMetrics.Description; +``` + +### What `Style` means + +`FontDescription.Style` is the resolved `FontStyle` for that face. Fonts derives it from the face metadata in the font tables, so it is a useful quick check when you want to know whether a face is marked as bold, italic, or both. + +For loading fonts into collections, see [Loading Fonts and Collections](gettingstarted.md). For working with installed machine fonts, see [System Fonts](systemfonts.md). + +If you want the face-level metrics that drive layout and glyph inspection rather than just the descriptive metadata, see [Font Metrics](fontmetrics.md). diff --git a/articles/fonts/fontmetrics.md b/articles/fonts/fontmetrics.md new file mode 100644 index 000000000..cbe133491 --- /dev/null +++ b/articles/fonts/fontmetrics.md @@ -0,0 +1,231 @@ +# Font Metrics + +`FontDescription` tells you what a face is called. `FontMetrics` tells you how that face behaves. + +Use `FontMetrics` when you need inspection data that affects layout, glyph selection, decoration placement, variation support, or code-point coverage. + +### How to get `FontMetrics` + +The most direct route is through a resolved `Font` instance: + +```csharp +using System; +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/SourceSans3-Regular.ttf"); +Font font = family.CreateFont(16); + +FontMetrics metrics = font.FontMetrics; +``` + +You can also inspect available faces on a family before you create a `Font`. + +```csharp +using System; +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/SourceSans3-Regular.ttf"); + +if (family.TryGetMetrics(FontStyle.Regular, out FontMetrics? metrics)) +{ + Console.WriteLine(metrics.Description.FontNameInvariantCulture); +} +``` + +### Description, units, and scale + +The core identity and scaling properties are: + +- `Description` for the face metadata +- `UnitsPerEm` for the design-space resolution of the font +- `ScaleFactor` for the face-level unit-to-point scaling used by glyph metrics + +`UnitsPerEm` is the important anchor for understanding almost every other metric on the typeface. Values like ascenders, underline positions, or glyph advances are stored in font units and should be interpreted relative to that em square. + +### Horizontal and vertical metrics + +`FontMetrics` exposes both `HorizontalMetrics` and `VerticalMetrics`. + +Both headers provide the same core fields: + +- `Ascender` +- `Descender` +- `LineGap` +- `LineHeight` +- `AdvanceWidthMax` +- `AdvanceHeightMax` + +The difference is not in the property names. It is in which layout direction those values are meant to describe. + +- `HorizontalMetrics` describes the face when text is laid out in horizontal modes such as `LayoutMode.HorizontalTopBottom` and `LayoutMode.HorizontalBottomTop`. +- `VerticalMetrics` describes the face when text is laid out in vertical modes such as `LayoutMode.VerticalLeftRight`, `LayoutMode.VerticalRightLeft`, `LayoutMode.VerticalMixedLeftRight`, and `LayoutMode.VerticalMixedRightLeft`. + +In practical terms: + +- use `HorizontalMetrics` for normal Latin-style line layout, UI text, paragraphs, and most measurement scenarios +- use `VerticalMetrics` for vertical text layout, especially CJK-oriented column flow and vertical glyph advance + +### What the fields mean + +`Ascender` and `Descender` define the font's recommended extents above and below the baseline for the layout direction you are inspecting. + +`LineGap` is the additional space the font recommends between lines or columns beyond the ascender and descender space. + +`LineHeight` is the face's typographic line spacing for that metrics header. If you want the font's default line advance, this is usually the most direct value to start from. + +`AdvanceWidthMax` is the maximum glyph advance width in that face. + +`AdvanceHeightMax` is the maximum glyph advance height in that face. This matters most for vertical layout. For fonts that do not provide dedicated vertical metrics, this value falls back to the line height. + +### When to use `HorizontalMetrics` + +Reach for `HorizontalMetrics` when you need: + +- default line spacing for ordinary left-to-right or right-to-left text +- baseline, ascender, and descender values for UI layout or custom renderers +- a face-level sanity check before measuring or clipping horizontal text +- maximum advance budgeting for horizontally flowing glyphs + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 16); +FontMetrics metrics = font.FontMetrics; + +short ascender = metrics.HorizontalMetrics.Ascender; +short descender = metrics.HorizontalMetrics.Descender; +short lineHeight = metrics.HorizontalMetrics.LineHeight; +short maxAdvanceWidth = metrics.HorizontalMetrics.AdvanceWidthMax; +``` + +### When to use `VerticalMetrics` + +Reach for `VerticalMetrics` when you need: + +- default line or column spacing for vertical layout +- face-level values for custom vertical renderers +- the maximum advance height budget for vertical glyph flow +- inspection of whether a font behaves sensibly in vertical layout + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 16); +FontMetrics metrics = font.FontMetrics; + +short verticalAscender = metrics.VerticalMetrics.Ascender; +short verticalLineHeight = metrics.VerticalMetrics.LineHeight; +short maxAdvanceHeight = metrics.VerticalMetrics.AdvanceHeightMax; +``` + +These values are expressed in font units, not pixels. + +### Decoration and script-positioning metrics + +`FontMetrics` also exposes the face-level metrics that support decoration and typographic adjustments: + +- `UnderlinePosition` +- `UnderlineThickness` +- `StrikeoutPosition` +- `StrikeoutSize` +- `SubscriptXSize` +- `SubscriptYSize` +- `SubscriptXOffset` +- `SubscriptYOffset` +- `SuperscriptXSize` +- `SuperscriptYSize` +- `SuperscriptXOffset` +- `SuperscriptYOffset` +- `ItalicAngle` + +These are useful when you are building your own renderer, diagnostics, or typography tools and want the font's own recommendations rather than hard-coded values. + +### Variable-font support + +`FontMetrics.TryGetVariationAxes(...)` lets you inspect the variation axes that the resolved face supports. + +```csharp +using System; +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); +Font font = family.CreateFont(16); + +if (font.FontMetrics.TryGetVariationAxes(out VariationAxis[]? axes)) +{ + foreach (VariationAxis axis in axes) + { + Console.WriteLine($"{axis.Tag}: {axis.Min}..{axis.Max} (default {axis.Default})"); + } +} +``` + +Each `VariationAxis` gives you: + +- `Name` +- `Tag` +- `Min` +- `Max` +- `Default` + +The registered tags in `KnownVariationAxes` such as `wght`, `wdth`, `opsz`, `slnt`, and `ital` are useful when you want to relate those exposed axes back to font creation with `FontVariation`. + +### Code-point coverage + +Use `GetAvailableCodePoints()` when you need to know which Unicode scalar values the face can map directly. + +```csharp +using System.Collections.Generic; +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; + +Font font = SystemFonts.CreateFont("Segoe UI", 16); +IReadOnlyList codePoints = font.FontMetrics.GetAvailableCodePoints(); +``` + +This is useful for diagnostics, glyph coverage tooling, fallback decisions, and script-support inspection. + +### Inspect glyph metrics directly + +If you need glyph-level inspection without going through full text layout, use `TryGetGlyphMetrics(...)`. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; + +Font font = SystemFonts.CreateFont("Segoe UI", 16); + +if (font.FontMetrics.TryGetGlyphMetrics( + new CodePoint('A'), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out GlyphMetrics? glyphMetrics)) +{ + float width = glyphMetrics.Width; + ushort advance = glyphMetrics.AdvanceWidth; + GlyphType glyphType = glyphMetrics.GlyphType; +} +``` + +This is the lower-level face inspection API behind the higher-level `Font.TryGetGlyphs(...)` helpers. + +### When to use `FontMetrics` vs `FontDescription` + +Use `FontDescription` when you care about names and face identity. + +Use `FontMetrics` when you care about: + +- line and em metrics +- underline and strikeout placement +- subscript and superscript recommendations +- variation-axis availability +- code-point coverage +- direct glyph inspection + +For face names and other descriptive metadata, see [Font Metadata and Inspection](fontmetadata.md). For variable-font usage, see [Variable Fonts](variablefonts.md). diff --git a/articles/fonts/gettingstarted.md b/articles/fonts/gettingstarted.md index 92ca9ea7f..2a3ab175b 100644 --- a/articles/fonts/gettingstarted.md +++ b/articles/fonts/gettingstarted.md @@ -1,56 +1,131 @@ -# Getting Started +# Loading Fonts and Collections ->[!NOTE] ->The official guide assumes intermediate level knowledge of C# and .NET. If you are totally new to .NET development, it might not be the best idea to jump right into a framework as your first step - grasp the basics then come back. Prior experience with other languages and frameworks helps, but is not required. +Fonts separates font discovery from text layout: -### Fonts +- `FontCollection` stores the families you load. +- `FontFamily` represents a family and the styles available for it. +- `Font` represents a concrete instance of a family at a given point size, style, and optional variation settings. +- `SystemFonts` gives you access to the fonts installed on the current machine. -Fonts provides the core to your text layout and loading subsystems. +### Load a single font -- `SixLabors.Fonts.FontCollection` is the root type you will configure and load up with all the TrueType/OpenType/Woff/Woff2 fonts. (Font loading is deemed expensive and should be done once and shared across multiple rasterizations) -- `SixLabors.Fonts.Font` is our currying type for passing information about your chosen font face. +Use `FontCollection.Add(...)` when you want to register an individual font file such as a `.ttf`, `.otf`, `.woff`, or `.woff2`. -### Loading Fonts +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/SourceSans3-Regular.ttf"); +Font font = family.CreateFont(16, FontStyle.Regular); +``` + +`Font.Size` is expressed in points. Measurement and rendering are then converted to pixels using `TextOptions.Dpi`. -Fonts provides several options for loading fonts, you can load then from a streams or files, we also support loading collections out of `.ttc` files and well as single variants from individual `.ttf` files. We also support loading `.woff`, and `.woff2` files. +### Load from a stream and inspect metadata -#### Minimal Example +The `Add(...)` overloads can also return a `FontDescription`, which is useful when you want to inspect what was loaded. -```c# +```csharp +using System.IO; using SixLabors.Fonts; FontCollection collection = new(); -FontFamily family = collection.Add("path/to/font.ttf"); -Font font = family.CreateFont(12, FontStyle.Italic); -// "font" can now be used in calls to DrawText from our ImageSharp.Drawing library. +using FileStream stream = File.OpenRead("fonts/SourceSans3-Regular.ttf"); +FontFamily family = collection.Add(stream, out FontDescription description); +string familyName = description.FontFamilyInvariantCulture; +Font font = family.CreateFont(16); ``` -#### Expanded Example +If you only need metadata, use `FontDescription.LoadDescription(...)` or `FontDescription.LoadFontCollectionDescriptions(...)` instead of adding the font to a collection. See [Font Metadata and Inspection](fontmetadata.md) for more detail. + +### Load a font collection -```c# +Use `AddCollection(...)` for files that contain multiple faces, such as `.ttc` collections. + +```csharp using SixLabors.Fonts; FontCollection collection = new(); -collection.Add("path/to/font.ttf"); -collection.Add("path/to/font2.ttf"); -collection.Add("path/to/emojiFont.ttf"); -collection.AddCollection("path/to/font.ttc"); +var families = collection.AddCollection("fonts/NotoSansCJK-Regular.ttc"); +``` -if(collection.TryGet("Font Name", out FontFamily family)) -if(collection.TryGet("Emoji Font Name", out FontFamily emojiFamily)) -{ - // family will not be null here - Font font = family.CreateFont(12, FontStyle.Italic); +### Resolve families by name + +Once fonts are loaded, resolve a family with `Get(...)` or `TryGet(...)`. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +collection.Add("fonts/SourceSans3-Regular.ttf"); +collection.Add("fonts/NotoColorEmoji-Regular.ttf"); - // TextOptions provides comprehensive customization options support - TextOptions options = new(font) +if (collection.TryGet("Source Sans 3", out FontFamily textFamily) && + collection.TryGet("Noto Color Emoji", out FontFamily emojiFamily)) +{ + TextOptions options = new(textFamily.CreateFont(16)) { - // Will be used if a particular code point doesn't exist in the font passed into the constructor. (e.g. emoji) - FallbackFontFamilies = new [] { emojiFamily } + FallbackFontFamilies = new[] { emojiFamily } }; - - FontRectangle rect = TextMeasurer.MeasureAdvance("Text to measure", options); } ``` + +`FallbackFontFamilies` is a list of `FontFamily` instances, not `Font` instances. Fonts are created after the fallback family is selected for a run. + +### Use system fonts + +If you want to work with fonts installed on the current machine, use `SystemFonts`. + +```csharp +using SixLabors.Fonts; + +Font caption = SystemFonts.CreateFont("Segoe UI", 12); +Font heading = SystemFonts.CreateFont("Segoe UI", 24, FontStyle.Bold); +``` + +Replace `"Segoe UI"` with any installed family that exists on your machine. + +You can also merge the system font set into your own `FontCollection`. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +collection.AddSystemFonts(); +collection.Add("fonts/BrandSans-Regular.ttf"); +``` + +When you need localized family-name lookup, use `AddWithCulture(...)`, `GetByCulture(...)`, or `TryGetByCulture(...)`. + +See [System Fonts](systemfonts.md) for the fuller system-font API surface, including enumeration, culture-aware lookup, and `SearchDirectories`. + +### Create variable-font instances + +Variable fonts are exposed through `FontVariation` and `KnownVariationAxes`. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); + +Font font = family.CreateFont( + 16, + new FontVariation(KnownVariationAxes.Weight, 700), + new FontVariation(KnownVariationAxes.OpticalSize, 16)); +``` + +The active variation values become part of the `Font` instance, so the same family can be reused to create multiple design-space instances. + +### Next steps + +- Use [Measuring Text](measuringtext.md) when you need layout metrics before rendering. +- Use [System Fonts](systemfonts.md) when you want to inspect or consume the fonts installed on the current machine. +- Use [Font Metadata and Inspection](fontmetadata.md) when you need names, styles, or version information without loading a font for shaping. +- Use [Text Layout and Options](textlayout.md) to control wrapping, alignment, direction, shaping, fallback fonts, and text runs. +- Use [OpenType Features](opentypefeatures.md) when you want to request fractions, tabular figures, stylistic sets, or other font features explicitly. +- Use [Fallback Fonts and Multilingual Text](fallbackfonts.md) when one family is not enough for your content. +- Use [Variable Fonts](variablefonts.md) when you want to work with weight, width, optical size, or custom axes from a single font file. +- Use [Custom Rendering](customrendering.md) if you need to render glyph geometry to your own output surface. diff --git a/articles/fonts/hintingandshaping.md b/articles/fonts/hintingandshaping.md new file mode 100644 index 000000000..1663fd022 --- /dev/null +++ b/articles/fonts/hintingandshaping.md @@ -0,0 +1,160 @@ +# Hinting and Shaping + +Hinting and shaping both affect the final appearance of text, but they are not the same stage and they do not solve the same problem. + +Shaping answers "which glyphs should this text use, and where do those glyphs go?" + +Hinting answers "how should this specific TrueType outline be adjusted for this size and DPI so it lands cleanly on the pixel grid?" + +Fonts has comprehensive support for both, but the scope is different: + +- shaping is a full text-layout concern and runs for all normal measurement and rendering +- hinting is a TrueType outline concern and runs when hinted glyph outlines are materialized + +### The short version + +| Topic | Shaping | Hinting | +| --- | --- | --- | +| Input | Unicode text, script, direction, font selection, OpenType features | A concrete glyph outline, size, and DPI | +| Output | The final glyph sequence and glyph positions | A grid-fitted outline for raster-oriented rendering | +| Main goal | Correct text layout and glyph choice | Better small-size screen rendering | +| Controlled by | `TextDirection`, `FeatureTags`, `KerningMode`, `Tracking`, `TextRuns`, `FallbackFontFamilies`, `LayoutMode` | `HintingMode` | + +### What shaping means + +Shaping is the process of turning text into the glyph sequence a font actually needs. + +That is more than a simple character-to-glyph lookup. A shaping engine may need to: + +- choose different glyph forms depending on neighboring text +- form ligatures such as `ffi` +- apply fractions or numeral variants +- position marks relative to a base glyph +- apply kerning and cursive attachment +- reorder glyphs for complex scripts +- resolve bidirectional text and mirrored forms + +In other words, shaping works at the typography level. It decides what the text is supposed to look like before any pixel-grid tuning happens. + +### Fonts has comprehensive shaping support + +Fonts does not require a separate public shaping API for normal use because shaping is built into the layout engine that backs both `TextMeasurer` and `TextRenderer`. + +That shaping support includes: + +- full OpenType layout processing through GSUB and GPOS +- bidirectional analysis and automatic direction handling through `TextDirection.Auto` +- mirrored-form substitution for right-to-left text where required +- script-aware shapers in the codebase for Arabic, Hangul, Hebrew, Indic, Myanmar, and Thai/Lao text +- a Universal Shaping Engine for additional complex scripts +- kerning, ligatures, fractions, tabular figures, vertical alternates, and other OpenType feature-driven behaviors +- font fallback and per-range font selection through `FallbackFontFamilies` and `TextRuns` + +This is why measurement and rendering stay aligned when you use the same `TextOptions` instance for both. Fonts measures shaped text, not a simplified pre-layout approximation. + +### What you control in shaping + +The main shaping controls are: + +- `TextDirection` to force left-to-right, right-to-left, or automatic bidi resolution +- `LayoutMode` for horizontal and vertical layout behavior +- `FeatureTags` to request additional OpenType features such as fractions or tabular figures +- `KerningMode` to enable or disable font-provided kerning during shaping +- `Tracking` to add uniform letter spacing after the font's own spacing behavior +- `FallbackFontFamilies` when the main font does not cover every glyph you need +- `TextRuns` when different text ranges need different fonts, attributes, or decorations + +Required script shaping still happens automatically. `FeatureTags` is for extra typographic features you want to request on top of that baseline shaping behavior. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + TextDirection = TextDirection.Auto, + KerningMode = KerningMode.Standard, + FeatureTags = new Tag[] + { + KnownFeatureTags.Fractions, + KnownFeatureTags.TabularFigures + } +}; + +FontRectangle bounds = TextMeasurer.MeasureAdvance("9/2", options); +``` + +### What hinting means + +Hinting is not about choosing glyphs. It is about adjusting the points in a glyph outline so the shape lands better on the pixel grid at a particular size and DPI. + +That matters most at smaller sizes, where a one-pixel decision can noticeably affect: + +- stem thickness +- counter shape +- bar height +- baseline alignment +- mark attachment consistency + +At larger sizes the difference is usually much smaller because the outline already has enough pixel resolution to describe itself cleanly. + +### Fonts has comprehensive TrueType hinting support + +Within the scope of TrueType outlines, Fonts has comprehensive hinting support. + +`TextOptions.HintingMode` controls whether that hinting path is active: + +- `HintingMode.None` leaves outlines unhinted +- `HintingMode.Standard` applies the library's FreeType v40-compatible TrueType hinting behavior + +That means Fonts uses a modern screen-oriented TrueType hinting model rather than treating hinting as old black-and-white full-grid-fitting for legacy CRT text. + +The hinting pipeline in Fonts includes: + +- TrueType glyph instruction execution +- support for the standard TrueType hinting tables such as `fpgm`, `prep`, and `cvt` +- per-glyph hinting at the active size and DPI +- `cvar`-driven control-value adjustments for variable TrueType fonts before hinting runs +- hinted contour-point resolution for GPOS anchor data when the font uses contour-point anchors + +This is specifically a TrueType feature. Fonts only applies this hinting path to TrueType glyph data, so CFF and CFF2 outlines do not gain extra hinting behavior from `HintingMode.Standard`. + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 11); +TextOptions options = new(font) +{ + Dpi = 96, + HintingMode = HintingMode.Standard +}; +``` + +### Hinting and shaping are separate stages + +The easiest way to think about the pipeline is: + +1. Fonts analyzes the text, script, direction, features, and font selection. +2. Fonts shapes the text into the correct glyph sequence and glyph positions. +3. If the resolved glyphs are TrueType outlines and hinting is enabled, Fonts adjusts those outlines for the current size and DPI. + +So: + +- shaping decides which glyphs you get and where they belong +- hinting adjusts how those resolved glyphs are fit to the raster grid + +Hinting does not choose ligatures, apply Arabic joining, reorder Indic glyphs, or enable OpenType features. Those are shaping concerns. + +Shaping does not grid-fit outlines. It decides the typographic result that hinting may later refine for small-size raster output. + +### Practical guidance + +- Use `TextDirection.Auto` unless you have a specific reason to force directionality. +- Use `FallbackFontFamilies` for multilingual text, emoji, and scripts your main font does not cover. +- Use `FeatureTags` for discretionary features such as fractions, stylistic sets, or tabular figures. +- Use `HintingMode.Standard` when rendering small TrueType UI text and leave it off when you want the raw outline behavior. +- Treat shaping as a typography and layout concern. +- Treat hinting as a size-dependent TrueType raster-quality concern. + +For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For multilingual font fallback, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). diff --git a/articles/fonts/index.md b/articles/fonts/index.md index da4539d48..9f8570af7 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -1,18 +1,21 @@ # Introduction ### What is Fonts? -Fonts is a font loading and layout library built primarily to provide text drawing support to ImageSharp.Drawing. +Fonts is a cross-platform library for loading, measuring, and laying out fonts and text. + +It supports TrueType and OpenType fonts, including CFF1 and CFF2 outlines, WOFF and WOFF2 web fonts, variable fonts, color fonts, advanced OpenType layout, complex script shaping, and bidirectional text rendering. + +Fonts is often used underneath [ImageSharp.Drawing](../imagesharp.drawing/index.md), but it is not limited to image rendering. You can also use it for font inspection, text measurement, shaping, and custom rendering pipelines. + +### License -Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), Fonts can be used in device, cloud, and embedded/IoT scenarios. - -### License Fonts is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/Fonts/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] >Starting with Fonts 3.0.0, projects that directly depend on SixLabors.Fonts require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. - + ### Installation - + Fonts is installed via [NuGet](https://www.nuget.org/packages/SixLabors.Fonts) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) @@ -43,3 +46,21 @@ paket add SixLabors.Fonts --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. + +### Start Here + +- [Loading Fonts and Collections](gettingstarted.md) +- [System Fonts](systemfonts.md) +- [Font Metadata and Inspection](fontmetadata.md) +- [Font Metrics](fontmetrics.md) +- [Measuring Text](measuringtext.md) +- [Text Layout and Options](textlayout.md) +- [OpenType Features](opentypefeatures.md) +- [Hinting and Shaping](hintingandshaping.md) +- [Color Fonts](colorfonts.md) +- [Unicode, Code Points, and Graphemes](unicode.md) +- [Fallback Fonts and Multilingual Text](fallbackfonts.md) +- [Variable Fonts](variablefonts.md) +- [Custom Rendering](customrendering.md) +- [Recipes](recipes.md) +- [Troubleshooting](troubleshooting.md) diff --git a/articles/fonts/inspectfontfiles.md b/articles/fonts/inspectfontfiles.md new file mode 100644 index 000000000..a34a9be11 --- /dev/null +++ b/articles/fonts/inspectfontfiles.md @@ -0,0 +1,39 @@ +# Inspect Font Files and Collections + +Use this recipe when you want to inspect a font file before adding it to a `FontCollection`. + +### Read a single font file + +```csharp +using System.Globalization; +using SixLabors.Fonts; +using SixLabors.Fonts.WellKnownIds; + +FontDescription description = FontDescription.LoadDescription("fonts/SourceSans3-Regular.ttf"); + +string family = description.FontFamilyInvariantCulture; +string fullName = description.FontNameInvariantCulture; +string subfamily = description.FontSubFamilyNameInvariantCulture; +string version = description.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.Version); +``` + +This is useful for import tools, font pickers, diagnostics, and file-inspection utilities. + +### Inspect a font collection such as a `.ttc` + +```csharp +using System; +using SixLabors.Fonts; + +FontDescription[] descriptions = + FontDescription.LoadFontCollectionDescriptions("fonts/NotoSansCJK-Regular.ttc"); + +foreach (FontDescription description in descriptions) +{ + Console.WriteLine(description.FontNameInvariantCulture); +} +``` + +If you do want to load the collection afterward, use `FontCollection.AddCollection(...)`. + +For the broader metadata API, see [Font Metadata and Inspection](fontmetadata.md). diff --git a/articles/fonts/listsystemfonts.md b/articles/fonts/listsystemfonts.md new file mode 100644 index 000000000..d136377b3 --- /dev/null +++ b/articles/fonts/listsystemfonts.md @@ -0,0 +1,45 @@ +# List System Fonts and Resolve by Culture + +Use this recipe when you want to inspect what the current machine exposes through `SystemFonts`. + +### List installed families + +```csharp +using System; +using SixLabors.Fonts; + +foreach (FontFamily family in SystemFonts.Families) +{ + Console.WriteLine(family.Name); +} +``` + +### Show the searched directories + +```csharp +using System; +using SixLabors.Fonts; + +foreach (string directory in SystemFonts.Collection.SearchDirectories) +{ + Console.WriteLine(directory); +} +``` + +### Resolve a family by culture-aware name + +```csharp +using System.Globalization; +using SixLabors.Fonts; + +CultureInfo japanese = CultureInfo.GetCultureInfo("ja-JP"); + +if (SystemFonts.TryGetByCulture("Yu Gothic", japanese, out FontFamily family)) +{ + Font font = family.CreateFont(16); +} +``` + +This is especially useful when a family's localized name differs from the invariant name you would use elsewhere. + +For the fuller system-font API surface, see [System Fonts](systemfonts.md). diff --git a/articles/fonts/measuringtext.md b/articles/fonts/measuringtext.md new file mode 100644 index 000000000..991184843 --- /dev/null +++ b/articles/fonts/measuringtext.md @@ -0,0 +1,107 @@ +# Measuring Text + +`TextMeasurer` lets you run the same layout engine that rendering uses, without drawing anything. This is the right tool when you need to size a container, choose a wrapping width, place labels, or inspect line metrics before you render. + +### Choose the right measurement + +- `MeasureAdvance(...)` returns the logical advance rectangle from layout, including line height and advance. +- `MeasureBounds(...)` returns only the tight rendered glyph ink bounds. +- `MeasureRenderableBounds(...)` returns the union of the logical advance rectangle and the glyph ink bounds. +- `MeasureSize(...)` returns the rendered width and height normalized to `(0, 0)`. + +The important distinction is that glyph geometry and layout geometry are not the same thing. Glyphs can overshoot the logical advance box, and the logical advance box can also include space that no glyph pixels occupy. + +### Measure a block of text + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 320 +}; + +FontRectangle advance = TextMeasurer.MeasureAdvance("Hello world", options); +FontRectangle bounds = TextMeasurer.MeasureBounds("Hello world", options); +FontRectangle renderable = TextMeasurer.MeasureRenderableBounds("Hello world", options); +FontRectangle size = TextMeasurer.MeasureSize("Hello world", options); +``` + +Replace `"Segoe UI"` with any installed family that exists on your machine. + +Use `MeasureAdvance(...)` when you care about layout flow, alignment, wrapping, or line-box size. + +Use `MeasureBounds(...)` when you want the pure glyph bounds only. + +Use `MeasureRenderableBounds(...)` when you need the full rendered area that combines layout space and glyph overshoot. + +### Understand bounds and origin + +`MeasureBounds(...)` returns absolute glyph bounds only, so the returned `X` and `Y` can be non-zero, and the width and height reflect only where glyph ink exists. + +`MeasureRenderableBounds(...)` returns a larger conceptual rectangle when needed: it includes the full logical advance rectangle from layout and then expands that rectangle to also include any glyph ink that extends beyond it. + +`MeasureSize(...)` is the rendered glyph-bounds measurement normalized to width and height only. + +If you need a rectangle that can safely contain both the typographic layout box and any glyph overshoot, prefer `MeasureRenderableBounds(...)`. + +### Measure per-character entries + +Fonts can also expose measurements for each laid-out entry. + +```csharp +using System; +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font); + +if (TextMeasurer.TryMeasureCharacterBounds("Hello", options, out ReadOnlySpan bounds)) +{ + GlyphBounds first = bounds[0]; +} +``` + +These APIs measure laid-out output, not raw UTF-16 code units, so do not assume a one-to-one mapping with the original string in the presence of shaping, ligatures, or complex scripts. + +If you need a refresher on the difference between UTF-16 code units, `CodePoint` values, and graphemes, see [Unicode, Code Points, and Graphemes](unicode.md). + +Available per-entry methods include: + +- `TryMeasureCharacterAdvances(...)` +- `TryMeasureCharacterSizes(...)` +- `TryMeasureCharacterBounds(...)` +- `TryMeasureCharacterRenderableBounds(...)` + +### Measure lines + +When you care about wrapped text, use `CountLines(...)` and `GetLineMetrics(...)`. + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 320 +}; + +int lineCount = TextMeasurer.CountLines("Hello world from Fonts", options); +LineMetrics[] lines = TextMeasurer.GetLineMetrics("Hello world from Fonts", options); +``` + +Each `LineMetrics` entry includes: + +- `Ascender`: the ascender guide position within the line box. This marks where tall glyphs such as `H` or `l` typically rise to. +- `Baseline`: the baseline position within the line box. This is the line most glyphs sit on. +- `Descender`: the descender guide position within the line box. This marks where descending glyph parts such as `g`, `p`, or `y` typically fall to. +- `LineHeight`: the total height of the line box after line spacing has been applied. +- `Start`: the aligned start position of the line in the primary flow direction. +- `Extent`: the size of the line in the primary flow direction. + +In horizontal layouts, `Start` is the X position and `Extent` is the line width. In vertical layouts, `Start` is the Y position and `Extent` is the line height. + +### Keep measurement and rendering aligned + +Always measure with the same `TextOptions` that you intend to render with. `Dpi`, `LineSpacing`, `WrappingLength`, `TextDirection`, `LayoutMode`, `KerningMode`, `Tracking`, `FeatureTags`, `TextRuns`, and fallback fonts all affect the final layout. diff --git a/articles/fonts/opentypefeatures.md b/articles/fonts/opentypefeatures.md new file mode 100644 index 000000000..ab61e4273 --- /dev/null +++ b/articles/fonts/opentypefeatures.md @@ -0,0 +1,123 @@ +# OpenType Features + +Fonts applies the shaping features that are required for correct layout automatically. `TextOptions.FeatureTags` is for the additional OpenType features you want to request on top of that baseline behavior. + +That makes it a typography control, not a substitute for the shaping engine. + +### How `FeatureTags` works + +`TextOptions.FeatureTags` is an `IReadOnlyList`. + +You can populate it with: + +- named values from `KnownFeatureTags` +- raw four-character tags parsed with `Tag.Parse(...)` + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] + { + KnownFeatureTags.Fractions, + KnownFeatureTags.TabularFigures, + Tag.Parse("ss01") + } +}; +``` + +A requested feature only has an effect if the font actually supports it. + +### When to use feature tags + +Use explicit feature tags for discretionary typographic behavior such as: + +- fractions +- tabular figures +- oldstyle figures +- discretionary ligatures +- stylistic sets +- small capitals +- case-sensitive punctuation +- vertical alternates + +Do not think of `FeatureTags` as a way to manually replace the shaping engine. Core script shaping, bidi handling, and other required layout behavior are already handled by Fonts. + +### Common feature examples + +Fractions: + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); + TextOptions options = new(font) +{ + FeatureTags = new Tag[] { KnownFeatureTags.Fractions } +}; +``` + +Tabular figures for aligned numeric columns: + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] { KnownFeatureTags.TabularFigures } +}; +``` + +Oldstyle figures plus discretionary ligatures: + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] + { + KnownFeatureTags.OldstyleFigures, + KnownFeatureTags.DiscretionaryLigatures + } +}; +``` + +Raw stylistic-set tag: + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] { Tag.Parse("ss01") } +}; +``` + +### Named tags vs raw tags + +Prefer the `KnownFeatureTags` enum when the feature already has a named constant in the library. Use `Tag.Parse(...)` for raw feature tags that you know exist in the target font but that you want to specify directly in your code. + +`Tag.Parse(...)` expects a four-character tag such as `"liga"`, `"frac"`, or `"ss01"`. + +### Feature tags and layout + +Feature requests participate in shaping, so they affect both measurement and rendering. If you want the measured result to match the rendered result, use the same `TextOptions` instance for both `TextMeasurer` and `TextRenderer`. + +### Vertical layout + +Some OpenType features are especially relevant in vertical layout, such as `KnownFeatureTags.VerticalAlternates`, `KnownFeatureTags.VerticalAlternatesAndRotation`, and `KnownFeatureTags.VerticalAlternatesForRotation`. + +Those work alongside `LayoutMode`; they do not replace it. + +For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For the broader shaping pipeline, see [Hinting and Shaping](hintingandshaping.md). diff --git a/articles/fonts/recipes.md b/articles/fonts/recipes.md new file mode 100644 index 000000000..aa4311f55 --- /dev/null +++ b/articles/fonts/recipes.md @@ -0,0 +1,11 @@ +# Recipes + +These short recipes show common Fonts workflows without the full conceptual discussion from the main guides. + +- [Fit Text to a Target Width](fittexttowidth.md) +- [Inspect Font Files and Collections](inspectfontfiles.md) +- [List System Fonts and Resolve by Culture](listsystemfonts.md) +- [Use OpenType Features for Numbers and Fractions](useopentypefeatures.md) +- [Check Glyph Coverage Before Choosing Fallbacks](checkglyphcoverage.md) + +Use the conceptual guides when you need the bigger picture. Use these recipes when you want a practical starting point quickly. diff --git a/articles/fonts/systemfonts.md b/articles/fonts/systemfonts.md new file mode 100644 index 000000000..849bb4bf4 --- /dev/null +++ b/articles/fonts/systemfonts.md @@ -0,0 +1,104 @@ +# System Fonts + +`SystemFonts` gives you access to the fonts installed on the current machine. + +Use it when you want to work with platform fonts directly instead of loading files into your own `FontCollection`. + +### What `SystemFonts` exposes + +The main entry points are: + +- `SystemFonts.Families` to enumerate installed families +- `SystemFonts.Get(...)` and `SystemFonts.TryGet(...)` to resolve a family by invariant name +- `SystemFonts.CreateFont(...)` to create a `Font` directly +- `SystemFonts.Collection` when you also need access to the searched directories + +```csharp +using SixLabors.Fonts; + +Font caption = SystemFonts.CreateFont("Segoe UI", 12); +Font heading = SystemFonts.CreateFont("Segoe UI", 24, FontStyle.Bold); +``` + +Replace `"Segoe UI"` with any installed family that exists on your machine. + +### Enumerate available families + +Use `SystemFonts.Families` when you want to inspect what the current environment actually exposes. + +```csharp +using System; +using SixLabors.Fonts; + +foreach (FontFamily family in SystemFonts.Families) +{ + Console.WriteLine(family.Name); +} +``` + +### Use culture-aware lookup + +Font family names can vary by culture, so `SystemFonts` also exposes the same culture-aware lookup helpers as `FontCollection`. + +```csharp +using System.Globalization; +using SixLabors.Fonts; + +CultureInfo japanese = CultureInfo.GetCultureInfo("ja-JP"); + +if (SystemFonts.TryGetByCulture("Yu Gothic", japanese, out FontFamily family)) +{ + Font font = family.CreateFont(16); +} +``` + +You can also create a font directly with the culture-aware `CreateFont(...)` overloads. + +### Merge system fonts into your own collection + +If you want your own custom fonts and the machine fonts in one lookup surface, copy the system font set into a `FontCollection`. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +collection.AddSystemFonts(); +collection.Add("fonts/BrandSans-Regular.ttf"); +``` + +There is also a filtered overload when you only want a subset of the installed fonts. + +```csharp +using System; +using SixLabors.Fonts; + +FontCollection collection = new(); +collection.AddSystemFonts(metric => + metric.Description.FontFamilyInvariantCulture.Contains("Noto", StringComparison.OrdinalIgnoreCase)); +``` + +### Search directories + +`SystemFonts.Collection` implements `IReadOnlySystemFontCollection`, which exposes `SearchDirectories`. + +That is useful for diagnostics and for understanding where the current process looked for fonts. + +```csharp +using System; +using SixLabors.Fonts; + +foreach (string directory in SystemFonts.Collection.SearchDirectories) +{ + Console.WriteLine(directory); +} +``` + +### Portability considerations + +The available system fonts are environment-specific. + +- Windows, Linux, macOS, containers, and CI agents will often expose different families. +- A family name that exists on your dev machine may not exist in production. +- If predictable output matters, prefer shipping the fonts you need and loading them into a `FontCollection`. + +For file-based loading, see [Loading Fonts and Collections](gettingstarted.md). For metadata-only inspection, see [Font Metadata and Inspection](fontmetadata.md). diff --git a/articles/fonts/textlayout.md b/articles/fonts/textlayout.md new file mode 100644 index 000000000..7ea3738c1 --- /dev/null +++ b/articles/fonts/textlayout.md @@ -0,0 +1,174 @@ +# Text Layout and Options + +`TextOptions` is the central layout object in Fonts. The same options type is used by both `TextMeasurer` and `TextRenderer`, which makes it easy to keep measurement and rendering in sync. + +### Core units + +`Font.Size` is expressed in points. `TextOptions.Dpi` controls how that size is converted into pixels for measurement and rendering. The default DPI is `72`. + +`WrappingLength` is expressed in pixels and defines when text wraps. `Origin` sets the rendering origin used by the layout engine. + +```csharp +using System.Numerics; +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + Dpi = 96, + Origin = new Vector2(20, 40), + WrappingLength = 480 +}; +``` + +Replace `"Segoe UI"` with any installed family that exists on your machine in the `SystemFonts` examples on this page. + +### Wrapping, flow, and direction + +These properties control how text is broken into lines and laid out: + +- `WrappingLength` +- `WordBreaking` +- `TextDirection` +- `LayoutMode` + +`WordBreaking` supports `Standard`, `BreakAll`, `KeepAll`, and `BreakWord`. `TextDirection` supports left-to-right, right-to-left, and automatic detection. `LayoutMode` supports horizontal and vertical layouts, including mixed vertical modes that rotate horizontal glyphs. + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 320, + WordBreaking = WordBreaking.BreakWord, + TextDirection = TextDirection.Auto, + LayoutMode = LayoutMode.HorizontalTopBottom +}; +``` + +### Alignment and justification + +`TextAlignment` expresses logical alignment within the text box using `Start`, `End`, and `Center`, and it respects the active text direction. `TextJustification` controls whether additional spacing is distributed between words or between characters. + +`HorizontalAlignment` and `VerticalAlignment` give you physical alignment controls for the layout box itself. + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 320, + TextAlignment = TextAlignment.Start, + TextJustification = TextJustification.InterWord, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top +}; +``` + +### Spacing, hinting, and shaping controls + +Fonts exposes several knobs that directly affect glyph layout: + +- `LineSpacing` multiplies the line height. +- `TabWidth` controls tab stops in space units. +- `KerningMode` enables, disables, or lets the engine decide about font-provided kerning during shaping. +- `Tracking` applies uniform letter-spacing and is measured in em. +- `HintingMode` is separate from shaping and controls TrueType grid fitting for the current size and DPI. + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + LineSpacing = 1.2F, + TabWidth = 4, + KerningMode = KerningMode.Standard, + Tracking = 0.02F, + HintingMode = HintingMode.Standard +}; +``` + +For a deeper explanation of how Fonts applies GSUB/GPOS shaping, bidi analysis, fallback runs, and TrueType hinting, see [Hinting and Shaping](hintingandshaping.md). + +### Fallback fonts and color fonts + +Use `FallbackFontFamilies` when a single font cannot cover every glyph you need. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily textFamily = collection.Add("fonts/NotoSans-Regular.ttf"); +FontFamily arabicFamily = collection.Add("fonts/NotoSansArabic-Regular.ttf"); +FontFamily emojiFamily = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); + +TextOptions options = new(textFamily.CreateFont(16)) +{ + FallbackFontFamilies = new[] { arabicFamily, emojiFamily }, + ColorFontSupport = ColorFontSupport.ColrV1 | ColorFontSupport.Svg +}; +``` + +`ColorFontSupport` controls which color-font technologies are honored during layout and rendering: `ColrV0`, `ColrV1`, and `Svg`. + +For a fuller discussion of multilingual text, fallback ordering, and script coverage, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). + +### OpenType feature tags + +`FeatureTags` lets you request additional OpenType features during shaping. The property type is `IReadOnlyList`, which means you can use either `KnownFeatureTags` enum values or parse raw four-character tags with `Tag.Parse(...)`. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] + { + KnownFeatureTags.Ligatures, + KnownFeatureTags.TabularFigures, + Tag.Parse("ss01") + } +}; +``` + +Use `KnownFeatureTags` values when the feature already has a named constant. Use `Tag.Parse(...)` when you need a raw tag that is not otherwise surfaced in your code. + +See [OpenType Features](opentypefeatures.md) for a fuller guide to common feature tags and when to request them explicitly. + +### Text runs + +`TextRuns` lets you override layout attributes for subranges of text. A `TextRun` can replace the font and apply `TextAttributes` or `TextDecorations`. + +`TextRun.Start` is inclusive and `TextRun.End` is exclusive. Both are grapheme indices, not UTF-16 code-unit indices. + +```csharp +using SixLabors.Fonts; + +const string text = "Title: 1234"; + +Font baseFont = SystemFonts.CreateFont("Segoe UI", 18); +Font emphasisFont = SystemFonts.CreateFont("Segoe UI", 18, FontStyle.Bold); + +TextOptions options = new(baseFont) +{ + TextRuns = new[] + { + new TextRun + { + Start = 7, + End = 11, + Font = emphasisFont, + TextDecorations = TextDecorations.Underline + } + } +}; +``` + +For plain ASCII text, grapheme indices often line up with character positions. For emoji, combining marks, and complex scripts, calculate ranges in graphemes rather than assuming one UTF-16 code unit equals one visible character. + +See [Unicode, Code Points, and Graphemes](unicode.md) for a fuller explanation of `char`, `CodePoint`, and grapheme units. diff --git a/articles/fonts/troubleshooting.md b/articles/fonts/troubleshooting.md new file mode 100644 index 000000000..a2c47924e --- /dev/null +++ b/articles/fonts/troubleshooting.md @@ -0,0 +1,105 @@ +# Troubleshooting + +This page covers the most common issues people hit when working with Fonts. + +### A font family cannot be found + +If `Get(...)` or `SystemFonts.CreateFont(...)` fails, you may see `FontFamilyNotFoundException`. + +Typical causes: + +- the family name does not match the font's actual family name +- the font was never added to your `FontCollection` +- you are relying on a system font that is not installed on the current machine +- you loaded the font with a culture-specific family name and are resolving it with a different culture + +Safer patterns are: + +- use `TryGet(...)` instead of `Get(...)` when probing +- inspect `FontDescription` after loading a file +- prefer application-owned font files over machine-specific `SystemFonts` when portability matters + +### A font file loads poorly or throws + +Invalid or unsupported font data can surface as: + +- `InvalidFontFileException` +- `InvalidFontTableException` +- `MissingFontTableException` + +If you hit one of these: + +- verify the file is a real font and not an incomplete download +- prefer loading from a stable local file or stream +- if the font is a collection, use `AddCollection(...)` or `LoadFontCollectionDescriptions(...)` + +### Text renders with missing glyphs + +If some characters do not render as expected: + +- make sure the selected font actually contains the script you need +- add script-specific families to `FallbackFontFamilies` +- enable color-font support if the missing content is emoji +- use `TryGetGlyphs(...)` when you need to probe a specific `CodePoint` value directly + +Fallback can only help if one of the supplied families actually contains the required glyphs. + +### Fallback fonts are not being used + +The most common reason is that the primary font already contains a glyph for that Unicode scalar value, so fallback never activates. + +If you want a specific range to use a different font even when the primary font could render it, use `TextRuns` instead of relying on fallback. + +Fallback order also matters. Fonts searches `FallbackFontFamilies` in order and uses the first suitable family it finds. + +### RTL or complex-script text looks wrong + +Check these first: + +- use a font that actually supports the script +- set `TextDirection = TextDirection.Auto` or explicitly choose the correct direction +- avoid assuming simple one-character-per-glyph behavior +- verify your fallback families cover the script, not just isolated characters + +Arabic, Indic, Thai, Hebrew, and similar scripts depend on shaping, not just raw Unicode coverage. + +### Measurements look larger or smaller than expected + +This is usually a measurement-choice issue: + +- `MeasureAdvance(...)` is the logical layout box +- `MeasureBounds(...)` is pure glyph ink bounds +- `MeasureRenderableBounds(...)` is the union of the two + +It is normal for these values to differ. Italics, accents, and decorative forms often extend outside the advance box, while line height can add space that no glyph pixels occupy. + +### Text run indices look wrong + +`TextRun.Start` and `TextRun.End` are grapheme indices, not UTF-16 code-unit indices. + +That matters for: + +- emoji +- combining marks +- ligatures +- many non-Latin scripts + +If a text run seems offset or slices the wrong part of the string, re-check the range in grapheme terms. + +See [Unicode, Code Points, and Graphemes](unicode.md) for the distinction between raw `char` positions, `CodePoint` values, and grapheme indices. + +### Variable font changes do nothing + +Usually one of these is true: + +- the font is not actually variable +- the axis tag is wrong +- the value is outside the font's supported range + +Use `font.FontMetrics.TryGetVariationAxes(...)` to inspect the actual axes and ranges exposed by the font. `FontVariation` tags must be exactly four characters. + +### System font behavior differs by machine + +`SystemFonts` is convenient, but it is not deterministic across environments. Different machines can have different installed families, versions, and script coverage. + +If you need repeatable output across CI, servers, containers, and user machines, ship your own fonts and load them through `FontCollection`. diff --git a/articles/fonts/unicode.md b/articles/fonts/unicode.md new file mode 100644 index 000000000..99f9b9b76 --- /dev/null +++ b/articles/fonts/unicode.md @@ -0,0 +1,146 @@ +# Unicode, Code Points, and Graphemes + +Fonts works with several different levels of text units. It is important to keep them separate, because they are not interchangeable. + +### The text-unit levels + +- `char`: a single UTF-16 code unit in a .NET `string` +- `CodePoint`: a Unicode scalar value, represented by +- grapheme: a user-perceived text element, represented by a `ReadOnlySpan` returned from `SpanGraphemeEnumerator` + +In everyday text, those levels often line up for simple ASCII. Once you move beyond that, they diverge quickly. + +### What is a `char`? + +In .NET, `string` is UTF-16. That means a single `char` is just one UTF-16 code unit. + +A `char` is not always a full Unicode character: + +- BMP scalars such as `A` fit in one `char` +- supplementary-plane scalars such as many emoji use two `char` values as a surrogate pair +- combining sequences can use multiple `char` values to represent what a user sees as one text element + +So if you index raw `char` positions, you are working at the storage level, not the text-semantics level. + +### What is a `CodePoint`? + +In strict Unicode terminology: + +- a code point is any value in the range `U+0000` through `U+10FFFF` +- a Unicode scalar value is any code point except the surrogate range `U+D800` through `U+DFFF` + + represents Unicode scalar values. Despite the type name, it intentionally excludes standalone surrogate code points because those are UTF-16 encoding artifacts, not meaningful text values to shape or render. + +That makes it the right unit when you want to talk about valid Unicode text values directly. + +Useful `CodePoint` members include: + +- `Value` +- `Utf16SequenceLength` +- `Utf8SequenceLength` +- `IsAscii` +- `IsBmp` +- `Plane` +- `ReplacementChar` + +This is also the unit used by glyph-probing APIs such as `Font.TryGetGlyphs(...)`. + +### What is a grapheme? + +A grapheme cluster is the closest thing to a user-perceived text element. + +Examples: + +- `A` is one grapheme +- `A` followed by a combining acute accent is still one grapheme +- many emoji sequences joined with zero-width joiners are one grapheme +- a flag emoji made from two regional indicators is one grapheme + +Fonts exposes grapheme enumeration through `SpanGraphemeEnumerator`, which implements the Unicode grapheme cluster algorithm from UAX #29. + +This is why `TextRun.Start` and `TextRun.End` are grapheme indices rather than raw `char` indices. + +### Enumerate `CodePoint` values + +The Unicode enumeration helpers live in `SixLabors.Fonts.Unicode`. + +```csharp +using System; +using SixLabors.Fonts.Unicode; + +string text = "A\u0301 \U0001F600"; + +foreach (CodePoint codePoint in text.AsSpan().EnumerateCodePoints()) +{ + Console.WriteLine( + $"U+{codePoint.Value:X}: UTF-16 length {codePoint.Utf16SequenceLength}"); +} +``` + +`EnumerateCodePoints()` returns a . It yields `CodePoint` values, which means the enumeration surface is Unicode scalar values. Invalid UTF-16 sequences are surfaced as `CodePoint.ReplacementChar`. + +Count helpers are also available: + +```csharp +using SixLabors.Fonts.Unicode; + +int count = "A\u0301 \U0001F600".GetCodePointCount(); +``` + +### Enumerate graphemes + +Use grapheme enumeration when you need units that better match what a reader sees. + +```csharp +using System; +using SixLabors.Fonts.Unicode; + +string text = "A\u0301 \U0001F600"; +int index = 0; + +foreach (ReadOnlySpan grapheme in text.AsSpan().EnumerateGraphemes()) +{ + Console.WriteLine($"{index++}: {grapheme.ToString()}"); +} +``` + +`EnumerateGraphemes()` returns a . + +Count helpers are available here too: + +```csharp +using SixLabors.Fonts.Unicode; + +int count = "A\u0301 \U0001F600".GetGraphemeCount(); +``` + +### Which unit should you use? + +Use `char` when: + +- you are working with raw .NET string storage +- you truly need UTF-16 code-unit offsets + +Use `CodePoint` when: + +- you are inspecting Unicode scalar values +- you are probing glyph availability with `TryGetGlyphs(...)` +- you care about Unicode values, planes, or encoded sequence lengths + +Use graphemes when: + +- you are slicing visible text ranges +- you are working with `TextRun.Start` and `TextRun.End` +- you want indices that align better with user-visible text elements + +### Relation to layout + +Fonts uses additional Unicode logic internally during layout, including line-breaking and script/shaping data. But the public text-unit APIs you will use most often are: + +- `EnumerateCodePoints()` +- `EnumerateGraphemes()` +- `GetCodePointCount()` +- `GetGraphemeCount()` +- `CodePoint` + +If you are debugging a `TextRun` range, a missing glyph, or a mismatch between visible text and string indices, start by checking whether you are reasoning in `char`, `CodePoint`, or grapheme units. diff --git a/articles/fonts/useopentypefeatures.md b/articles/fonts/useopentypefeatures.md new file mode 100644 index 000000000..56179145c --- /dev/null +++ b/articles/fonts/useopentypefeatures.md @@ -0,0 +1,55 @@ +# Use OpenType Features for Numbers and Fractions + +Use `TextOptions.FeatureTags` when you want to request discretionary OpenType features from the font. + +### Align numeric columns with tabular figures + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] { KnownFeatureTags.TabularFigures } +}; +``` + +This is useful for scoreboards, tables, counters, and any UI where digits should line up cleanly. + +### Request diagonal fractions + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] { KnownFeatureTags.Fractions } +}; +``` + +This only has an effect if the font actually provides the requested feature. + +### Combine multiple features + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + FeatureTags = new Tag[] + { + KnownFeatureTags.TabularFigures, + KnownFeatureTags.OldstyleFigures, + Tag.Parse("ss01") + } +}; +``` + +Use the same `TextOptions` for both `TextMeasurer` and `TextRenderer` so the measured result matches the rendered result. + +For the fuller feature model, see [OpenType Features](opentypefeatures.md). diff --git a/articles/fonts/variablefonts.md b/articles/fonts/variablefonts.md new file mode 100644 index 000000000..400819d3e --- /dev/null +++ b/articles/fonts/variablefonts.md @@ -0,0 +1,121 @@ +# Variable Fonts + +Variable fonts expose one or more design axes, such as weight, width, slant, or optical size, from a single font file. Fonts models those axes with `FontVariation`. + +### Create a variable-font instance + +Use `FontFamily.CreateFont(...)` with one or more `FontVariation` values. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); + +Font font = family.CreateFont( + 16, + new FontVariation(KnownVariationAxes.Weight, 700), + new FontVariation(KnownVariationAxes.Width, 85), + new FontVariation(KnownVariationAxes.OpticalSize, 16)); +``` + +The tag must be exactly four characters. Common registered axis tags are available in `KnownVariationAxes`, but custom axes can also be addressed directly. + +### Use a prototype font + +If you already have a base `Font`, you can derive a new instance from it. + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); + +Font baseFont = family.CreateFont(16); +Font bolderFont = new( + baseFont, + new FontVariation(KnownVariationAxes.Weight, 700)); +``` + +This is useful when you want to keep the same family, size, and requested style while changing only the variation coordinates. + +### Inspect supported axes + +You can query the variable axes exposed by the current font through `FontMetrics.TryGetVariationAxes(...)`. + +```csharp +using System; +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); +Font font = family.CreateFont(16); + +if (font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)) +{ + foreach (VariationAxis axis in axes) + { + Console.WriteLine($"{axis.Tag}: {axis.Min}..{axis.Max} (default {axis.Default})"); + } +} +``` + +Each `VariationAxis` exposes: + +- `Name` +- `Tag` +- `Min` +- `Max` +- `Default` + +That makes it possible to build UI controls or configuration validation based on the actual font rather than on hard-coded assumptions. + +### Registered and custom axes + +`KnownVariationAxes` includes the registered tags most users expect: + +- `Weight` (`wght`) +- `Width` (`wdth`) +- `OpticalSize` (`opsz`) +- `Italic` (`ital`) +- `Slant` (`slnt`) + +Fonts also supports arbitrary four-character axis tags: + +```csharp +using SixLabors.Fonts; + +FontCollection collection = new(); +FontFamily family = collection.Add("fonts/SomeVariableFont.ttf"); + +Font font = family.CreateFont( + 16, + new FontVariation("GRAD", 50), + new FontVariation("XTRA", 420)); +``` + +### How values behave + +`FontVariation` follows CSS `font-variation-settings` semantics. Variation values are clamped to the axis range defined by the font. + +That means: + +- valid tags must be four characters long +- out-of-range values are constrained by the font +- different fonts can expose different axis sets and ranges + +### Non-variable fonts + +Applying `FontVariation` values to a non-variable font is harmless but has no effect. If you need to know whether a font is actually variable, check `TryGetVariationAxes(...)` before building variation-driven UI or configuration. + +### When to use variable fonts + +Variable fonts are especially useful when you want to: + +- tune weight or width continuously instead of switching discrete files +- match optical size to the rendered point size +- reduce the number of separate font files you need to ship +- keep a single family while exploring many design-space instances + +If you run into unexpected results, see [Troubleshooting](troubleshooting.md). diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index 2ff4ec43e..35fe4cfa1 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -2,6 +2,13 @@ ImageSharp is a fully managed, cross-platform 2D graphics library for .NET. It provides a format-agnostic in-memory image model, a rich processing pipeline, flexible encoders and decoders, and low-level pixel APIs for advanced workloads. +## License + +ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. + +>[!IMPORTANT] +>Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. + ## Install ImageSharp ImageSharp is distributed on [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp) with preview and nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). @@ -68,10 +75,3 @@ When enabled, ImageSharp adds implicit `global using` directives for: - `SixLabors.ImageSharp.Processing` You can turn this off by removing the property or setting it to `false`. - -## License - -ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. - ->[!IMPORTANT] ->Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. diff --git a/articles/toc.md b/articles/toc.md index 75e57f55d..445c4adfc 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -40,7 +40,25 @@ ### [Image Caches](imagesharp.web/imagecaches.md) # [Fonts](fonts/index.md) -## [Getting Started](fonts/gettingstarted.md) +## [Loading Fonts and Collections](fonts/gettingstarted.md) +## [System Fonts](fonts/systemfonts.md) +## [Font Metadata and Inspection](fonts/fontmetadata.md) +## [Font Metrics](fonts/fontmetrics.md) +## [Measuring Text](fonts/measuringtext.md) +## [Text Layout and Options](fonts/textlayout.md) +## [OpenType Features](fonts/opentypefeatures.md) +## [Hinting and Shaping](fonts/hintingandshaping.md) +## [Color Fonts](fonts/colorfonts.md) +## [Unicode, Code Points, and Graphemes](fonts/unicode.md) +## [Fallback Fonts and Multilingual Text](fonts/fallbackfonts.md) +## [Variable Fonts](fonts/variablefonts.md) ## [Custom Rendering](fonts/customrendering.md) +## [Recipes](fonts/recipes.md) +### [Fit Text to a Target Width](fonts/fittexttowidth.md) +### [Inspect Font Files and Collections](fonts/inspectfontfiles.md) +### [List System Fonts and Resolve by Culture](fonts/listsystemfonts.md) +### [Use OpenType Features for Numbers and Fractions](fonts/useopentypefeatures.md) +### [Check Glyph Coverage Before Choosing Fallbacks](fonts/checkglyphcoverage.md) +## [Troubleshooting](fonts/troubleshooting.md) # [PolygonClipper](polygonclipper/index.md) diff --git a/ext/Fonts b/ext/Fonts index 6011281ed..739606071 160000 --- a/ext/Fonts +++ b/ext/Fonts @@ -1 +1 @@ -Subproject commit 6011281ed8b77c80a3598a6c934438384368d0d2 +Subproject commit 739606071151a3c8d4dbcd3c1d1b7e5ccb61e024 diff --git a/index.md b/index.md index a11b235fd..a7106f8f1 100644 --- a/index.md +++ b/index.md @@ -11,14 +11,14 @@ You can find tutorials, examples and API details covering all Six Labors project Detailed documentation for the entire API available across our projects. -### [Conceptual Documentation](articles/imagesharp/index.md) +### Project Documentation -Our graphics libraries are split into different projects. They cover different concerns separately, but there is strong cohesion in order to provide the best developer experience. +Our libraries are split into focused projects that work well together. They cover image processing, drawing, web middleware, fonts, and polygon clipping while keeping a consistent developer experience across the stack. You can find documentation for each project in the links below.
-
+ -
+
ImageSharp.Drawing
@@ -38,7 +38,7 @@ You can find documentation for each project in the links below.
-
+
ImageSharp.Web
@@ -48,14 +48,24 @@ You can find documentation for each project in the links below.
-
+
-
Fonts
-

Font Loading and Drawing API.

- - Learn More - +
Fonts
+

Font Loading and Drawing API.

+ + Learn More + +
+
+
+
+ PolygonClipper Logo +
PolygonClipper
+

High-performance polygon clipping and stroking.

+ + Learn More +
From 2b29576c1ff77b1441b1abe9a12aa9e47bed704d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 5 Apr 2026 22:29:20 +1000 Subject: [PATCH 05/21] humanize docs --- articles/fonts/checkglyphcoverage.md | 2 +- articles/fonts/colorfonts.md | 2 +- articles/fonts/customrendering.md | 2 ++ articles/fonts/fallbackfonts.md | 4 +++- articles/fonts/fittexttowidth.md | 2 +- articles/fonts/fontmetadata.md | 4 +--- articles/fonts/fontmetrics.md | 2 +- articles/fonts/gettingstarted.md | 4 +++- articles/fonts/hintingandshaping.md | 2 +- articles/fonts/index.md | 7 +++++-- articles/fonts/inspectfontfiles.md | 2 +- articles/fonts/listsystemfonts.md | 2 +- articles/fonts/measuringtext.md | 2 +- articles/fonts/opentypefeatures.md | 2 +- articles/fonts/recipes.md | 2 +- articles/fonts/systemfonts.md | 2 +- articles/fonts/textlayout.md | 4 +++- articles/fonts/troubleshooting.md | 2 +- articles/fonts/unicode.md | 2 +- articles/fonts/useopentypefeatures.md | 2 +- articles/fonts/variablefonts.md | 2 +- articles/imagesharp/animatedgif.md | 5 +++-- articles/imagesharp/colorandeffects.md | 2 ++ articles/imagesharp/colorprofiles.md | 2 ++ articles/imagesharp/configuration.md | 4 ++-- articles/imagesharp/cropandcanvas.md | 4 +++- articles/imagesharp/formatconversion.md | 4 +++- articles/imagesharp/gettingstarted.md | 4 +++- articles/imagesharp/gif.md | 4 +++- articles/imagesharp/identify.md | 2 +- articles/imagesharp/imageformats.md | 4 +++- articles/imagesharp/index.md | 4 +++- articles/imagesharp/interop.md | 6 ++++-- articles/imagesharp/jpeg.md | 2 +- articles/imagesharp/loadingandsaving.md | 4 +++- articles/imagesharp/memorymanagement.md | 4 ++-- articles/imagesharp/metadata.md | 4 +++- articles/imagesharp/migratingfromsystemdrawing.md | 4 +++- articles/imagesharp/orientation.md | 4 +++- articles/imagesharp/pixelbuffers.md | 4 +++- articles/imagesharp/pixelformats.md | 2 ++ articles/imagesharp/png.md | 4 +++- articles/imagesharp/processing.md | 2 +- articles/imagesharp/quantization.md | 4 +++- articles/imagesharp/recipes.md | 2 +- articles/imagesharp/resize.md | 4 +++- articles/imagesharp/security.md | 4 +--- articles/imagesharp/stripmetadata.md | 2 +- articles/imagesharp/thumbnails.md | 4 +++- articles/imagesharp/tiff.md | 4 +++- articles/imagesharp/troubleshooting.md | 2 +- articles/imagesharp/webp.md | 4 +++- 52 files changed, 107 insertions(+), 57 deletions(-) diff --git a/articles/fonts/checkglyphcoverage.md b/articles/fonts/checkglyphcoverage.md index 9a2edba08..87c5877e2 100644 --- a/articles/fonts/checkglyphcoverage.md +++ b/articles/fonts/checkglyphcoverage.md @@ -1,6 +1,6 @@ # Check Glyph Coverage Before Choosing Fallbacks -This recipe is useful when you want to know whether a font can cover the text you plan to render before you choose fallback families. +Before you wire up fallback families, it helps to know what your primary font can already cover. This recipe shows a quick way to probe individual scalar values or scan a string so you can make fallback decisions based on actual glyph coverage instead of guesswork. ### Check individual code points diff --git a/articles/fonts/colorfonts.md b/articles/fonts/colorfonts.md index a66cd07ff..f47d600d5 100644 --- a/articles/fonts/colorfonts.md +++ b/articles/fonts/colorfonts.md @@ -1,6 +1,6 @@ # Color Fonts -Color fonts let glyphs carry paint information instead of only a monochrome outline. +Color fonts are one of the clearest signs of how much richer modern text rendering has become. Instead of a single monochrome outline, a glyph can carry layers, gradients, or even SVG content, and Fonts exposes that support explicitly. Fonts has comprehensive support for the major OpenType color-font technologies it exposes publicly: diff --git a/articles/fonts/customrendering.md b/articles/fonts/customrendering.md index 656eb50a9..e97f49325 100644 --- a/articles/fonts/customrendering.md +++ b/articles/fonts/customrendering.md @@ -3,6 +3,8 @@ >[!NOTE] >If you want to draw text onto images, [ImageSharp.Drawing](../imagesharp.drawing/index.md) already provides the rendering layer for you. This page is for cases where you want to render glyphs to your own surface or extract geometry for another system. +Most developers meet Fonts through [ImageSharp.Drawing](../imagesharp.drawing/index.md), where the rendering surface is already handled for you. This page is for the next step down: when you want Fonts to do the shaping and glyph decomposition, but you want to decide how those glyphs are painted or exported. + Custom rendering in Fonts is built around `IGlyphRenderer`. `TextRenderer.RenderTextTo(...)` performs layout and shaping, then sends the result to your renderer as glyphs, layers, figures, and path commands. ### When to use it diff --git a/articles/fonts/fallbackfonts.md b/articles/fonts/fallbackfonts.md index 59467f078..904af25b8 100644 --- a/articles/fonts/fallbackfonts.md +++ b/articles/fonts/fallbackfonts.md @@ -1,6 +1,8 @@ # Fallback Fonts and Multilingual Text -Modern text often mixes scripts, emoji, and symbols that do not all exist in a single font. Fonts handles that through `TextOptions.FallbackFontFamilies`. +Real text rarely stays inside one script or one font. User names, emoji, CJK text, math, and symbols all show up in the same application, so fallback is what turns a nice Latin-only demo into a text stack that survives real-world input. + +Fonts handles that through `TextOptions.FallbackFontFamilies`. When the primary `Font` does not contain a glyph for part of the text, the layout engine searches the fallback families in order and uses the first family that can supply the missing glyphs. diff --git a/articles/fonts/fittexttowidth.md b/articles/fonts/fittexttowidth.md index c34e824c8..a41914c58 100644 --- a/articles/fonts/fittexttowidth.md +++ b/articles/fonts/fittexttowidth.md @@ -1,6 +1,6 @@ # Fit Text to a Target Width -This recipe is useful when you need a heading, label, or short line of text to fit inside a fixed width. +Fitting text into a fixed width is one of those jobs that sounds simple until you decide how aggressively you want to shrink, wrap, or restyle it. This recipe covers the straightforward single-line measurement loop many apps start with. For single-line text, the usual pattern is: diff --git a/articles/fonts/fontmetadata.md b/articles/fonts/fontmetadata.md index bf75c2c0d..b6b177bc5 100644 --- a/articles/fonts/fontmetadata.md +++ b/articles/fonts/fontmetadata.md @@ -1,8 +1,6 @@ # Font Metadata and Inspection -You do not need to fully load a font into a `FontCollection` just to inspect its names and basic face metadata. - -Fonts exposes `FontDescription` for that purpose. +Sometimes you need to inspect a font long before you care about laying text out with it. Maybe you are building an importer, a picker, or a diagnostics tool. `FontDescription` is the lightweight part of the API for that job. ### Read metadata without loading the font for layout diff --git a/articles/fonts/fontmetrics.md b/articles/fonts/fontmetrics.md index cbe133491..5ba84c425 100644 --- a/articles/fonts/fontmetrics.md +++ b/articles/fonts/fontmetrics.md @@ -2,7 +2,7 @@ `FontDescription` tells you what a face is called. `FontMetrics` tells you how that face behaves. -Use `FontMetrics` when you need inspection data that affects layout, glyph selection, decoration placement, variation support, or code-point coverage. +Once you know what a font is, the next question is usually how it behaves. `FontMetrics` is where you inspect the measurements and coverage data that explain line spacing, decoration placement, variation support, and glyph availability. ### How to get `FontMetrics` diff --git a/articles/fonts/gettingstarted.md b/articles/fonts/gettingstarted.md index 2a3ab175b..2af6a92a4 100644 --- a/articles/fonts/gettingstarted.md +++ b/articles/fonts/gettingstarted.md @@ -1,6 +1,8 @@ # Loading Fonts and Collections -Fonts separates font discovery from text layout: +The quickest way to get comfortable with Fonts is to separate three ideas: where fonts come from, how a family becomes a concrete font instance, and how that font is later used for measurement or rendering. This page walks through that path before the more advanced layout guides. + +The main types you will meet first are: - `FontCollection` stores the families you load. - `FontFamily` represents a family and the styles available for it. diff --git a/articles/fonts/hintingandshaping.md b/articles/fonts/hintingandshaping.md index 1663fd022..f2584ef0e 100644 --- a/articles/fonts/hintingandshaping.md +++ b/articles/fonts/hintingandshaping.md @@ -1,6 +1,6 @@ # Hinting and Shaping -Hinting and shaping both affect the final appearance of text, but they are not the same stage and they do not solve the same problem. +Hinting and shaping are often mentioned in the same breath because both influence the final appearance of text. For newcomers, it helps to separate them early: shaping decides which glyphs and positions the layout engine should use, while hinting adjusts outlines for pixel-oriented rendering. Shaping answers "which glyphs should this text use, and where do those glyphs go?" diff --git a/articles/fonts/index.md b/articles/fonts/index.md index 9f8570af7..7876073a7 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -1,7 +1,9 @@ # Introduction ### What is Fonts? -Fonts is a cross-platform library for loading, measuring, and laying out fonts and text. +Fonts is the part of the Six Labors stack that handles font loading, text measurement, layout, shaping, and custom text rendering. + +If you are new to the library, the easiest way to think about it is in layers: load families, create concrete `Font` instances, then measure or render text with `TextOptions`. The rest of this section is organized around that path so you can start simple and move into shaping, Unicode, fallback, and custom rendering only when you need them. It supports TrueType and OpenType fonts, including CFF1 and CFF2 outlines, WOFF and WOFF2 web fonts, variable fonts, color fonts, advanced OpenType layout, complex script shaping, and bidirectional text rendering. @@ -49,7 +51,8 @@ paket add SixLabors.Fonts --version VERSION_NUMBER ### Start Here -- [Loading Fonts and Collections](gettingstarted.md) +If you are new to Fonts, start with [Loading Fonts and Collections](gettingstarted.md) and then use the pages below to branch into the topics your application needs. + - [System Fonts](systemfonts.md) - [Font Metadata and Inspection](fontmetadata.md) - [Font Metrics](fontmetrics.md) diff --git a/articles/fonts/inspectfontfiles.md b/articles/fonts/inspectfontfiles.md index a34a9be11..fad976bd9 100644 --- a/articles/fonts/inspectfontfiles.md +++ b/articles/fonts/inspectfontfiles.md @@ -1,6 +1,6 @@ # Inspect Font Files and Collections -Use this recipe when you want to inspect a font file before adding it to a `FontCollection`. +This recipe is a good starting point when you have a font file in hand and want to learn what it contains before you add it to your app's normal font collection. ### Read a single font file diff --git a/articles/fonts/listsystemfonts.md b/articles/fonts/listsystemfonts.md index d136377b3..829d55998 100644 --- a/articles/fonts/listsystemfonts.md +++ b/articles/fonts/listsystemfonts.md @@ -1,6 +1,6 @@ # List System Fonts and Resolve by Culture -Use this recipe when you want to inspect what the current machine exposes through `SystemFonts`. +This recipe is useful when you want a quick picture of what the current machine can actually provide through `SystemFonts`, whether for diagnostics, UI pickers, or culture-aware name resolution. ### List installed families diff --git a/articles/fonts/measuringtext.md b/articles/fonts/measuringtext.md index 991184843..ddd47643b 100644 --- a/articles/fonts/measuringtext.md +++ b/articles/fonts/measuringtext.md @@ -1,6 +1,6 @@ # Measuring Text -`TextMeasurer` lets you run the same layout engine that rendering uses, without drawing anything. This is the right tool when you need to size a container, choose a wrapping width, place labels, or inspect line metrics before you render. +Measurement is often the point where text layout stops being abstract and starts affecting a real UI. `TextMeasurer` lets you run the same shaping and layout engine that rendering uses, which means you can decide widths, line breaks, placements, and bounds before anything is drawn. ### Choose the right measurement diff --git a/articles/fonts/opentypefeatures.md b/articles/fonts/opentypefeatures.md index ab61e4273..66237c573 100644 --- a/articles/fonts/opentypefeatures.md +++ b/articles/fonts/opentypefeatures.md @@ -1,6 +1,6 @@ # OpenType Features -Fonts applies the shaping features that are required for correct layout automatically. `TextOptions.FeatureTags` is for the additional OpenType features you want to request on top of that baseline behavior. +Fonts already applies the OpenType features that are required for correct shaping and layout. `TextOptions.FeatureTags` is where you ask for the extra typographic touches a font may support, such as tabular figures, fractions, stylistic alternates, or discretionary ligatures. That makes it a typography control, not a substitute for the shaping engine. diff --git a/articles/fonts/recipes.md b/articles/fonts/recipes.md index aa4311f55..fc0e8eb23 100644 --- a/articles/fonts/recipes.md +++ b/articles/fonts/recipes.md @@ -1,6 +1,6 @@ # Recipes -These short recipes show common Fonts workflows without the full conceptual discussion from the main guides. +These pages are the quick-start side of the Fonts docs. They are meant for the moment when you know roughly what you want to do and would rather start from a short working example than read the full conceptual guide first. - [Fit Text to a Target Width](fittexttowidth.md) - [Inspect Font Files and Collections](inspectfontfiles.md) diff --git a/articles/fonts/systemfonts.md b/articles/fonts/systemfonts.md index 849bb4bf4..ccdf99383 100644 --- a/articles/fonts/systemfonts.md +++ b/articles/fonts/systemfonts.md @@ -1,6 +1,6 @@ # System Fonts -`SystemFonts` gives you access to the fonts installed on the current machine. +System fonts are convenient because they let you get moving without shipping font files yourself. They also come with the tradeoff that the available families depend on the machine you are running on, so this page treats portability as part of the topic rather than an afterthought. Use it when you want to work with platform fonts directly instead of loading files into your own `FontCollection`. diff --git a/articles/fonts/textlayout.md b/articles/fonts/textlayout.md index 7ea3738c1..6b6e16081 100644 --- a/articles/fonts/textlayout.md +++ b/articles/fonts/textlayout.md @@ -1,6 +1,8 @@ # Text Layout and Options -`TextOptions` is the central layout object in Fonts. The same options type is used by both `TextMeasurer` and `TextRenderer`, which makes it easy to keep measurement and rendering in sync. +Once you have a `Font`, `TextOptions` becomes the center of almost everything else. It is where you tell Fonts how text should flow, wrap, align, shape, and render, so getting comfortable with this type pays off quickly. + +The same options type is used by both `TextMeasurer` and `TextRenderer`, which makes it easy to keep measurement and rendering in sync. ### Core units diff --git a/articles/fonts/troubleshooting.md b/articles/fonts/troubleshooting.md index a2c47924e..dde979771 100644 --- a/articles/fonts/troubleshooting.md +++ b/articles/fonts/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -This page covers the most common issues people hit when working with Fonts. +When text does not measure or render the way you expect, the underlying cause is usually one of a few things: family resolution, font-file validity, fallback, shaping assumptions, or misunderstanding the different measurement APIs. This page starts with those common failure modes. ### A font family cannot be found diff --git a/articles/fonts/unicode.md b/articles/fonts/unicode.md index 99f9b9b76..f3bdf3102 100644 --- a/articles/fonts/unicode.md +++ b/articles/fonts/unicode.md @@ -1,6 +1,6 @@ # Unicode, Code Points, and Graphemes -Fonts works with several different levels of text units. It is important to keep them separate, because they are not interchangeable. +Text handling gets easier once you stop treating every `char` as a whole character. Fonts exposes the text-unit levels it actually uses during layout so you can reason about indexing, fallback, shaping, and glyph coverage with the same vocabulary as the library. ### The text-unit levels diff --git a/articles/fonts/useopentypefeatures.md b/articles/fonts/useopentypefeatures.md index 56179145c..9ca21056a 100644 --- a/articles/fonts/useopentypefeatures.md +++ b/articles/fonts/useopentypefeatures.md @@ -1,6 +1,6 @@ # Use OpenType Features for Numbers and Fractions -Use `TextOptions.FeatureTags` when you want to request discretionary OpenType features from the font. +This recipe shows the most common way people first encounter discretionary OpenType features: asking fonts to align figures more neatly or substitute fraction glyphs for number-heavy text. ### Align numeric columns with tabular figures diff --git a/articles/fonts/variablefonts.md b/articles/fonts/variablefonts.md index 400819d3e..17c15135a 100644 --- a/articles/fonts/variablefonts.md +++ b/articles/fonts/variablefonts.md @@ -1,6 +1,6 @@ # Variable Fonts -Variable fonts expose one or more design axes, such as weight, width, slant, or optical size, from a single font file. Fonts models those axes with `FontVariation`. +Variable fonts let one font file behave more like a design space than a single static face. Once that idea clicks, `FontVariation` becomes a practical way to ask for weight, width, slant, or optical-size variants without switching families. ### Create a variable-font instance diff --git a/articles/imagesharp/animatedgif.md b/articles/imagesharp/animatedgif.md index 8095a5569..09ecba327 100644 --- a/articles/imagesharp/animatedgif.md +++ b/articles/imagesharp/animatedgif.md @@ -1,7 +1,8 @@ # Create an Animated GIF -ImageSharp builds animated GIFs by creating a multi-frame [`Image`](xref:SixLabors.ImageSharp.Image`1), configuring GIF metadata, and then saving with [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) when you need encoder-specific control. -When you start from [`Color`](xref:SixLabors.ImageSharp.Color) values, convert them to the target pixel type with `ToPixel()` before passing them to generic image constructors. +Creating an animated GIF in ImageSharp is really about building a multi-frame image on purpose. Once that mental model is in place, the rest of the API starts to feel straightforward: create frames, set per-frame metadata, configure the animation metadata, then save with the encoder you want. + +ImageSharp builds animated GIFs by creating a multi-frame [`Image`](xref:SixLabors.ImageSharp.Image`1), configuring GIF metadata, and then saving with [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) when you need encoder-specific control. When you start from [`Color`](xref:SixLabors.ImageSharp.Color) values, convert them to the target pixel type with `ToPixel()` before passing them to generic image constructors. For format background and palette tradeoffs, see [GIF and Animation](gif.md). This page focuses on the actual authoring workflow. diff --git a/articles/imagesharp/colorandeffects.md b/articles/imagesharp/colorandeffects.md index 3d019ece2..9885a6eb1 100644 --- a/articles/imagesharp/colorandeffects.md +++ b/articles/imagesharp/colorandeffects.md @@ -1,5 +1,7 @@ # Color and Effects +Color adjustments are where many small ImageSharp processors start to feel composable instead of isolated. You can reach for named helpers like `Grayscale()` and `Sepia()`, or drop down to [`ColorMatrix`](xref:SixLabors.ImageSharp.ColorMatrix) when you want to express the transformation yourself. + ImageSharp includes a wide range of processors for tonal adjustment, color transforms, and simple stylistic effects. Common entry points include `Grayscale()`, `Sepia()`, `Brightness()`, `Contrast()`, `Hue()`, `Saturate()`, and `Opacity()`. Under the hood, many of these effects are expressed as a [`ColorMatrix`](xref:SixLabors.ImageSharp.ColorMatrix) and applied with [`Filter()`](xref:SixLabors.ImageSharp.Processing.FilterExtensions.Filter*). ## Convert to Grayscale diff --git a/articles/imagesharp/colorprofiles.md b/articles/imagesharp/colorprofiles.md index ec46121f7..d755bc7d0 100644 --- a/articles/imagesharp/colorprofiles.md +++ b/articles/imagesharp/colorprofiles.md @@ -1,5 +1,7 @@ # Color Profiles and Color Conversion +Color management can feel intimidating at first because there are really two related topics hiding under one name: the profiles attached to image files, and the value-level color conversions you may apply in your own code. This page separates those concerns so it is easier to decide when you need metadata preservation, when you need actual color conversion, and when you need both. + ImageSharp exposes color management in two different layers: - Embedded color metadata on decoded images, such as [`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) and [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile). diff --git a/articles/imagesharp/configuration.md b/articles/imagesharp/configuration.md index d02ed70d9..80c226acd 100644 --- a/articles/imagesharp/configuration.md +++ b/articles/imagesharp/configuration.md @@ -1,8 +1,8 @@ # Configuration -[`Configuration`](xref:SixLabors.ImageSharp.Configuration) controls how ImageSharp discovers formats, allocates memory, reads streams, and runs processor pipelines. +Most applications can use ImageSharp exactly as it comes out of the box. [`Configuration`](xref:SixLabors.ImageSharp.Configuration) only becomes interesting when you need to change what formats are available, how memory is allocated, how streams are read, or how aggressively work is parallelized. -For most applications, [`Configuration.Default`](xref:SixLabors.ImageSharp.Configuration.Default) is the right place for process-wide defaults. When you need different behavior for one workload, clone it and use the cloned instance locally. +That is why this page treats configuration as an opt-in advanced topic. Start with [`Configuration.Default`](xref:SixLabors.ImageSharp.Configuration.Default), and customize only the parts your workload truly needs. ## Use a Local Configuration for Targeted Overrides diff --git a/articles/imagesharp/cropandcanvas.md b/articles/imagesharp/cropandcanvas.md index 24004a82c..b8a2d4c56 100644 --- a/articles/imagesharp/cropandcanvas.md +++ b/articles/imagesharp/cropandcanvas.md @@ -1,6 +1,8 @@ # Crop, Pad, and Canvas -ImageSharp includes several processors for changing the visible region of an image or the size of its canvas. The most commonly used are `Crop()`, `Pad()`, `BackgroundColor()`, and `EntropyCrop()`. +Cropping and canvas operations are closely related, but they solve different problems. Cropping decides which part of the current pixels you keep. Canvas operations decide how much room the image has and where those pixels sit inside it. + +Thinking about those as separate questions makes the API much easier to navigate. ## Crop to an Explicit Rectangle diff --git a/articles/imagesharp/formatconversion.md b/articles/imagesharp/formatconversion.md index 28bfbaa35..d2b02b09c 100644 --- a/articles/imagesharp/formatconversion.md +++ b/articles/imagesharp/formatconversion.md @@ -1,6 +1,8 @@ # Convert Between Formats -Format conversion in ImageSharp is a decode-and-re-encode operation, but it is not a blind one. Once an image is loaded, the processing pipeline works with format-agnostic pixel data, while the metadata layer still carries enough information for the destination format to choose the best representation it supports. +Format conversion is one of the most common reasons people adopt ImageSharp in the first place. The nice part is that you usually do not have to think in terms of format-to-format adapters; you load into ImageSharp's common image model, make any changes you need, and then save with the target encoder. + +That decode-and-re-encode flow is not a blind one. Once an image is loaded, the processing pipeline works with format-agnostic pixel data, while the metadata layer still carries enough information for the destination format to choose the best representation it supports. ## How ImageSharp Bridges Formats diff --git a/articles/imagesharp/gettingstarted.md b/articles/imagesharp/gettingstarted.md index 0d959b836..9cbb4a5cb 100644 --- a/articles/imagesharp/gettingstarted.md +++ b/articles/imagesharp/gettingstarted.md @@ -1,6 +1,8 @@ # Getting Started -ImageSharp centers around a small set of core types: +ImageSharp is easiest to learn if you think in terms of a simple flow: load or identify an image, optionally process it, then save it in the format you need. This page introduces the handful of types that show up most often in that flow so the rest of the docs feel familiar more quickly. + +The main types you will run into first are: - [`Image`](xref:SixLabors.ImageSharp.Image) is the format-agnostic image container used by the main loading, processing, and saving APIs. - `Image` is the generic image container to use when you know the pixel format and want direct pixel access. See [Pixel Formats](pixelformats.md) for more detail. diff --git a/articles/imagesharp/gif.md b/articles/imagesharp/gif.md index 0d54c7bbd..6330a39da 100644 --- a/articles/imagesharp/gif.md +++ b/articles/imagesharp/gif.md @@ -1,6 +1,8 @@ # GIF and Animation -GIF is a palette-based format commonly used for simple animations. In ImageSharp, GIF encoding is built on a quantizing animated encoder, which means palette generation and frame metadata are both important parts of the workflow. +GIF is one of the oldest formats ImageSharp supports, and it comes with tradeoffs that matter more than many newcomers expect. It is still useful for simple animation and very broad compatibility, but because it is palette based, color reduction and frame metadata are part of the story from the start. + +In ImageSharp, GIF encoding is built on a quantizing animated encoder, which means palette generation and frame metadata are both important parts of the workflow. ## Format Characteristics diff --git a/articles/imagesharp/identify.md b/articles/imagesharp/identify.md index fcd22d2f9..9b6c49370 100644 --- a/articles/imagesharp/identify.md +++ b/articles/imagesharp/identify.md @@ -1,6 +1,6 @@ # Read Image Info Without Decoding -Use `Image.Identify()` and `Image.DetectFormat()` when you need to inspect an image without fully decoding the pixel data. This is useful for upload validation, metadata extraction, and planning later processing work. +When you are working with uploads, queues, or validation rules, fully decoding every image is often unnecessary work. `Image.Identify()` and `Image.DetectFormat()` let you answer the early questions first: what is this file, how large is it, how many frames does it have, and what kind of pixel data does it claim to contain? ## Read Dimensions, Frame Count, and Pixel Info diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index 842541a8a..d05a36bd6 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -1,6 +1,8 @@ # Image Formats -ImageSharp supports a broad set of built-in image formats and is designed so additional formats can be registered through configuration when needed. +ImageSharp keeps the in-memory image model separate from the file format on disk. That means the same processing code can work across JPEG, PNG, WebP, TIFF, GIF, and the other built-in codecs, while the encoder and metadata layers handle the format-specific details at the edges. + +This page is the format map for the library: which built-in formats ship by default, what each one is good at, and where to go next for format-specific guidance. ## Built-In Formats diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index 35fe4cfa1..1a5a45e35 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -1,6 +1,8 @@ # ImageSharp -ImageSharp is a fully managed, cross-platform 2D graphics library for .NET. It provides a format-agnostic in-memory image model, a rich processing pipeline, flexible encoders and decoders, and low-level pixel APIs for advanced workloads. +ImageSharp is the part of the Six Labors stack you reach for when you need to load, inspect, process, and save images entirely in managed .NET code. It gives you one consistent image model whether you are building a thumbnail service, a photo workflow, a web upload pipeline, or a lower-level imaging tool. + +This section is written as a guided set of articles rather than a flat feature list. Start with [Getting Started](gettingstarted.md) if you are new to the library, then branch into loading, processing, formats, or lower-level pixel work as your needs get more specific. ## License diff --git a/articles/imagesharp/interop.md b/articles/imagesharp/interop.md index 25d73bd69..6f5643088 100644 --- a/articles/imagesharp/interop.md +++ b/articles/imagesharp/interop.md @@ -1,6 +1,8 @@ # Interop and Raw Memory -ImageSharp supports both copy-based and zero-copy workflows for raw pixel data. Which API you choose depends on who owns the memory and whether you need ImageSharp to keep its own copy. +Most applications can stay inside ImageSharp's managed image model. When you need to exchange raw buffers with another library, device API, or native component, the important question becomes who owns the memory and whether you want a copy or a view. + +That is the lens this page uses for the interop APIs. ## Choose the Right API @@ -131,7 +133,7 @@ bool contiguous = image.DangerousTryGetSinglePixelMemory(out _); // false Important consequences of a strided wrapper: -- [`DangerousTryGetSinglePixelMemory(...)`](xref:SixLabors.ImageSharp.Image`1.DangerousTryGetSinglePixelMemory*) will usually return `false`; +- [`DangerousTryGetSinglePixelMemory(...)`](xref:SixLabors.ImageSharp.Image`1.DangerousTryGetSinglePixelMemory*) returns `false`, because it only succeeds when the image's logical pixels can be exposed as one tightly packed `width * height` block; - [`CopyPixelDataTo(...)`](xref:SixLabors.ImageSharp.Image`1.CopyPixelDataTo*) uses the backing row layout, so destination length must account for stride, not only `width * height`; - row padding belongs to the wrapped view contract, so make sure the caller and callee agree on it. diff --git a/articles/imagesharp/jpeg.md b/articles/imagesharp/jpeg.md index de4b9d49d..e20be841c 100644 --- a/articles/imagesharp/jpeg.md +++ b/articles/imagesharp/jpeg.md @@ -1,6 +1,6 @@ # JPEG -JPEG is typically the right choice for photographic images and other continuous-tone content where smaller file sizes matter more than lossless preservation. +JPEG remains the workhorse format for photographs on the web and in many application pipelines. It is best when you care about keeping file sizes small and are willing to trade away some exact pixel fidelity to get there. ## Format Characteristics diff --git a/articles/imagesharp/loadingandsaving.md b/articles/imagesharp/loadingandsaving.md index 50b671aec..8e5237e08 100644 --- a/articles/imagesharp/loadingandsaving.md +++ b/articles/imagesharp/loadingandsaving.md @@ -1,6 +1,8 @@ # Loading, Identifying, and Saving -ImageSharp provides a consistent set of APIs for working with image files, streams, and in-memory buffers. The main entry points are `Image.Load()`, `Image.Identify()`, `Image.DetectFormat()`, and the corresponding async APIs. +Most ImageSharp applications start here. Whether images come from disk, streams, or upload buffers, the same load, identify, and save model applies, which makes it easy to move from a quick sample to a production pipeline without relearning the API surface. + +The core idea is straightforward: use `Image.Load()` when you need pixels, `Image.Identify()` when you only need dimensions or metadata, and `Image.DetectFormat()` when you only need to know what kind of file you were given. ## Load Images diff --git a/articles/imagesharp/memorymanagement.md b/articles/imagesharp/memorymanagement.md index d1189421e..b23d653dc 100644 --- a/articles/imagesharp/memorymanagement.md +++ b/articles/imagesharp/memorymanagement.md @@ -1,8 +1,8 @@ # Memory Management -ImageSharp stores image pixels in pooled buffers managed by [`MemoryAllocator`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator). In normal use, you should assume large images are not backed by one giant contiguous span. +ImageSharp is designed so large images are practical to process without forcing every workload into one giant contiguous allocation. That is a big part of why the library scales well, but it also means memory behavior is worth understanding once you move beyond simple load-process-save samples. -That design keeps large-image handling practical, but it also means interop-heavy code should be explicit about when it truly needs contiguous memory. +This page explains the parts most developers eventually need: the default pooled allocator, when to customize it, and how those choices affect lower-level interop code. ## Default Behavior diff --git a/articles/imagesharp/metadata.md b/articles/imagesharp/metadata.md index 51d42c1cb..7b39d1469 100644 --- a/articles/imagesharp/metadata.md +++ b/articles/imagesharp/metadata.md @@ -1,6 +1,8 @@ # Working with Metadata -ImageSharp exposes image metadata through [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata). You can access it from a fully decoded image through `image.Metadata`, or from [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) when using `Image.Identify()`. +Metadata is where ImageSharp stores the information around the pixels: resolution, format details, EXIF, ICC profiles, and other auxiliary data. For newcomers, the key idea is that you can inspect or preserve this information without treating it as part of the pixel-processing pipeline itself. + +ImageSharp exposes that data through [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata). You can access it from a fully decoded image through `image.Metadata`, or from [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) when using `Image.Identify()`. ## Read Metadata from a File diff --git a/articles/imagesharp/migratingfromsystemdrawing.md b/articles/imagesharp/migratingfromsystemdrawing.md index 0948cb5a3..5abde8e69 100644 --- a/articles/imagesharp/migratingfromsystemdrawing.md +++ b/articles/imagesharp/migratingfromsystemdrawing.md @@ -1,6 +1,8 @@ # Migrating from System.Drawing -ImageSharp is not a one-for-one clone of `System.Drawing`, but most common workflows map cleanly once you shift from GDI-style APIs to ImageSharp's generic image and processing model. +If you are coming from `System.Drawing`, the biggest adjustment is not learning a brand-new set of image concepts. It is mostly learning that ImageSharp makes a few things explicit that GDI+ used to hide: pixel type, processing pipelines, and encoder choices. + +Once that shift lands, most everyday workflows map over cleanly. ## Core Type Mapping diff --git a/articles/imagesharp/orientation.md b/articles/imagesharp/orientation.md index b0407d763..2017f70d1 100644 --- a/articles/imagesharp/orientation.md +++ b/articles/imagesharp/orientation.md @@ -1,6 +1,8 @@ # Rotate, Flip, and Auto-Orient -ImageSharp provides several processors for correcting orientation and applying geometric transforms. The most common are `AutoOrient()`, `Rotate()`, `Flip()`, and `RotateFlip()`. +Orientation issues usually show up the first time a phone photo looks rotated even though the file came straight from a camera roll. This page covers the small set of operations you will use most often to normalize orientation metadata or apply explicit geometric transforms. + +The most common entry points are `AutoOrient()`, `Rotate()`, `Flip()`, and `RotateFlip()`. ## Correct Orientation from EXIF Metadata diff --git a/articles/imagesharp/pixelbuffers.md b/articles/imagesharp/pixelbuffers.md index 0b73a749c..dfb68dbfc 100644 --- a/articles/imagesharp/pixelbuffers.md +++ b/articles/imagesharp/pixelbuffers.md @@ -1,6 +1,8 @@ # Working with Pixel Buffers -ImageSharp gives you several ways to work directly with pixel data. The right one depends on whether you care most about simplicity, throughput, pixel-format independence, or interop. +When you first start with ImageSharp, the indexer is often enough. As soon as performance, reuse across pixel formats, or interop enter the picture, it helps to know the other buffer-access patterns the library offers and why they exist. + +This page is the map for that lower-level work. ## Choose the Right Access Pattern diff --git a/articles/imagesharp/pixelformats.md b/articles/imagesharp/pixelformats.md index 991c608d2..bf09699c4 100644 --- a/articles/imagesharp/pixelformats.md +++ b/articles/imagesharp/pixelformats.md @@ -1,5 +1,7 @@ # Pixel Formats +Pixel formats are one of the places where ImageSharp differs most clearly from older imaging APIs. The pixel type is not an afterthought or a hidden enum value; it is part of the image's actual type, which makes low-level code more explicit and more predictable once the model clicks. + [`Image`](xref:SixLabors.ImageSharp.Image`1) is generic because the in-memory pixel type is part of the image contract. An [`Image`](xref:SixLabors.ImageSharp.Image`1) and an [`Image`](xref:SixLabors.ImageSharp.Image`1) can represent the same picture, but they differ in channel layout, precision, memory usage, and what direct pixel access means for your code. Image memory is usually treated as discontiguous, even though smaller images may fit in a single backing buffer. See [Memory Management](memorymanagement.md) for more detail on how ImageSharp stores large images efficiently. diff --git a/articles/imagesharp/png.md b/articles/imagesharp/png.md index c312ff29c..60cbb8931 100644 --- a/articles/imagesharp/png.md +++ b/articles/imagesharp/png.md @@ -1,6 +1,8 @@ # PNG -PNG is the standard lossless choice for UI assets, screenshots, icons, and any workflow where transparency or exact pixel preservation matters. ImageSharp also supports animated PNG metadata and encoding scenarios. +PNG is the format people usually reach for when they want the saved pixels to stay exactly as they were processed in memory. That makes it a natural fit for UI assets, screenshots, diagrams, icons, and any workflow where transparency and crisp edges matter more than aggressive compression. + +ImageSharp also supports animated PNG metadata and encoding scenarios. ## Format Characteristics diff --git a/articles/imagesharp/processing.md b/articles/imagesharp/processing.md index 29b3f2f1c..1113cffc8 100644 --- a/articles/imagesharp/processing.md +++ b/articles/imagesharp/processing.md @@ -1,6 +1,6 @@ # Processing Images -ImageSharp processing pipelines are imperative and ordered. The processors you add inside `Mutate()` or `Clone()` run in the same order you write them, which makes the pipeline easy to reason about and compose. +Once an image is in memory, most work in ImageSharp happens through small ordered processing pipelines. That is one of the library's strengths: the code you write usually reads in the same order the pixels are transformed, which makes even longer pipelines approachable for newcomers. The main entry points are [`Mutate`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*?displayProperty=name) and [`Clone`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Clone*?displayProperty=name): diff --git a/articles/imagesharp/quantization.md b/articles/imagesharp/quantization.md index 3870d3bd0..beba8e4b6 100644 --- a/articles/imagesharp/quantization.md +++ b/articles/imagesharp/quantization.md @@ -1,6 +1,8 @@ # Quantization, Palettes, and Dithering -Quantization reduces an image to a limited set of colors. In ImageSharp, that matters both as an explicit processing step and as part of formats that write indexed or palette-constrained output. +Quantization is the part of image processing where you stop thinking in terms of continuous color and start thinking in terms of a limited palette. Even if you never call `Quantize()` directly, it still matters because formats like GIF, indexed PNG, CUR, and ICO rely on the same ideas. + +In ImageSharp, quantization matters both as an explicit processing step and as part of formats that write indexed or palette-constrained output. ## Where Quantization Shows Up diff --git a/articles/imagesharp/recipes.md b/articles/imagesharp/recipes.md index 4ad6e6cf7..26b9ca8e1 100644 --- a/articles/imagesharp/recipes.md +++ b/articles/imagesharp/recipes.md @@ -1,6 +1,6 @@ # Recipes -This section collects common ImageSharp tasks into small, copy-pasteable examples. These recipes are intentionally practical and build on the broader guidance in the conceptual docs. +These pages are the fast path through the ImageSharp docs. They skip most of the background explanation and focus on the handful of workflows people reach for over and over again, while linking back to the deeper guides when you need more context. ## Common Tasks diff --git a/articles/imagesharp/resize.md b/articles/imagesharp/resize.md index 0b98d60a2..e6c49b36f 100644 --- a/articles/imagesharp/resize.md +++ b/articles/imagesharp/resize.md @@ -1,6 +1,8 @@ # Resizing Images -Resizing is one of the most common ImageSharp operations. The simple `Resize()` overloads are good for direct width and height changes, while [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) gives you control over fit mode, anchor position, background padding, sampler choice, alpha handling, and manual target rectangles. +Resizing looks simple on the surface, but it is also one of the easiest places to make an image look subtly wrong. Aspect ratio, resampler choice, fit mode, alpha handling, and decode-time downscaling all influence the result, so this page walks through the common paths in the order most people need them. + +The simple `Resize()` overloads are good for direct width and height changes, while [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) gives you control over fit mode, anchor position, background padding, sampler choice, alpha handling, and manual target rectangles. ## Basic Resize diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index 6f97aad07..6a8ca1a28 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -1,8 +1,6 @@ # Security Considerations -Image processing is resource-intensive by nature. Public or semi-public systems that accept untrusted images should treat decode and processing as potentially expensive work and put explicit limits around what is accepted. - -ImageSharp gives you several practical controls for doing that. +Image processing is powerful, but it is also one of the easier places for an application to burn CPU, memory, and time on untrusted input. This page is written as a practical hardening guide: what to check early, what to limit, and which ImageSharp hooks help you keep risky inputs under control. ## Preflight with Identify When Possible diff --git a/articles/imagesharp/stripmetadata.md b/articles/imagesharp/stripmetadata.md index 771c34627..14377240a 100644 --- a/articles/imagesharp/stripmetadata.md +++ b/articles/imagesharp/stripmetadata.md @@ -1,6 +1,6 @@ # Strip Metadata -Stripping metadata is useful when reducing file size, removing personal information, or normalizing exported assets. +Removing metadata is usually about one of three goals: smaller files, less personal information, or a cleaner normalized export. ImageSharp makes that straightforward, but it helps to be clear about whether you want the encoder to skip metadata on write or whether you want to clear profiles in memory first. ## Strip Metadata with the Encoder diff --git a/articles/imagesharp/thumbnails.md b/articles/imagesharp/thumbnails.md index 7a49d8f8f..1ad3d493e 100644 --- a/articles/imagesharp/thumbnails.md +++ b/articles/imagesharp/thumbnails.md @@ -1,6 +1,8 @@ # Generate Thumbnails -Thumbnail generation is one of the most common ImageSharp workflows. The two usual patterns are: +Thumbnail generation is one of those jobs that sounds trivial until you have to decide what "good enough" means. Do you keep the whole image visible? Do you crop to fill? Do you normalize orientation first? This page covers the two thumbnail patterns people use most often. + +The usual patterns are: - fit the image within a bounding box while preserving aspect ratio, and - create a fixed-size thumbnail that fills the target area by cropping. diff --git a/articles/imagesharp/tiff.md b/articles/imagesharp/tiff.md index 489c5da46..88f02dddd 100644 --- a/articles/imagesharp/tiff.md +++ b/articles/imagesharp/tiff.md @@ -1,6 +1,8 @@ # TIFF -TIFF is useful in workflows where compression mode, pixel layout, and metadata fidelity matter more than broad browser support. ImageSharp exposes a range of TIFF-specific encoder and metadata options for those cases. +TIFF is less about browser delivery and more about control. When a workflow cares about archival fidelity, scanning, publishing, print, or carrying richer metadata and pixel-layout choices forward, TIFF is often the format that gives you the room to do it. + +ImageSharp exposes a range of TIFF-specific encoder and metadata options for those cases. ## Format Characteristics diff --git a/articles/imagesharp/troubleshooting.md b/articles/imagesharp/troubleshooting.md index 5e0d376c0..893c0ac0d 100644 --- a/articles/imagesharp/troubleshooting.md +++ b/articles/imagesharp/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -Most ImageSharp problems fall into a few common categories: format detection, invalid content, stream positioning, memory pressure, interop assumptions, and disposal bugs. +Most ImageSharp issues turn out to be understandable once you know which layer is complaining: format detection, decoding, memory, streams, or disposal. This page groups the common failures that way so it is easier to move from symptom to likely cause. ## "Image format is unknown" diff --git a/articles/imagesharp/webp.md b/articles/imagesharp/webp.md index 531eb4f0d..03304d042 100644 --- a/articles/imagesharp/webp.md +++ b/articles/imagesharp/webp.md @@ -1,6 +1,8 @@ # WebP -WebP supports lossy and lossless compression, transparency, and animation. In ImageSharp, it is one of the most flexible general-purpose web output formats. +WebP is often the first format to consider when you want one codec that can cover several common web scenarios. It can handle photographs, transparency, and animation, which makes it a flexible alternative to juggling separate JPEG, PNG, and GIF outputs. + +In ImageSharp, it is one of the most flexible general-purpose web output formats. ## Format Characteristics From 6bcf7ae71d7e2eef38a4b8e88ba1bf1537fb2482 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 5 Apr 2026 23:07:13 +1000 Subject: [PATCH 06/21] Add PolygonClipper docs --- articles/polygonclipper/booleanoperations.md | 87 ++++++++++++++ articles/polygonclipper/gettingstarted.md | 70 ++++++++++++ articles/polygonclipper/index.md | 25 +++-- articles/polygonclipper/normalization.md | 53 +++++++++ .../polygonclipper/polygonsandcontours.md | 106 ++++++++++++++++++ articles/polygonclipper/stroking.md | 92 +++++++++++++++ articles/toc.md | 5 + index.md | 10 +- templates/modern/public/main.css | 84 ++++++++++++-- 9 files changed, 512 insertions(+), 20 deletions(-) create mode 100644 articles/polygonclipper/booleanoperations.md create mode 100644 articles/polygonclipper/gettingstarted.md create mode 100644 articles/polygonclipper/normalization.md create mode 100644 articles/polygonclipper/polygonsandcontours.md create mode 100644 articles/polygonclipper/stroking.md diff --git a/articles/polygonclipper/booleanoperations.md b/articles/polygonclipper/booleanoperations.md new file mode 100644 index 000000000..22f995b2f --- /dev/null +++ b/articles/polygonclipper/booleanoperations.md @@ -0,0 +1,87 @@ +# Boolean Operations + +Boolean operations are the center of PolygonClipper. They let you combine or subtract polygon regions without dropping down into segment-level geometry code. + +The public entry points are the static methods on [`PolygonClipper`](xref:SixLabors.PolygonClipper.PolygonClipper): + +- `Intersection(subject, clip)` +- `Union(subject, clip)` +- `Difference(subject, clip)` +- `Xor(subject, clip)` + +These are also the recommended entry points in the source, because they route work through internal reusable instances. + +## Choose the Right Operation + +The four operations have different semantics: + +- `Intersection` keeps only the area shared by both inputs. +- `Union` keeps the area covered by either input. +- `Difference` subtracts the clip polygon from the subject polygon. +- `Xor` keeps the non-overlapping parts of both inputs and removes the shared overlap. + +If `Xor` is not a familiar term, it helps to think of it as "union, but with the overlapping middle cut away." + +A few quick cases make the behavior easier to picture: + +- if the two polygons do not touch, `Xor` gives the same result as `Union`; +- if the two polygons are identical, `Xor` returns an empty result; +- if one polygon sits inside the other, `Xor` keeps the outer region and removes the shared inner region. + +`Difference` is the one where argument order matters most. `Difference(a, b)` is not the same as `Difference(b, a)`. + +## Run a Boolean Operation + +```csharp +using SixLabors.PolygonClipper; + +Polygon result = PolygonClipper.Union(subject, clip); +Polygon overlap = PolygonClipper.Intersection(subject, clip); +Polygon remaining = PolygonClipper.Difference(subject, clip); +Polygon exclusive = PolygonClipper.Xor(subject, clip); +``` + +The returned [`Polygon`](xref:SixLabors.PolygonClipper.Polygon) may contain: + +- more than one contour; +- hole relationships; +- different contour counts than either input. + +That is normal. Boolean operations work with regions, not one-contour-in one-contour-out assumptions. + +## Subject and Clip Inputs + +Both input polygons can contain: + +- multiple contours; +- holes; +- disjoint islands; +- non-convex shapes. + +That is one of the main reasons to use PolygonClipper instead of writing one-off rectangle or convex-shape code. + +## Implementation Note + +PolygonClipper's boolean operations are built on a Martinez-Rueda sweep-line pipeline. For most users, the practical takeaway is simply that the library is designed for complex polygon inputs rather than only simple convex cases. + +## Inspect Returned Hierarchy + +The result can include parent-child contour relationships: + +```csharp +for (int i = 0; i < result.Count; i++) +{ + Contour contour = result[i]; + + Console.WriteLine( + $"Contour {i}: Parent={contour.ParentIndex}, Depth={contour.Depth}, Holes={contour.HoleCount}"); +} +``` + +If you care about preserving hole structure or exporting contours to another renderer, inspect that hierarchy instead of assuming every returned contour is a top-level exterior ring. + +## Used by ImageSharp.Drawing + +If you use [ImageSharp.Drawing](../imagesharp.drawing/index.md), this part of PolygonClipper may already be in your rendering pipeline. ImageSharp.Drawing converts path geometry into PolygonClipper polygons and uses these boolean operations internally when combining clipped path regions. + +That makes PolygonClipper a good fit both as a standalone geometry library and as the lower-level model behind higher-level drawing systems. diff --git a/articles/polygonclipper/gettingstarted.md b/articles/polygonclipper/gettingstarted.md new file mode 100644 index 000000000..e5e28d8c5 --- /dev/null +++ b/articles/polygonclipper/gettingstarted.md @@ -0,0 +1,70 @@ +# Getting Started + +The fastest way to get comfortable with PolygonClipper is to think in terms of three building blocks: + +- a [`Vertex`](xref:SixLabors.PolygonClipper.Vertex) is one 2D point; +- a [`Contour`](xref:SixLabors.PolygonClipper.Contour) is one ring of vertices; +- a [`Polygon`](xref:SixLabors.PolygonClipper.Polygon) is a collection of contours. + +From there, most applications either run a boolean operation with [`PolygonClipper`](xref:SixLabors.PolygonClipper.PolygonClipper) or generate stroke-outline geometry with [`PolygonStroker`](xref:SixLabors.PolygonClipper.PolygonStroker). + +## Build Two Input Polygons + +This example creates two rectangles, then intersects them: + +```csharp +using System; +using SixLabors.PolygonClipper; + +static Contour Rectangle(double x, double y, double width, double height) +{ + Contour contour = new(4); + contour.Add(new Vertex(x, y)); + contour.Add(new Vertex(x + width, y)); + contour.Add(new Vertex(x + width, y + height)); + contour.Add(new Vertex(x, y + height)); + return contour; +} + +Polygon subject = new(); +subject.Add(Rectangle(0, 0, 80, 60)); + +Polygon clip = new(); +clip.Add(Rectangle(40, 20, 80, 60)); + +Polygon result = PolygonClipper.Intersection(subject, clip); + +Console.WriteLine($"Contours: {result.Count}"); +Console.WriteLine($"Vertices: {result.VertexCount}"); +``` + +You do not need to repeat the first vertex at the end of a contour for normal polygon operations. Contours are treated as implicitly closed. + +## Prefer the Static Entry Points + +Most applications should call the static methods: + +- `PolygonClipper.Intersection(...)` +- `PolygonClipper.Union(...)` +- `PolygonClipper.Difference(...)` +- `PolygonClipper.Xor(...)` +- `PolygonClipper.Normalize(...)` +- `PolygonStroker.Stroke(...)` + +Those are the recommended entry points in the source and route work through internal reusable instances. The instance constructors are there for advanced manual flows, but they are not the usual starting point. + +## Inspect the Result + +Returned polygons can contain multiple contours, including holes: + +```csharp +for (int i = 0; i < result.Count; i++) +{ + Contour contour = result[i]; + + Console.WriteLine( + $"Contour {i}: Count={contour.Count}, Parent={contour.ParentIndex}, Depth={contour.Depth}, Holes={contour.HoleCount}"); +} +``` + +That contour hierarchy is one of the main things PolygonClipper preserves for you. If you want to understand how parent contours, holes, and winding fit together, the next page to read is [Polygons, Contours, and Holes](polygonsandcontours.md). diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index bc01232ae..addade1e1 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -1,18 +1,19 @@ -# Introduction +# PolygonClipper -### What is PolygonClipper? -PolygonClipper is a high-performance polygon clipping and stroking in C#. +PolygonClipper is Six Labors' focused geometry library for polygon boolean operations, contour normalization, and stroke-outline generation in managed .NET. It is designed for real 2D geometry workloads: non-convex shapes, holes, multiple contours, overlapping edges, and inputs that need canonicalized output. -Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview), PolygonClipper can be used in device, cloud, and embedded/IoT scenarios. - -### License +The current package targets [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview). If you already use [ImageSharp.Drawing](../imagesharp.drawing/index.md), you may already be relying on PolygonClipper indirectly: ImageSharp.Drawing uses it internally for boolean operations against paths and for stroke geometry generation. + +Under the hood, the boolean-operation pipeline is based on a Martinez-Rueda sweep-line approach for complex polygon clipping, while normalization uses a separate Vatti/Clipper2-inspired cleanup path for resolving self-intersections and overlaps into positive-winding output. You do not need to understand those algorithms to use the library well, but it helps explain why PolygonClipper is comfortable with complex contour topology. + +### License PolygonClipper is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] >Starting with PolygonClipper 1.0.0, projects that directly depend on SixLabors.PolygonClipper require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. - + ### Installation - + PolygonClipper is installed via [NuGet](https://www.nuget.org/packages/SixLabors.PolygonClipper) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) @@ -43,3 +44,11 @@ paket add SixLabors.PolygonClipper --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. + +### Start Here + +- [Getting Started](gettingstarted.md) walks through building a polygon from contours and vertices, then running a first boolean operation. +- [Polygons, Contours, and Holes](polygonsandcontours.md) explains the library's core data model and how hierarchy is represented. +- [Boolean Operations](booleanoperations.md) covers `Intersection`, `Union`, `Difference`, and `Xor`, including subject-versus-clip semantics. +- [Normalization and Winding](normalization.md) explains when to use `Normalize(...)` to resolve self-intersections and overlaps into positive-winding output. +- [Stroking](stroking.md) covers `PolygonStroker`, `StrokeOptions`, joins, caps, and open-versus-closed path behavior. diff --git a/articles/polygonclipper/normalization.md b/articles/polygonclipper/normalization.md new file mode 100644 index 000000000..01d1c6714 --- /dev/null +++ b/articles/polygonclipper/normalization.md @@ -0,0 +1,53 @@ +# Normalization and Winding + +Boolean operations combine two polygons. Normalization is different: it cleans up one polygon by resolving self-intersections and overlaps into a canonical positive-winding result. + +That makes [`PolygonClipper.Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) the right tool when your input geometry is already yours, but its contours are messy enough that you want a cleaner region description before export, rendering, or further processing. + +## When to Use `Normalize(...)` + +Normalization is useful when: + +- a contour self-intersects; +- multiple contours overlap and you want one clean regional result; +- you want positive-winding output for a downstream system that depends on winding semantics; +- you want hierarchy and overlap resolution without performing a two-input boolean operation. + +## Normalize a Self-Intersecting Input + +```csharp +using SixLabors.PolygonClipper; + +Contour contour = new(4); +contour.Add(new Vertex(0, 0)); +contour.Add(new Vertex(80, 80)); +contour.Add(new Vertex(0, 80)); +contour.Add(new Vertex(80, 0)); + +Polygon input = new(); +input.Add(contour); + +Polygon normalized = PolygonClipper.Normalize(input); +``` + +The output may have a different contour count and different contour hierarchy than the input. That is expected. Normalization is free to split or reorganize the input region as needed to produce clean positive-winding output. + +## Positive Winding Matters + +The source describes normalization in terms of positive fill semantics. In practice, that means the result is intended for consumers that care about winding-consistent filled regions rather than raw overlapping edges. + +This is especially useful when you are moving polygon data into a renderer, exporter, or geometry pipeline that expects contours to describe filled regions cleanly. + +## Implementation Note + +Normalization is a separate pipeline from the two-input boolean operations. In PolygonClipper it follows a Vatti/Clipper2-inspired approach focused on turning overlapping or self-intersecting contour input into a canonical positive-winding result. + +## Normalization Is Not Required for Every Workflow + +You do not need to call `Normalize(...)` before every boolean operation. The boolean APIs already process complex polygon inputs. + +Reach for normalization when your goal is specifically: + +- cleaning up one polygon rather than combining two; +- resolving self-overlap into a canonical result; +- preparing output for systems that rely on positive-winding contour semantics. diff --git a/articles/polygonclipper/polygonsandcontours.md b/articles/polygonclipper/polygonsandcontours.md new file mode 100644 index 000000000..08c9c8542 --- /dev/null +++ b/articles/polygonclipper/polygonsandcontours.md @@ -0,0 +1,106 @@ +# Polygons, Contours, and Holes + +PolygonClipper's public model is intentionally small. Most of the time you only need to understand three types: + +- [`Vertex`](xref:SixLabors.PolygonClipper.Vertex) for 2D coordinates and basic vector math; +- [`Contour`](xref:SixLabors.PolygonClipper.Contour) for one ring of vertices; +- [`Polygon`](xref:SixLabors.PolygonClipper.Polygon) for a collection of contours. + +That small model is enough to describe simple shapes, complex multi-contour shapes, and polygons with holes. + +## A `Contour` Is One Ring + +A [`Contour`](xref:SixLabors.PolygonClipper.Contour) is a sequence of vertices. For clipping and normalization, it is treated as implicitly closed, so the library always considers an edge between the last vertex and the first vertex. + +That means this is a complete rectangle contour: + +```csharp +using SixLabors.PolygonClipper; + +Contour contour = new(4); +contour.Add(new Vertex(0, 0)); +contour.Add(new Vertex(80, 0)); +contour.Add(new Vertex(80, 60)); +contour.Add(new Vertex(0, 60)); +``` + +There is no need to append `(0, 0)` again at the end unless you are deliberately feeding the stroker a contour you want treated as explicitly closed. + +## A `Polygon` Is a Collection of Contours + +A [`Polygon`](xref:SixLabors.PolygonClipper.Polygon) is simply a list of contours: + +```csharp +using SixLabors.PolygonClipper; + +Polygon polygon = new(); +polygon.Add(contour); +``` + +That is enough for a single simple region. As soon as you need holes or multiple disjoint regions, you add more contours. + +## Hole Hierarchy Is Represented Explicitly + +Contours can participate in a parent-child hierarchy: + +- `ParentIndex` points to the owning contour when a contour is a hole or nested child; +- `HoleCount` and `GetHoleIndex(...)` let an outer contour enumerate its direct holes; +- `Depth` records how deeply nested the contour is; +- `IsExternal` is `true` when `ParentIndex` is `null`. + +If you already know the hierarchy of your input data, you can represent it directly: + +```csharp +using SixLabors.PolygonClipper; + +Polygon polygon = new(2); + +Contour outer = new(4); +outer.Add(new Vertex(0, 0)); +outer.Add(new Vertex(100, 0)); +outer.Add(new Vertex(100, 100)); +outer.Add(new Vertex(0, 100)); + +Contour hole = new(4); +hole.Add(new Vertex(25, 25)); +hole.Add(new Vertex(75, 25)); +hole.Add(new Vertex(75, 75)); +hole.Add(new Vertex(25, 75)); + +polygon.Add(outer); +polygon.Add(hole); + +hole.ParentIndex = 0; +hole.Depth = 1; +outer.AddHoleIndex(1); +``` + +When you do not already know the hierarchy, boolean operations and normalization will compute it for the returned polygon. + +## Orientation Helpers + +[`Contour`](xref:SixLabors.PolygonClipper.Contour) also exposes orientation helpers: + +- `IsCounterClockwise()` +- `IsClockwise()` +- `Reverse()` +- `SetClockwise()` +- `SetCounterClockwise()` + +Those are useful when you are inspecting or preparing contours, but you do not need to normalize orientation by hand for every workflow. If your real goal is to resolve messy self-overlapping input into canonical positive-winding output, use [`PolygonClipper.Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*). + +## Bounding Boxes and Translation + +Both polygons and contours can answer a few practical geometry questions without running a boolean operation: + +- `GetBoundingBox()` returns a [`Box2`](xref:SixLabors.PolygonClipper.Box2) +- `Translate(x, y)` offsets the geometry in place + +```csharp +using SixLabors.PolygonClipper; + +Box2 bounds = polygon.GetBoundingBox(); +polygon.Translate(10, 20); +``` + +Those helpers are especially useful when you are staging input, culling broad regions, or preparing geometry for a later clip or stroke pass. diff --git a/articles/polygonclipper/stroking.md b/articles/polygonclipper/stroking.md new file mode 100644 index 000000000..0dea51876 --- /dev/null +++ b/articles/polygonclipper/stroking.md @@ -0,0 +1,92 @@ +# Stroking + +Stroking in PolygonClipper means turning a path-like input into filled outline geometry. Instead of drawing centerlines directly, [`PolygonStroker`](xref:SixLabors.PolygonClipper.PolygonStroker) emits a polygon that represents the area the stroke would cover. + +That makes it useful both for standalone geometry workflows and for renderers that want stroke outlines as polygons. + +## Use the Static Entry Point + +Most callers should use the static method: + +```csharp +using SixLabors.PolygonClipper; + +Polygon outline = PolygonStroker.Stroke(input, width: 12); +``` + +Like the boolean-operation APIs, this is the recommended entry point in the source and uses internal reusable instances. + +## Stroke an Open Polyline + +```csharp +using SixLabors.PolygonClipper; + +Contour polyline = new(); +polyline.Add(new Vertex(0, 0)); +polyline.Add(new Vertex(60, 20)); +polyline.Add(new Vertex(120, 0)); + +Polygon source = new(); +source.Add(polyline); + +Polygon outline = PolygonStroker.Stroke(source, 12); +``` + +In this case the contour is treated as open, so the emitted geometry includes end caps. + +## Control Joins, Caps, and Output Cleanup + +[`StrokeOptions`](xref:SixLabors.PolygonClipper.StrokeOptions) lets you control the shape of the generated outline: + +```csharp +using SixLabors.PolygonClipper; + +StrokeOptions options = new() +{ + LineJoin = LineJoin.Round, + LineCap = LineCap.Round, + InnerJoin = InnerJoin.Round, + MiterLimit = 4, + InnerMiterLimit = 1.01, + ArcDetailScale = 1, + NormalizeOutput = true +}; + +Polygon outline = PolygonStroker.Stroke(source, 12, options); +``` + +The main knobs are: + +- `LineJoin` for outer corners; +- `LineCap` for open-path ends; +- `InnerJoin` for sharp interior turns; +- `MiterLimit` and `InnerMiterLimit` for clamping long miters; +- `ArcDetailScale` for the smoothness-versus-vertex-count tradeoff on round joins and caps; +- `NormalizeOutput` when you want overlaps and self-intersections in the emitted stroke geometry resolved before returning. + +`NormalizeOutput` defaults to `false` for throughput. When you leave it off, render the returned geometry with a non-zero winding fill rule. + +## Open Versus Closed Stroke Input + +For stroking, PolygonClipper distinguishes between contours that should behave like open polylines and contours that should behave like closed loops. + +If the last vertex returns to the first vertex, or is extremely close to it, the stroker treats the contour as closed and does not emit end caps. Otherwise it treats the contour as open and emits caps. + +That means these two inputs are interpreted differently: + +- a contour whose endpoints are clearly different behaves like an open path; +- a contour whose last vertex returns to its first behaves like a closed path. + +## Width Semantics + +Most callers use a positive width: + +```csharp +Polygon outline = PolygonStroker.Stroke(source, 8); +``` + +Negative widths are supported for advanced scenarios. They flip the emitted side orientation while preserving the width magnitude. + +## Used by ImageSharp.Drawing + +ImageSharp.Drawing also uses PolygonClipper for stroke geometry generation. Its higher-level stroke options are mapped down to PolygonClipper's `LineJoin`, `LineCap`, `InnerJoin`, miter, and normalization settings before outline polygons are generated. diff --git a/articles/toc.md b/articles/toc.md index 445c4adfc..3c782bdd1 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -62,3 +62,8 @@ ## [Troubleshooting](fonts/troubleshooting.md) # [PolygonClipper](polygonclipper/index.md) +## [Getting Started](polygonclipper/gettingstarted.md) +## [Polygons, Contours, and Holes](polygonclipper/polygonsandcontours.md) +## [Boolean Operations](polygonclipper/booleanoperations.md) +## [Normalization and Winding](polygonclipper/normalization.md) +## [Stroking](polygonclipper/stroking.md) diff --git a/index.md b/index.md index a7106f8f1..06bbc164a 100644 --- a/index.md +++ b/index.md @@ -18,7 +18,7 @@ Our libraries are split into focused projects that work well together. They cove You can find documentation for each project in the links below.
-
+
ImageSharp Logo
ImageSharp
@@ -28,7 +28,7 @@ You can find documentation for each project in the links below.
-
+
ImageSharp.Drawing
@@ -38,7 +38,7 @@ You can find documentation for each project in the links below.
-
+
ImageSharp.Web
@@ -48,7 +48,7 @@ You can find documentation for each project in the links below.
-
+
Fonts
@@ -58,7 +58,7 @@ You can find documentation for each project in the links below.
-
+
PolygonClipper Logo
PolygonClipper
diff --git a/templates/modern/public/main.css b/templates/modern/public/main.css index fa734f098..7057a12f8 100644 --- a/templates/modern/public/main.css +++ b/templates/modern/public/main.css @@ -5,7 +5,7 @@ * ################## */ -body{ +body { font-family: "Inter", sans-serif; } @@ -106,16 +106,86 @@ h3 a { fill: #fff; } -body>header, body[data-layout=landing]>header { - box-shadow: 0 0 8px rgb(0 0 0 / 12%); - border-bottom: 0; +body>header, +body[data-layout=landing]>header { + box-shadow: 0 0 8px rgb(0 0 0 / 12%); + border-bottom: 0; } -[data-bs-theme="dark"] body>header, html[data-bs-theme="dark"] body[data-layout=landing]>header { - box-shadow: 0 0 8px rgb(0 0 0 / 48%); - border-bottom: 0; +[data-bs-theme="dark"] body>header, +html[data-bs-theme="dark"] body[data-layout=landing]>header { + box-shadow: 0 0 8px rgb(0 0 0 / 48%); + border-bottom: 0; } header .navbar { text-transform: uppercase; } + +/* ################### + * ##### PRODUCTS #### + * ################### + */ + +.products { + margin-top: 2rem; +} + +.product { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + text-align: center; + height: 100%; + padding-bottom: 2.3667rem; + margin-bottom: 2rem; +} + +@media (min-width: 992px) { + .products { + display: flex; + } + + .product { + padding-bottom: 0; + } +} + +.product img { + max-height: 150px; +} + +.product h5 { + font-size: 1.25rem; +} + +.product h5 a { + text-decoration: none; +} + +.product h5 a::after { + display: none; +} + +.product .btn { + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin: 0 auto; +} + +@media (min-width: 1200px) { + .product .btn { + max-width: 50%; + } +} + +.icon { + max-width: 20px; + margin-right: 0.5rem; +} \ No newline at end of file From 35eab63dbe1d546a2a43556b8410a9b096119c14 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Apr 2026 01:15:04 +1000 Subject: [PATCH 07/21] Update to latest releases --- api/toc.yml | 4 ++-- docfx.json | 2 +- ext/ImageSharp.Web | 2 +- ext/PolygonClipper | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/toc.yml b/api/toc.yml index ff7b86df2..dffe42a44 100644 --- a/api/toc.yml +++ b/api/toc.yml @@ -5,9 +5,9 @@ - name: ImageSharp.Web href: ImageSharp.Web/SixLabors.ImageSharp.Web.yml - name: ImageSharp.Web.Providers.Azure - href: ImageSharp.Web.Providers.Azure/SixLabors.ImageSharp.Web.Providers.Azure.yml + href: ImageSharp.Web.Providers.Azure/SixLabors.ImageSharp.Web.Azure.Resolvers.yml - name: ImageSharp.Web.Providers.AWS - href: ImageSharp.Web.Providers.AWS/SixLabors.ImageSharp.Web.Providers.AWS.yml + href: ImageSharp.Web.Providers.AWS/SixLabors.ImageSharp.Web.AWS.yml - name: Fonts href: Fonts/SixLabors.Fonts.yml - name: PolygonClipper diff --git a/docfx.json b/docfx.json index 50b77f1a3..b93250e6d 100644 --- a/docfx.json +++ b/docfx.json @@ -91,7 +91,7 @@ } }, - { + { "src": [ { "files": [ diff --git a/ext/ImageSharp.Web b/ext/ImageSharp.Web index a1aec1122..037fa9a95 160000 --- a/ext/ImageSharp.Web +++ b/ext/ImageSharp.Web @@ -1 +1 @@ -Subproject commit a1aec1122812e78752c14a066ea20d61350bd39e +Subproject commit 037fa9a95803ca4460c8935d1ca5bc02a08bfc60 diff --git a/ext/PolygonClipper b/ext/PolygonClipper index 96cd7a5ac..f52404c2e 160000 --- a/ext/PolygonClipper +++ b/ext/PolygonClipper @@ -1 +1 @@ -Subproject commit 96cd7a5ac4d29ff8119f039ff4acf9f123d541c3 +Subproject commit f52404c2ee42f2daf996c67012eab3d1223aee55 From d4e1309c5088da0ceb6f4e5e09ec19aa402f791f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Apr 2026 15:33:54 +1000 Subject: [PATCH 08/21] Add web docs and tweak ImageSharp animation docs --- articles/imagesharp.web/configuration.md | 140 ++++++++++++ articles/imagesharp.web/extensibility.md | 105 +++++++++ articles/imagesharp.web/gettingstarted.md | 156 ++++++-------- articles/imagesharp.web/imagecaches.md | 192 ++++++++++------- articles/imagesharp.web/imageproviders.md | 168 +++++++-------- articles/imagesharp.web/index.md | 46 ++-- articles/imagesharp.web/processingcommands.md | 199 ++++++------------ articles/imagesharp.web/security.md | 112 ++++++++++ articles/imagesharp.web/taghelpers.md | 87 ++++++++ articles/imagesharp.web/troubleshooting.md | 83 ++++++++ articles/imagesharp/animatedgif.md | 92 -------- articles/imagesharp/animations.md | 145 +++++++++++++ articles/imagesharp/gif.md | 4 +- articles/imagesharp/imageformats.md | 2 +- articles/imagesharp/processing.md | 2 +- articles/imagesharp/quantization.md | 2 +- articles/imagesharp/security.md | 2 +- articles/toc.md | 15 +- 18 files changed, 1031 insertions(+), 521 deletions(-) create mode 100644 articles/imagesharp.web/configuration.md create mode 100644 articles/imagesharp.web/extensibility.md create mode 100644 articles/imagesharp.web/security.md create mode 100644 articles/imagesharp.web/taghelpers.md create mode 100644 articles/imagesharp.web/troubleshooting.md delete mode 100644 articles/imagesharp/animatedgif.md create mode 100644 articles/imagesharp/animations.md diff --git a/articles/imagesharp.web/configuration.md b/articles/imagesharp.web/configuration.md new file mode 100644 index 000000000..6274fa213 --- /dev/null +++ b/articles/imagesharp.web/configuration.md @@ -0,0 +1,140 @@ +# Configuration and Pipeline + +ImageSharp.Web is assembled through `AddImageSharp()` and the returned [`IImageSharpBuilder`](xref:SixLabors.ImageSharp.Web.IImageSharpBuilder). Most applications never need to replace every piece, but it helps to know what is there so you can change the correct layer without over-customizing the whole pipeline. + +## What `AddImageSharp()` Registers + +The default registration wires up: + +- [`QueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.QueryCollectionRequestParser) for query-string command parsing. +- [`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) for processed-output storage. +- [`UriRelativeLowerInvariantCacheKey`](xref:SixLabors.ImageSharp.Web.Caching.UriRelativeLowerInvariantCacheKey) and [`SHA256CacheHash`](xref:SixLabors.ImageSharp.Web.Caching.SHA256CacheHash) for cache naming. +- [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) for source image resolution. +- The built-in resize, format, quality, background-color, and auto-orient processors. +- The built-in command converters for numbers, booleans, strings, arrays, lists, colors, and enums. + +That gives you a fully working middleware out of the box, but every one of those pieces can be swapped or extended. + +## Configure Middleware Options + +[`ImageSharpMiddlewareOptions`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions) controls the shared middleware behavior: + +- [`Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) is the underlying ImageSharp [`Configuration`](xref:SixLabors.ImageSharp.Configuration). +- [`MemoryStreamManager`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.MemoryStreamManager) controls pooled response streams. +- [`UseInvariantParsingCulture`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.UseInvariantParsingCulture) controls whether command parsing is culture-invariant. +- [`BrowserMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.BrowserMaxAge), [`CacheMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheMaxAge), and [`CacheHashLength`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheHashLength) control cache behavior. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Caching; +using SixLabors.ImageSharp.Web.Providers; + +builder.Services.AddImageSharp(options => +{ + options.Configuration = Configuration.Default.Clone(); + options.UseInvariantParsingCulture = true; + options.BrowserMaxAge = TimeSpan.FromDays(7); + options.CacheMaxAge = TimeSpan.FromDays(30); + options.CacheHashLength = 16; +}) +.Configure(options => +{ + options.ProviderRootPath = "assets"; + options.ProcessingBehavior = ProcessingBehavior.CommandOnly; +}) +.Configure(options => +{ + options.CacheRootPath = "cache"; + options.CacheFolder = "imagesharp"; + options.CacheFolderDepth = 8; +}); +``` + +Use a cloned ImageSharp configuration when you need a different format set, allocator behavior, or other base ImageSharp customization for the middleware. + +## Change Individual Pipeline Pieces + +The builder methods let you replace only the layer you actually need to change: + +- `SetRequestParser()` replaces the request parser. +- `SetCache()` replaces the backend cache. +- `SetCacheKey()` and `SetCacheHash()` change cache naming. +- `AddProvider()`, `InsertProvider()`, `RemoveProvider()`, and `ClearProviders()` manage source providers. +- `AddProcessor()`, `RemoveProcessor()`, and `ClearProcessors()` manage the processing command set. +- `AddConverter()`, `RemoveConverter()`, and `ClearConverters()` manage typed command parsing. +- `Configure(...)` binds or mutates option objects for any registered provider, cache, or parser. + +For example, if you want to keep the default middleware but remove format conversion: + +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Processors; + +builder.Services.AddImageSharp() + .RemoveProcessor(); +``` + +## Use Presets Instead of Free-Form Query Strings + +[`PresetOnlyQueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.PresetOnlyQueryCollectionRequestParser) is the built-in alternative to the normal query parser. Instead of reading every query-string command, it reads a single `preset` key and expands that to a predefined command set. + +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Commands; + +builder.Services.AddImageSharp() + .SetRequestParser() + .Configure(options => + { + options.Presets["thumb"] = "width=160&height=160&rmode=crop"; + options.Presets["card"] = "width=640&height=360&rmode=crop&format=webp&quality=75"; + }); +``` + +That turns requests like `/images/photo.jpg?preset=thumb` into a controlled, named command set without exposing arbitrary query-string processing. + +## Middleware Callbacks + +[`ImageSharpMiddlewareOptions`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions) also exposes targeted callbacks for app-specific customization: + +- [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) runs after a provider has matched the request and after the command set has been sanitized, but before the source image is resolved. +- [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync) can return custom [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) before the source image is decoded. +- [`OnBeforeSaveAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeSaveAsync) can adjust the [`FormattedImage`](xref:SixLabors.ImageSharp.Web.FormattedImage) after processing but before encoding. +- [`OnProcessedAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnProcessedAsync) receives an [`ImageProcessingContext`](xref:SixLabors.ImageSharp.Web.Middleware.ImageProcessingContext) after encoding but before the result is cached. +- [`OnPrepareResponseAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnPrepareResponseAsync) runs after status code and headers are set but before the body is written. + +```csharp +using SixLabors.ImageSharp.Web; + +builder.Services.AddImageSharp(options => +{ + options.OnParseCommandsAsync = context => + { + if (!context.Commands.Contains("format")) + { + context.Commands["format"] = "webp"; + } + + return Task.CompletedTask; + }; + + options.OnPrepareResponseAsync = context => + { + context.Response.Headers["X-ImageSharp"] = "true"; + return Task.CompletedTask; + }; +}); +``` + +These callbacks are often the right tool when you need small workflow adjustments without inventing a custom provider, parser, or processor. + +>[!NOTE] +>`OnParseCommandsAsync` runs after HMAC generation. If you sign requests, keep any command mutations in that callback deterministic and within your own trust boundary. + +## Related Topics + +- [Getting Started](gettingstarted.md) +- [Processing Commands](processingcommands.md) +- [Securing Requests](security.md) +- [Extensibility](extensibility.md) diff --git a/articles/imagesharp.web/extensibility.md b/articles/imagesharp.web/extensibility.md new file mode 100644 index 000000000..9b9e3cac5 --- /dev/null +++ b/articles/imagesharp.web/extensibility.md @@ -0,0 +1,105 @@ +# Extensibility + +ImageSharp.Web is designed as a set of replaceable layers rather than one monolithic middleware. Most customizations only need one of those layers, so the first job is choosing the lightest extension point that matches your problem. + +## Choose the Right Extension Point + +- Use [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) when the request shape is already close to what you want and you only need to add, remove, or normalize commands. +- Use [`IRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.IRequestParser) when the request syntax changes completely. +- Use [`IImageWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.IImageWebProcessor) when you need a new image-processing command. +- Use [`ICommandConverter`](xref:SixLabors.ImageSharp.Web.Commands.Converters.ICommandConverter) when your processor needs a custom typed command value. +- Use [`IImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider) and [`IImageResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageResolver) when source images come from a new backend. +- Use [`IImageCache`](xref:SixLabors.ImageSharp.Web.Caching.IImageCache) and [`IImageCacheResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageCacheResolver) when processed output should be stored in a new backend. +- Use [`ICacheKey`](xref:SixLabors.ImageSharp.Web.Caching.ICacheKey) or [`ICacheHash`](xref:SixLabors.ImageSharp.Web.Caching.ICacheHash) when only cache naming needs to change. + +## Add a Custom Processor + +Custom processors are the usual way to introduce a new query-string command. Implement [`IImageWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.IImageWebProcessor), parse your command values from the [`CommandCollection`](xref:SixLabors.ImageSharp.Web.Commands.CommandCollection), and mutate the [`FormattedImage`](xref:SixLabors.ImageSharp.Web.FormattedImage): + +```csharp +using System.Globalization; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Commands; +using SixLabors.ImageSharp.Web.Processors; + +public sealed class SepiaWebProcessor : IImageWebProcessor +{ + public IEnumerable Commands { get; } = new[] { "sepia" }; + + public FormattedImage Process( + FormattedImage image, + ILogger logger, + CommandCollection commands, + CommandParser parser, + CultureInfo culture) + { + if (parser.ParseValue(commands.GetValueOrDefault("sepia"), culture)) + { + image.Image.Mutate(x => x.Sepia()); + } + + return image; + } + + public bool RequiresTrueColorPixelFormat( + CommandCollection commands, + CommandParser parser, + CultureInfo culture) => false; +} +``` + +Register it with the builder: + +```csharp +builder.Services.AddImageSharp() + .AddProcessor(); +``` + +Processor order is driven by the order of the recognized command keys in the request, so custom processors participate in the same ordering model as the built-in ones. + +## Custom Command Converters + +The built-in converters already cover integral types, floating-point values, booleans, strings, arrays, lists, colors, and enums. If your processor wants a custom command type, implement `ICommandConverter`, register it with `AddConverter()`, then parse it inside the processor with `CommandParser.ParseValue()`. + +This is the right place to centralize parsing rules for custom value syntaxes instead of repeating string parsing inside each processor. + +## Custom Providers and Caches + +Implement a custom provider when your source image is not on disk, in Azure Blob Storage, or in S3. A provider owns request matching and returns a resolver that can: + +- open the source stream; +- report source last-write and cache metadata; +- decide whether requests are `CommandOnly` or always handled. + +When the source maps naturally to an `IFileProvider`, [`FileProviderImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.FileProviderImageProvider) is the easiest base class. + +Implement a custom cache when processed images should live somewhere other than the built-in physical filesystem cache or the cloud caches. A cache receives the hashed key, encoded stream, and [`ImageCacheMetadata`](xref:SixLabors.ImageSharp.Web.ImageCacheMetadata), then later returns an [`IImageCacheResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageCacheResolver) that can reopen the cached entry. + +If you only need different cache naming rather than a whole new backend, replace [`ICacheKey`](xref:SixLabors.ImageSharp.Web.Caching.ICacheKey) or [`ICacheHash`](xref:SixLabors.ImageSharp.Web.Caching.ICacheHash) instead of writing a new cache. + +## Replace the Request Syntax + +Implement [`IRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.IRequestParser) when commands should come from somewhere other than the raw query string, for example: + +- route values; +- a compact signed token; +- database-backed presets; +- application-specific aliases. + +Your parser returns an ordered [`CommandCollection`](xref:SixLabors.ImageSharp.Web.Commands.CommandCollection). That order matters because it is what the middleware uses to decide processor execution order. + +## Extend Razor Integration + +If you add custom processors and want equally natural Razor markup, derive from [`ImageTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper) and override `AddProcessingCommands(...)` to write your custom command keys into the outgoing URL. + +That lets your Razor layer stay strongly typed instead of falling back to raw query-string fragments. + +## Related Topics + +- [Configuration and Pipeline](configuration.md) +- [Processing Commands](processingcommands.md) +- [Image Providers](imageproviders.md) +- [Image Caches](imagecaches.md) +- [Tag Helpers](taghelpers.md) diff --git a/articles/imagesharp.web/gettingstarted.md b/articles/imagesharp.web/gettingstarted.md index f395c7ae0..fa28c2466 100644 --- a/articles/imagesharp.web/gettingstarted.md +++ b/articles/imagesharp.web/gettingstarted.md @@ -1,123 +1,87 @@ # Getting Started ->[!NOTE] ->The official guide assumes intermediate level knowledge of C# and .NET. If you are totally new to .NET development, it might not be the best idea to jump right into a framework as your first step - grasp the basics then come back. Prior experience with other languages and frameworks helps, but is not required. +ImageSharp.Web is easiest to understand as a request pipeline: match a source image, parse commands, process with ImageSharp, cache the result, then serve the cached bytes on later requests. This page shows the smallest setup first and then explains what the default registration gives you. -### Setup and Configuration +## Minimal ASP.NET Core Setup -Once installed you will need to add the following code to `ConfigureServices` and `Configure` in your `Startup.cs` file. +```csharp +using SixLabors.ImageSharp.Web; -This installs the the default service and options. +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -``` c# -public void ConfigureServices(IServiceCollection services) { - // Add the default service and options. - services.AddImageSharp(); -} +builder.Services.AddImageSharp(); -public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { +WebApplication app = builder.Build(); - // Add the image processing middleware. Make sure this appears BEFORE app.UseStaticFiles(), - // otherwise images will be served by ASP.NET's static file middleware before ImageSharp can process them. - app.UseImageSharp(); +app.UseImageSharp(); +app.UseStaticFiles(); - app.UseStaticFiles(); -} +app.Run(); ``` -The fluent configuration is flexible allowing you to configure a multitude of different options. For example you can add the default service and custom options. - -``` c# -// Add the default service and custom options. -services.AddImageSharp( - options => - { - // You only need to set the options you want to change here - // All properties have been listed for demonstration purposes - // only. - options.Configuration = Configuration.Default; - options.MemoryStreamManager = new RecyclableMemoryStreamManager(); - options.BrowserMaxAge = TimeSpan.FromDays(7); - options.CacheMaxAge = TimeSpan.FromDays(365); - options.CacheHashLength = 8; - options.OnParseCommandsAsync = _ => Task.CompletedTask; - options.OnBeforeSaveAsync = _ => Task.CompletedTask; - options.OnProcessedAsync = _ => Task.CompletedTask; - options.OnPrepareResponseAsync = _ => Task.CompletedTask; - }); -``` +`app.UseImageSharp()` must appear before `app.UseStaticFiles()`. If static files run first, requests such as `/images/photo.jpg?width=400` will be served directly from disk and ImageSharp.Web will never see them. + +## What the Default Registration Includes + +`AddImageSharp()` wires up the core middleware services plus a sensible default pipeline: -Or you can fine-grain control adding the default options and configure other services. +- [`QueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.QueryCollectionRequestParser) reads commands from the query string. +- [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) resolves source images from the web root by default. +- [`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) stores processed output under `wwwroot/is-cache` by default. +- [`UriRelativeLowerInvariantCacheKey`](xref:SixLabors.ImageSharp.Web.Caching.UriRelativeLowerInvariantCacheKey) and [`SHA256CacheHash`](xref:SixLabors.ImageSharp.Web.Caching.SHA256CacheHash) create hashed cache filenames. +- [`ResizeWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.ResizeWebProcessor), [`FormatWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.FormatWebProcessor), [`BackgroundColorWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.BackgroundColorWebProcessor), [`QualityWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.QualityWebProcessor), and [`AutoOrientWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.AutoOrientWebProcessor) provide the built-in command set. -``` c# -// Fine-grain control adding the default options and configure other services. -services.AddImageSharp() - .RemoveProcessor() - .RemoveProcessor(); +With that setup in place, requests like these are processed automatically: + +```text +/images/photo.jpg?width=400 +/images/photo.jpg?width=400&height=250&rmode=crop +/images/logo.png?bgcolor=white&format=jpg&quality=85 ``` -There are also factory methods for each builder that will allow building from configuration files. +## A Useful Default Mental Model + +With the default [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider), plain file requests still fall through to static files because it uses [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly). That means: + +- `/images/photo.jpg` is served by ASP.NET Core static files. +- `/images/photo.jpg?width=400` is intercepted and processed by ImageSharp.Web. -``` c# -// Use the factory methods to configure the PhysicalFileSystemCacheOptions -services.AddImageSharp() - .Configure(options => - { - options.CacheFolder = "different-cache"; - }); -``` +This is usually the behavior you want for local images because it keeps the unmodified path fast and predictable. ->[!IMPORTANT] ->ImageSharp.Web v2.0.0 and above contains breaking changes to caching which require additional configuration when migrating from v1.x installs. +## Configure the Physical Provider and Cache -With ImageSharp.Web v2.0.0 a new concept @SixLabors.ImageSharp.Web.Caching.ICacheKey was introduced to allow greater flexibility when generating cached file names. To preserve the v1.x cache format users must configure two settings: +If your source images or cache should live somewhere other than the default web root locations, configure the provider and cache options explicitly: -1. @SixLabors.ImageSharp.Web.Caching.ICacheKey should be configured to use @SixLabors.ImageSharp.Web.Caching.LegacyV1CacheKey -2. @SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCacheOptions.CacheFolderDepth should be configured to use the same value as @SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheHashLength - Default `12`. +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Caching; +using SixLabors.ImageSharp.Web.Providers; -A complete configuration sample allowing the replication of legacy v1.x behavior can be found below: +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -```c# -services.AddImageSharp(options => +builder.Services.AddImageSharp(options => { - // Set to previous default value of CachedNameLength - options.CacheHashLength = 12; - - // Use the same command parsing as v1.x - options.OnParseCommandsAsync = c => - { - if (c.Commands.Count == 0) - { - return Task.CompletedTask; - } - - // It's a good idea to have this to provide very basic security. - // We can safely use the static resize processor properties. - uint width = c.Parser.ParseValue( - c.Commands.GetValueOrDefault(ResizeWebProcessor.Width), - c.Culture); - - uint height = c.Parser.ParseValue( - c.Commands.GetValueOrDefault(ResizeWebProcessor.Height), - c.Culture); - - if (width > 4000 && height > 4000) - { - c.Commands.Remove(ResizeWebProcessor.Width); - c.Commands.Remove(ResizeWebProcessor.Height); - } - - return Task.CompletedTask; - }); + options.BrowserMaxAge = TimeSpan.FromDays(7); + options.CacheMaxAge = TimeSpan.FromDays(30); }) -.Configure(options => +.Configure(options => { - // Ensure this value is the same as CacheHashLength to generate a backwards-compatible cache folder structure - options.CacheFolderDepth = 12; + options.ProviderRootPath = "assets"; }) -.SetCacheKey() -.ClearProviders() -.AddProvider(); +.Configure(options => +{ + options.CacheRootPath = "cache"; + options.CacheFolder = "imagesharp"; + options.CacheFolderDepth = 8; +}); ``` -Full Configuration API options are available [here](xref:SixLabors.ImageSharp.Web.DependencyInjection.ImageSharpBuilderExtensions). +Relative paths are resolved against the application content root. If your app does not define a web root, set both `ProviderRootPath` and `CacheRootPath` explicitly. + +## Next Steps + +- [Configuration and Pipeline](configuration.md) +- [Processing Commands](processingcommands.md) +- [Image Providers](imageproviders.md) +- [Image Caches](imagecaches.md) +- [Securing Requests](security.md) diff --git a/articles/imagesharp.web/imagecaches.md b/articles/imagesharp.web/imagecaches.md index f4a6d6dfd..feeb02e04 100644 --- a/articles/imagesharp.web/imagecaches.md +++ b/articles/imagesharp.web/imagecaches.md @@ -1,122 +1,152 @@ # Image Caches -ImageSharp.Web caches the result of any valid processing operation to allow the fast retrieval of future identical requests. The cache is smart, storing additional metadata to allow the detection of updated source images and can be configured to a fine degree to determine the duration a processed image should be cached for. - ->[!NOTE] ->It is possible to configure your own image cache by implementing and registering your own version of the @"SixLabors.ImageSharp.Web.Caching.IImageCache" interface. +ImageSharp.Web caches processed output so that identical requests do not repeatedly decode, process, and re-encode the source image. The cache stores both the encoded bytes and metadata about the source and response so the middleware can detect stale entries and serve correct headers. -The following caches are available for the middleware. +## How the Cache Works -### PhysicalFileSystemCache +For each processed request, the middleware: -The @"SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache", by default, stores processed image files in the [web root](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/?view=aspnetcore-3.1&tabs=macos#web-root) folder. This is the default cache installed when configuring the middleware. - -Images are cached in separate folders based upon a hash of the request URL. this allows the caching of millions of image files without slowing down the file system. - -### AzureBlobStorageImageCache - -This cache allows the caching of image files using [Azure Blob Storage](https://docs.microsoft.com/en-us/azure/storage/blobs/) and is available as an external package installable via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web.Providers.Azure) +- builds a cache key from the request path plus the sanitized command collection; +- hashes that key into a filesystem-safe cache name; +- stores the encoded image plus metadata such as source last-write time, cache write time, content type, browser max-age, and content length; +- reuses the cached result until the source changes or the cache entry ages beyond [`ImageSharpMiddlewareOptions.CacheMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheMaxAge). -# [Package Manager](#tab/tabid-1) +## Default Physical Cache -```bash -PM > Install-Package SixLabors.ImageSharp.Web.Providers.Azure -Version VERSION_NUMBER -``` +[`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) is the default backend registered by `AddImageSharp()`. -# [.NET CLI](#tab/tabid-2) +- It stores cached files under the web root by default. +- [`PhysicalFileSystemCacheOptions.CacheFolder`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCacheOptions.CacheFolder) defaults to `is-cache`. +- [`PhysicalFileSystemCacheOptions.CacheFolderDepth`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCacheOptions.CacheFolderDepth) defaults to `8`, which spreads cached files across nested folders. -```bash -dotnet add package SixLabors.ImageSharp.Web.Providers.Azure --version VERSION_NUMBER +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Caching; + +builder.Services.AddImageSharp() + .Configure(options => + { + options.CacheRootPath = "cache"; + options.CacheFolder = "imagesharp"; + options.CacheFolderDepth = 8; + }); ``` -# [PackageReference](#tab/tabid-3) +If your app does not define a web root, set `CacheRootPath` explicitly. Relative paths are resolved against the application content root. -```xml - -``` +## Browser Lifetime Versus Backend Lifetime -# [Paket CLI](#tab/tabid-4) +ImageSharp.Web tracks two different lifetimes: -```bash -paket add SixLabors.ImageSharp.Web.Providers.Azure --version VERSION_NUMBER -``` +- [`ImageSharpMiddlewareOptions.BrowserMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.BrowserMaxAge) controls the `Cache-Control` lifetime sent to clients. +- [`ImageSharpMiddlewareOptions.CacheMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheMaxAge) controls how long the processed result stays valid in the backend cache. -*** +If the source provider supplies a source `Cache-Control` max-age, that value overrides `BrowserMaxAge` for the response. -Once installed the cache @SixLabors.ImageSharp.Web.Caching.Azure.AzureBlobStorageCacheOptions can be configured as follows: +## Cache Keys and Hashes +By default, ImageSharp.Web uses: -```c# -// Configure and register the container. -// Alteratively use `appsettings.json` to represent the class and bind those settings. -.Configure(options => +- [`UriRelativeLowerInvariantCacheKey`](xref:SixLabors.ImageSharp.Web.Caching.UriRelativeLowerInvariantCacheKey) to turn the request path and command collection into a canonical cache key. +- [`SHA256CacheHash`](xref:SixLabors.ImageSharp.Web.Caching.SHA256CacheHash) to hash that key into the stored filename. +- [`ImageSharpMiddlewareOptions.CacheHashLength`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheHashLength) to control how many hash characters are kept. + +If you need cache entries to vary by host or some other request detail, swap the key implementation: + +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Caching; + +builder.Services.AddImageSharp(options => { - options.ConnectionString = {AZURE_CONNECTION_STRING}; - options.ContainerName = {AZURE_CONTAINER_NAME}; - - // Optionally use a cache folder under the container. - options.CacheFolder = {AZURE_CACHE_FOLDER}; - - // Optionally create the cache container on startup if not already created. - AzureBlobStorageCache.CreateIfNotExists(options, PublicAccessType.None); + options.CacheHashLength = 16; }) -.SetCache() +.SetCacheKey() +.SetCacheHash(); ``` -Images are cached using a hash of the request URL as the blob name. All appropriate metadata is stored in the blob properties to correctly serve the blob with the correct response headers. +## Preserve the v1 Cache Layout +If you are migrating an older installation and want new requests to keep using the v1 cache naming layout, switch to [`LegacyV1CacheKey`](xref:SixLabors.ImageSharp.Web.Caching.LegacyV1CacheKey) and keep the folder depth aligned with the hash length: -### AWSS3StorageCache - -This cache allows the caching of image files using [Amazon Simple Storage Service (Amazon S3)](https://aws.amazon.com/s3/) and is available as an external package installable via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web.Providers.AWS) +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Caching; -# [Package Manager](#tab/tabid-1a) - -```bash -PM > Install-Package SixLabors.ImageSharp.Web.Providers.AWS -Version VERSION_NUMBER +builder.Services.AddImageSharp(options => +{ + options.CacheHashLength = 12; +}) +.Configure(options => +{ + options.CacheFolderDepth = 12; +}) +.SetCacheKey(); ``` -# [.NET CLI](#tab/tabid-2a) +## Azure Blob Storage Cache + +Install the Azure provider package: ```bash -dotnet add package SixLabors.ImageSharp.Web.Providers.AWS --version VERSION_NUMBER +dotnet add package SixLabors.ImageSharp.Web.Providers.Azure ``` -# [PackageReference](#tab/tabid-3a) +Then replace the default cache backend: -```xml - -``` +```csharp +using Azure.Storage.Blobs.Models; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Azure.Caching; -# [Paket CLI](#tab/tabid-4a) +builder.Services.AddImageSharp() + .Configure(options => + { + options.ConnectionString = builder.Configuration["Azure:ConnectionString"]!; + options.ContainerName = "imagesharp-cache"; + options.CacheFolder = "processed"; -```bash -paket add SixLabors.ImageSharp.Web.Providers.AWS --version VERSION_NUMBER + AzureBlobStorageCache.CreateIfNotExists(options, PublicAccessType.None); + }) + .SetCache(); ``` -*** +Cached objects use the hashed request key as the blob name, and the cache metadata is stored in blob properties alongside the object. -Once installed the cache @SixLabors.ImageSharp.Web.Caching.AWS.AWSS3StorageCacheOptions can be configured as follows: +## AWS S3 Cache +Install the AWS provider package: -```c# -// Configure and register the bucket. -// Alteratively use `appsettings.json` to represent the class and bind those settings. -.Configure(options => -{ - options.Endpoint = {AWS_ENDPOINT}; - options.BucketName = {AWS_BUCKET_NAME}; - options.AccessKey = {AWS_ACCESS_KEY}; - options.AccessSecret = {AWS_ACCESS_SECRET}; - options.Region = {AWS_REGION}; - - // Optionally use a cache folder under the bucket. - options.CacheFolder = {AWS_CACHE_FOLDER}; - - // Optionally create the cache bucket on startup if not already created. - AWSS3StorageCache.CreateIfNotExists(options, S3CannedACL.Private); -}) -.SetCache() +```bash +dotnet add package SixLabors.ImageSharp.Web.Providers.AWS +``` + +Then replace the default cache backend: + +```csharp +using Amazon.S3; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.AWS.Caching; + +builder.Services.AddImageSharp() + .Configure(options => + { + options.BucketName = "imagesharp-cache"; + options.Region = "us-east-1"; + options.AccessKey = builder.Configuration["AWS:AccessKey"]; + options.AccessSecret = builder.Configuration["AWS:SecretKey"]; + options.CacheFolder = "processed"; + + AWSS3StorageCache.CreateIfNotExists(options, S3CannedACL.Private); + }) + .SetCache(); ``` -Images are cached using a hash of the request URL as the object name. All appropriate metadata is stored in the object properties to correctly serve the object with the correct response headers. +Cached objects use the hashed request key as the object key, and the response metadata needed by the middleware is stored with the object. + +## Related Topics + +- [Getting Started](gettingstarted.md) +- [Image Providers](imageproviders.md) +- [Securing Requests](security.md) +- [Extensibility](extensibility.md) diff --git a/articles/imagesharp.web/imageproviders.md b/articles/imagesharp.web/imageproviders.md index 136c256a4..d4b13f9c9 100644 --- a/articles/imagesharp.web/imageproviders.md +++ b/articles/imagesharp.web/imageproviders.md @@ -1,126 +1,126 @@ # Image Providers -ImageSharp.Web determines the location of a source image to process via the registration and application of image providers. - ->[!NOTE] ->It is possible to configure your own image provider by implementing and registering your own version of the @"SixLabors.ImageSharp.Web.Providers.IImageProvider" interface. +Image providers answer one question: where does the source image come from? Every incoming request is offered to the registered providers in order, and the first provider whose `Match` function returns `true` owns the request. -The following providers are available for the middleware. Multiples providers can be registered and will be queried for a URL match in the order of registration. +That means provider order matters. If two providers can both match the same path, put the more specific one first or narrow its `Match` predicate so the wrong provider does not claim the request. -### PhysicalFileSystemProvider +## Default Physical Filesystem Provider -The @"SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider" will allow the processing and serving of image files from the [web root](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/?view=aspnetcore-3.1&tabs=macos#web-root) folder. This is the default provider installed when configuring the middleware. - -Url matching for this provider follows the same rules as conventional static files. +[`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) is the default source provider registered by `AddImageSharp()`. -### AzureBlobStorageImageProvider - -This provider allows the processing and serving of image files from [Azure Blob Storage](https://docs.microsoft.com/en-us/azure/storage/blobs/) and is available as an external package installable via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web.Providers.Azure) +- It resolves images from the web root by default. +- [`PhysicalFileSystemProviderOptions.ProviderRootPath`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProviderRootPath) can be `null`, absolute, or relative to the application content root. +- [`PhysicalFileSystemProviderOptions.ProcessingBehavior`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProcessingBehavior) defaults to [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly), so commandless requests still fall through to static files. -# [Package Manager](#tab/tabid-1) +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Providers; -```bash -PM > Install-Package SixLabors.ImageSharp.Web.Providers.Azure -Version VERSION_NUMBER +builder.Services.AddImageSharp() + .Configure(options => + { + options.ProviderRootPath = "assets"; + options.ProcessingBehavior = ProcessingBehavior.CommandOnly; + }); ``` -# [.NET CLI](#tab/tabid-2) +If you want a provider fixed to `IWebHostEnvironment.WebRootFileProvider` with no extra options, [`WebRootImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.WebRootImageProvider) is also available. -```bash -dotnet add package SixLabors.ImageSharp.Web.Providers.Azure --version VERSION_NUMBER -``` +## Provider Matching and Ordering -# [PackageReference](#tab/tabid-3) +ImageSharp.Web stops at the first provider whose `Match` function returns `true`. It does not continue searching if that provider later decides the request is invalid, so keep these rules in mind: -```xml - -``` +- Register more specific providers before more general ones. +- Keep `Match` predicates mutually exclusive whenever possible. +- Use `InsertProvider(...)` when provider precedence matters more than registration order. -# [Paket CLI](#tab/tabid-4) +Cloud providers in particular usually want a path prefix such as a container or bucket name so they can distinguish their requests cheaply. + +## Azure Blob Storage + +Install the Azure provider package: ```bash -paket add SixLabors.ImageSharp.Web.Providers.Azure --version VERSION_NUMBER +dotnet add package SixLabors.ImageSharp.Web.Providers.Azure ``` -*** - -Once installed the provider @"SixLabors.ImageSharp.Web.Providers.Azure.AzureBlobContainerClientOptions" can be configured as follows: +Then configure one or more containers: +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Azure.Providers; -```c# -// Configure and register the containers. -// Alteratively use `appsettings.json` to represent the class and bind those settings. -.Configure(options => -{ - // The "BlobContainers" collection allows registration of multiple containers. - options.BlobContainers.Add(new AzureBlobContainerClientOptions +builder.Services.AddImageSharp() + .ClearProviders() + .Configure(options => { - ConnectionString = {AZURE_CONNECTION_STRING}, - ContainerName = {AZURE_CONTAINER_NAME} - }); -}) -.AddProvider() + options.BlobContainers.Add(new AzureBlobContainerClientOptions + { + ConnectionString = builder.Configuration["Azure:ConnectionString"]!, + ContainerName = "public-images" + }); + }) + .AddProvider(); ``` -Url requests are matched in accordance to the following rule: - -```bash -/{CONTAINER_NAME}/{BLOB_FILENAME} +Requests are matched by container name at the start of the path: + +```text +/public-images/avatars/jane.png?width=200 ``` -### AWSS3StorageImageProvider - -This provider allows the processing and serving of image files from [Amazon Simple Storage Service (Amazon S3)](https://aws.amazon.com/s3/) and is available as an external package installable via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web.Providers.AWS) +[`AzureBlobStorageImageProvider`](xref:SixLabors.ImageSharp.Web.Azure.Providers.AzureBlobStorageImageProvider) uses [`ProcessingBehavior.All`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.All), so it can serve both processed and commandless requests. -# [Package Manager](#tab/tabid-1a) +## AWS S3 + +Install the AWS provider package: ```bash -PM > Install-Package SixLabors.ImageSharp.Web.Providers.AWS -Version VERSION_NUMBER +dotnet add package SixLabors.ImageSharp.Web.Providers.AWS ``` -# [.NET CLI](#tab/tabid-2a) +Then configure one or more buckets: -```bash -dotnet add package SixLabors.ImageSharp.Web.Providers.AWS --version VERSION_NUMBER +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.AWS.Providers; + +builder.Services.AddImageSharp() + .ClearProviders() + .Configure(options => + { + options.S3Buckets.Add(new AWSS3BucketClientOptions + { + BucketName = "public-images", + Region = "us-east-1", + AccessKey = builder.Configuration["AWS:AccessKey"], + AccessSecret = builder.Configuration["AWS:SecretKey"] + }); + }) + .AddProvider(); ``` -# [PackageReference](#tab/tabid-3a) +Requests are matched by bucket name at the start of the path: -```xml - +```text +/public-images/avatars/jane.png?width=200 ``` -# [Paket CLI](#tab/tabid-4a) +If your public URL shape does not naturally include the bucket name, use URL rewriting before ImageSharp.Web or implement a custom provider. -```bash -paket add SixLabors.ImageSharp.Web.Providers.AWS --version VERSION_NUMBER -``` +## Implementing Your Own Provider -*** +Implement [`IImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider) when you need a new source backend. Your provider is responsible for three things: -Once installed the provider @SixLabors.ImageSharp.Web.Providers.AWS.AWSS3StorageImageProviderOptions can be configured as follows: +- deciding whether it owns the request via `Match`; +- deciding whether the request is valid via `IsValidRequest(...)`; +- returning an [`IImageResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageResolver) that can open the source stream and report source metadata. -```c# -// Configure and register the buckets. -// Alteratively use `appsettings.json` to represent the class and bind those settings. -.Configure(options => -{ - // The "S3Buckets" collection allows registration of multiple buckets. - options.S3Buckets.Add(new AWSS3BucketClientOptions - { - Endpoint = AWS_ENDPOINT, - BucketName = AWS_BUCKET_NAME, - AccessKey = AWS_ACCESS_KEY, - AccessSecret = AWS_ACCESS_SECRET, - Region = AWS_REGION - }); -}) -.AddProvider() -``` +If your source already fits an `IFileProvider`-style model, [`FileProviderImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.FileProviderImageProvider) is the easiest base class to start from. -Url requests are matched in accordance to the following rule: - -```bash -/{AWS_BUCKET_NAME}/{OBJECT_FILENAME} -``` +## Related Topics -Which is to say that the AWS S3 bucket name must appear in the Url so it can be matched with the correct S3 configuration. If you wished to override this and provide a deafult, this can be done using [URL Rewriting middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-6.0). +- [Getting Started](gettingstarted.md) +- [Image Caches](imagecaches.md) +- [Extensibility](extensibility.md) +- [Troubleshooting](troubleshooting.md) diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 4d512a07f..5da3c286f 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -1,19 +1,19 @@ -# Introduction +# ImageSharp.Web -### What is ImageSharp.Web? -ImageSharp.Web is a high performance ASP.NET 6 Middleware built on top of ImageSharp that allows the processing and caching of image requests via a simple API. +ImageSharp.Web is Six Labors' ASP.NET Core image middleware for on-the-fly processing and caching. It sits in front of one or more image providers, parses URL commands, runs the matching ImageSharp processors, and stores the result so repeated requests are inexpensive after the first hit. -ImageSharp.Web is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional extensions to add image sources, caching mechanisms or even your own processing API. +The current package targets .NET 8 and is built on top of [ImageSharp](../imagesharp/index.md). The middleware is intentionally modular: you can change how commands are parsed, where source images come from, how cache keys are built, where processed images are stored, and whether image requests must be signed. + +## License -### License ImageSharp.Web is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Web/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] >Starting with ImageSharp.Web 4.0.0, projects that directly depend on ImageSharp.Web require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. - -### Installation - -ImageSharp.Web is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). + +## Install ImageSharp.Web + +ImageSharp.Web is distributed on [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Web) with preview and nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) @@ -44,13 +44,21 @@ paket add SixLabors.ImageSharp.Web --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. -### Implicit Usings +## Start Here -The `UseImageSharp` property controls whether **implicit `global using` directives** for ImageSharp are included in your C# project. This feature is available in projects targeting **.NET 6 or later** with **C# 10 or later**. +- [Getting Started](gettingstarted.md) covers the minimal ASP.NET Core setup and the default provider and cache behavior. +- [Configuration and Pipeline](configuration.md) explains what `AddImageSharp()` registers and how to replace or reorder the moving parts. +- [Processing Commands](processingcommands.md) documents the built-in resize, auto-orient, format, quality, and background-color commands. +- [Image Providers](imageproviders.md) covers filesystem, Azure Blob Storage, and AWS S3 source images. +- [Image Caches](imagecaches.md) covers the default physical cache, cloud cache backends, cache keys, and cache lifetime. +- [Securing Requests](security.md) explains HMAC signing and preset-only parsing. +- [Tag Helpers](taghelpers.md) covers Razor integration and automatic HMAC generation. +- [Extensibility](extensibility.md) walks through custom processors, parsers, providers, caches, and converters. +- [Troubleshooting](troubleshooting.md) covers the most common middleware-order, provider, cache, and signing problems. -When enabled, a predefined set of `global using` directives for common ImageSharp namespaces (such as `SixLabors.ImageSharp`, `SixLabors.ImageSharp.Processing`, `SixLabors.ImageSharp.Web` etc.) is automatically added to the compilation. This eliminates the need to manually add `using` statements in every file. +## Implicit Usings -To enable implicit ImageSharp usings, set the property in your project file: +Set `UseImageSharp` in your project file to automatically import the most common ImageSharp and ImageSharp.Web namespaces: ```xml @@ -58,10 +66,10 @@ To enable implicit ImageSharp usings, set the property in your project file: ``` -To disable the feature, either remove the property or set it to `false`: +When enabled, ImageSharp.Web adds implicit `global using` directives for: -```xml - - false - -``` +- `SixLabors.ImageSharp` +- `SixLabors.ImageSharp.Processing` +- `SixLabors.ImageSharp.Web` + +You can turn this off by removing the property or setting it to `false`. diff --git a/articles/imagesharp.web/processingcommands.md b/articles/imagesharp.web/processingcommands.md index 62eed52f4..e1855a0af 100644 --- a/articles/imagesharp.web/processingcommands.md +++ b/articles/imagesharp.web/processingcommands.md @@ -1,172 +1,95 @@ # Processing Commands -The ImageSharp.Web processing API is imperative. This means that the order in which you supply the individual processing operations is the order in which they are compiled and applied. This allows the API to be very flexible, allowing you to combine processes in any order. - ->[!NOTE] ->It is possible to configure your own processing command pipeline by implementing and registering your own version of the @"SixLabors.ImageSharp.Web.Commands.IRequestParser" interface. +ImageSharp.Web ships with a small set of built-in processors that cover the most common web-image tasks: resize, EXIF-aware orientation, format conversion, quality control, and alpha flattening. By default those commands come from the query string, but the same processors also work with custom request parsers or Razor tag helpers. -The following processors are built into the middleware. In addition extension points are available to register your own command processors. +## How Command Execution Works -#### Resize +The default [`QueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.QueryCollectionRequestParser) reads query-string pairs into an ordered [`CommandCollection`](xref:SixLabors.ImageSharp.Web.Commands.CommandCollection). A few details are worth knowing: -Allows the resizing of images. +- If the same command key appears more than once, the last value wins. +- Unknown commands are stripped before HMAC validation and before the processor pipeline runs. +- Processors run in the order their first recognized command appears in the request, not in a hard-coded global order. +- Values are parsed with invariant culture by default. If you turn that off, parsing follows `CultureInfo.CurrentCulture`. ->[!NOTE] ->In V3 this processor will automatically correct the order of dimensional commands based on the presence of EXIF metadata indicating rotated (not flipped) images. ->This behavior can be turned off per request. +## Resize -``` bash -{PATH_TO_YOUR_IMAGE}?width=300 -{PATH_TO_YOUR_IMAGE}?width=300&height=120&rxy=0.37,0.78 -{PATH_TO_YOUR_IMAGE}?width=50&height=50&rsampler=nearest&rmode=stretch -{PATH_TO_YOUR_IMAGE}?width=300&compand=true&orient=false -``` -Resize commands represent the @"SixLabors.ImageSharp.Processing.ResizeOptions" class. - -- `width` The width of the image in px. Use only one dimension to preseve the aspect ratio. -- `height` The height of the image in px. Use only one dimension to preseve the aspect ratio. -- `rmode` The @"SixLabors.ImageSharp.Processing.ResizeMode" to use. -- `rsampler` The @"SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler" -sampler to use. - - `bicubic` @"SixLabors.ImageSharp.Processing.KnownResamplers.Bicubic" - - `nearest` @"SixLabors.ImageSharp.Processing.KnownResamplers.NearestNeighbor" - - `box` @"SixLabors.ImageSharp.Processing.KnownResamplers.Box" - - `mitchell` @"SixLabors.ImageSharp.Processing.KnownResamplers.MitchellNetravali" - - `catmull` @"SixLabors.ImageSharp.Processing.KnownResamplers.CatmullRom" - - `lanczos2` @"SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos2" - - `lanczos3` @"SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos3" - - `lanczos5` @"SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos5" - - `lanczos8` @"SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos8" - - `welch` @"SixLabors.ImageSharp.Processing.KnownResamplers.Welch" - - `robidoux` @"SixLabors.ImageSharp.Processing.KnownResamplers.Robidoux" - - `robidouxsharp` @"SixLabors.ImageSharp.Processing.KnownResamplers.RobidouxSharp" - - `spline` @"SixLabors.ImageSharp.Processing.KnownResamplers.Spline" - - `triangle` @"SixLabors.ImageSharp.Processing.KnownResamplers.Triangle" - - `hermite` @"SixLabors.ImageSharp.Processing.KnownResamplers.Hermite" -- `ranchor`The @"SixLabors.ImageSharp.Processing.AnchorPositionMode" to use. -- `rxy` Use an exact anchor position point. The comma-separated x and y values range from 0-1. -- `orient` Whether to swap command dimensions based on the presence of EXIF metadata indicating rotated (not flipped) images. Defaults to `true` -- `compand` Whether to compress and expand individual pixel colors values to/from a linear color space when processing. Defaults to `false` - - -#### Format - -Allows the encoding of the output image to a new image format. The available formats depend on your configuration settings. +Resize commands are handled by [`ResizeWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.ResizeWebProcessor) and map to [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions). +```text +/images/photo.jpg?width=300 +/images/photo.jpg?width=300&height=200&rmode=crop +/images/photo.jpg?width=300&height=200&rmode=pad&rcolor=limegreen +/images/photo.jpg?width=300&height=200&rxy=0.37,0.78 +/images/photo.jpg?width=300&rsampler=lanczos3&compand=true ``` -{PATH_TO_YOUR_IMAGE}?format=bmp -{PATH_TO_YOUR_IMAGE}?format=gif -{PATH_TO_YOUR_IMAGE}?format=jpg -{PATH_TO_YOUR_IMAGE}?format=pbm -{PATH_TO_YOUR_IMAGE}?format=png -{PATH_TO_YOUR_IMAGE}?format=tga -{PATH_TO_YOUR_IMAGE}?format=tiff -{PATH_TO_YOUR_IMAGE}?format=webp -``` - -#### Quality - -Allows the encoding of the output image at the given quality. - -- For Jpeg this ranges from 1—100. -- For WebP this ranges from 1—100. -``` -{PATH_TO_YOUR_IMAGE}?quality=90 -{PATH_TO_YOUR_IMAGE}?format=jpg&quality=42 -``` +- `width` and `height` set the target dimensions in pixels. If you provide only one dimension, the original aspect ratio is preserved. +- `rmode` selects the [`ResizeMode`](xref:SixLabors.ImageSharp.Processing.ResizeMode). Common values are `crop`, `pad`, `boxpad`, `max`, `min`, `stretch`, and `manual`. +- `ranchor` selects the [`AnchorPositionMode`](xref:SixLabors.ImageSharp.Processing.AnchorPositionMode). Valid values are `center`, `top`, `bottom`, `left`, `right`, `topleft`, `topright`, `bottomright`, and `bottomleft`. +- `rxy` supplies an exact focal point as `x,y`, where both values are between `0` and `1`. +- `rcolor` sets the pad color for resize modes that add canvas area. +- `rsampler` selects the resampler. Built-in keywords are `bicubic`, `nearest`, `box`, `mitchell`, `catmull`, `lanczos2`, `lanczos3`, `lanczos5`, `lanczos8`, `welch`, `robidoux`, `robidouxsharp`, `spline`, `triangle`, and `hermite`. +- `orient` defaults to `true` and changes how resize interprets EXIF rotation when mapping dimensions, anchors, and focal points. It does not physically rotate the pixels. +- `compand` toggles linear-light companding during the resize. ->[!NOTE] ->Only certain formats support adjustable quality. This is a constraint of individual image standards not the API. +`orient` is easy to confuse with `autoorient`. The short version is that `orient` only changes resize math, while `autoorient` actually rotates or flips the decoded image. -#### Background Color +## Auto-Orient -Allows the changing of the background color of transparent images. +[`AutoOrientWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.AutoOrientWebProcessor) applies EXIF orientation to the decoded image before later processors run. +```text +/images/photo.jpg?autoorient=true +/images/photo.jpg?autoorient=true&width=300&height=200&rmode=crop ``` -{PATH_TO_YOUR_IMAGE}?bgcolor=FFFF00 -{PATH_TO_YOUR_IMAGE}?bgcolor=C1FF0080 -{PATH_TO_YOUR_IMAGE}?bgcolor=red -{PATH_TO_YOUR_IMAGE}?bgcolor=128,64,32 -{PATH_TO_YOUR_IMAGE}?bgcolor=128,64,32,16 -``` - -## Securing Processing Commands - -With ImageSharp.Web it is possible to configure an action to generate an HMAC by setting the @SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.HMACSecretKey property to any byte array value. This triggers checks in the middleware to look for and compare a HMAC hash of the request URL with the hash that is passed alongside the commands. -In cryptography, an HMAC (sometimes expanded as either keyed-hash message authentication code or hash-based message authentication code) is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key. As with any MAC, it may be used to simultaneously verify both the data integrity and authenticity of a message. +Use `autoorient=true` when you want the output pixels themselves to be normalized to the display orientation. -HMAC can provide authentication using a shared secret instead of using digital signatures with asymmetric cryptography. It trades off the need for a complex public key infrastructure by delegating the key exchange to the communicating parties, who are responsible for establishing and using a trusted channel to agree on the key prior to communication. +## Format -Any cryptographic hash function, such as SHA-2 or SHA-3, may be used in the calculation of an HMAC; the resulting MAC algorithm is termed HMAC-X, where X is the hash function used (e.g. HMAC-SHA256 or HMAC-SHA3-512). The cryptographic strength of the HMAC depends upon the cryptographic strength of the underlying hash function, the size of its hash output, and the size and quality of the key. +[`FormatWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.FormatWebProcessor) switches the encoder used for the response and cached output. -HMAC does not encrypt the message. Instead, the message (encrypted or not) must be sent alongside the HMAC hash. Parties with the secret key will hash the message again themselves, and if it is authentic, the received and computed hashes will match. - -By default ImageSharp.Web will use a HMAC-SHA256 algorithm. - -```c# -private Func> onComputeHMACAsync = (context, secret) => -{ - string uri = CaseHandlingUriBuilder.BuildRelative( - CaseHandlingUriBuilder.CaseHandling.LowerInvariant, - context.Context.Request.PathBase, - context.Context.Request.Path, - QueryString.Create(context.Commands)); - - return Task.FromResult(HMACUtilities.ComputeHMACSHA256(uri, secret)); -}; +```text +/images/logo.png?format=jpg +/images/logo.png?format=webp +/images/logo.png?width=300&format=gif ``` -Users can replicate that key using the same @SixLabors.ImageSharp.Web.CaseHandlingUriBuilder and @SixLabors.ImageSharp.Web.HMACUtilities APIs to generate the HMAC hash on the client. The hash must be passed via a command using the @SixLabors.ImageSharp.Web.HMACUtilities.TokenCommand constant. - -Any invalid matches are rejected at the very start of the processing pipeline with a 400 HttpResponse code. - -## ImageTagHelper +Any file extension registered with the active [`ImageFormatsManager`](xref:SixLabors.ImageSharp.Configuration.ImageFormatsManager) can be used here. The exact set therefore depends on the underlying ImageSharp configuration. -ASP.NET tag helpers are useful because they provide a more natural syntax for creating HTML elements in server-side code. They allow developers to create HTML elements in a way that is similar to how they would write HTML markup in a Razor view. +## Quality -Some of the benefits of using tag helpers include: +[`QualityWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.QualityWebProcessor) controls encoder quality for JPEG and WebP output. -1. Improved readability: Tag helpers make it easier to understand the purpose of the code by providing a clear and concise syntax that is closer to HTML. -2. Reduced complexity: Tag helpers simplify the creation of complex HTML elements by reducing the amount of boilerplate code needed. -3. Type safety: Tag helpers are strongly typed, which means that the compiler can catch errors at compile time rather than at runtime. -4. Testability: Tag helpers make it easier to unit test server-side code by providing a cleaner separation of concerns between the server-side code and the HTML markup. -5. Code reuse: Tag helpers can be used to encapsulate commonly used HTML elements, making it easier to reuse code across multiple views and pages. - -Overall, ASP.NET tag helpers provide a more efficient and maintainable way to create HTML elements in server-side code. - -ImageSharp.Web v3.0.0 comes equipped with a custom tag helper that allows the generation of all the commands supported by the middleware in an easily accessible manner. This includes automatic generation of HMAC command tokens. +```text +/images/photo.jpg?quality=90 +/images/photo.jpg?format=jpg&quality=42 +/images/photo.jpg?format=webp&quality=75 +``` ->[!NOTE] ->Using @SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper is the recommended way to generate processing commands. +Quality values are clamped by the target encoder. For WebP, values below `100` switch the encoder to lossy mode. -To use @SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper, add the following `using` and `addTagHelper` commands to `_ViewImports.cshtml` in your project. +## Background Color -```html -@using SixLabors.ImageSharp -@using SixLabors.ImageSharp.Processing -@using SixLabors.ImageSharp.Web +[`BackgroundColorWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.BackgroundColorWebProcessor) fills transparent areas with a color. -@addTagHelper *, SixLabors.ImageSharp.Web +```text +/images/logo.png?bgcolor=FFFF00 +/images/logo.png?bgcolor=C1FF0080 +/images/logo.png?bgcolor=red +/images/logo.png?bgcolor=128,64,32 +/images/logo.png?bgcolor=128,64,32,16 ``` -All ImageSharp.Web commands are strongly typed and prefixed with `imagesharp` to namespace them against potentially conflicting commands. Visual Studio intellisense with automatically provide guidance -once you start typing. For example, the following markup... +This is most useful when flattening transparent images before converting them to opaque formats such as JPEG: -```html - +```text +/images/logo.png?bgcolor=white&format=jpg&quality=85 ``` -Will generate the following command when HMAC is enabled. - -```bash -/sixlabors.imagesharp.web.png?width=300&height=200&rmode=Pad&rcolor=32CD32FF&hmac=21f93e41021df0d3f88b5e2a8753bb273f292598e1511df67ec7cfb63f0b2994 -``` +## Related Topics -The @SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper type is unsealed so that you can inherit the type and support your own custom commands. \ No newline at end of file +- [Configuration and Pipeline](configuration.md) +- [Securing Requests](security.md) +- [Tag Helpers](taghelpers.md) +- [Extensibility](extensibility.md) diff --git a/articles/imagesharp.web/security.md b/articles/imagesharp.web/security.md new file mode 100644 index 000000000..dbe5d92d7 --- /dev/null +++ b/articles/imagesharp.web/security.md @@ -0,0 +1,112 @@ +# Securing Requests + +Once you let clients describe image transformations in the URL, you usually want some control over who can generate those URLs and which command shapes are allowed. ImageSharp.Web gives you two main tools for that: HMAC signing for request authorization and preset-only parsing for fixed command sets. + +## Require HMAC Tokens for Command Requests + +Set [`ImageSharpMiddlewareOptions.HMACSecretKey`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.HMACSecretKey) to enable request signing: + +```csharp +using SixLabors.ImageSharp.Web; + +builder.Services.AddImageSharp(options => +{ + options.HMACSecretKey = Convert.FromBase64String( + builder.Configuration["ImageSharp:HmacKey"]!); +}); +``` + +Once a non-empty secret key is configured, any request that still contains recognized commands after sanitization must also include a matching `hmac` query parameter. If the token is missing or invalid, the middleware returns HTTP 400. + +Use one stable secret across all app instances that must validate the same URLs. Rotating the secret invalidates previously generated signed URLs. + +## How the Default Token Is Computed + +By default, ImageSharp.Web computes HMAC-SHA256 over a lower-invariant relative URL built from: + +- the request path base; +- the request path; +- the sanitized command collection. + +That behavior is important because the middleware strips unknown commands before validation. The easiest way to stay in sync with the server is to let ImageSharp.Web compute the token for you instead of re-implementing the canonicalization rules yourself. + +>[!NOTE] +>`OnParseCommandsAsync` runs after the middleware computes the HMAC candidate value. If you mutate commands there, treat that callback as trusted server-side logic rather than part of the client-signable contract. + +## Generate Signed URLs on the Server + +[`RequestAuthorizationUtilities`](xref:SixLabors.ImageSharp.Web.RequestAuthorizationUtilities) is the simplest server-side API for generating a valid token: + +```csharp +using SixLabors.ImageSharp.Web; + +RequestAuthorizationUtilities auth = + app.Services.GetRequiredService(); + +string path = "/images/hero.jpg?width=400&format=webp"; +string token = auth.ComputeHMAC(path, CommandHandling.Sanitize)!; +string signedPath = $"{path}&{RequestAuthorizationUtilities.TokenCommand}={token}"; +``` + +Use [`CommandHandling.Sanitize`](xref:SixLabors.ImageSharp.Web.CommandHandling.Sanitize) unless you have a very specific reason to hash unsanitized commands. That keeps token generation aligned with the middleware's own validation path. + +## Customize the Hash Algorithm or Canonicalization + +If the default HMAC-SHA256 plus lower-invariant relative URL is not the contract you want, override [`OnComputeHMAC`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnComputeHMAC): + +```csharp +using Microsoft.AspNetCore.Http; +using SixLabors.ImageSharp.Web; + +builder.Services.AddImageSharp(options => +{ + options.OnComputeHMAC = (context, secret) => + { + string uri = CaseHandlingUriBuilder.BuildRelative( + CaseHandlingUriBuilder.CaseHandling.LowerInvariant, + context.Context.Request.PathBase, + context.Context.Request.Path, + QueryString.Create(context.Commands)); + + return HMACUtilities.ComputeHMACSHA512(uri, secret); + }; +}); +``` + +If you change the canonicalization rules or hash algorithm, every URL generator in your system must use the same logic. + +## Let Razor Tag Helpers Add the Token + +If you are rendering image URLs in Razor, the built-in tag helpers can generate the token automatically once `HMACSecretKey` is configured. See [Tag Helpers](taghelpers.md) for the Razor setup and examples. + +## Use Presets to Limit the Exposed Command Surface + +If you do not want clients to submit arbitrary commands at all, switch to [`PresetOnlyQueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.PresetOnlyQueryCollectionRequestParser): + +```csharp +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Commands; + +builder.Services.AddImageSharp() + .SetRequestParser() + .Configure(options => + { + options.Presets["avatar"] = "width=128&height=128&rmode=crop&format=webp"; + options.Presets["card"] = "width=640&height=360&rmode=crop"; + }); +``` + +That makes requests look like this: + +```text +/images/user.jpg?preset=avatar +``` + +Only the named preset is expanded into commands. Other free-form query-string keys are ignored by that parser. You can combine presets with HMAC signing if you want both a small command surface and signed URLs. + +## Related Topics + +- [Configuration and Pipeline](configuration.md) +- [Processing Commands](processingcommands.md) +- [Tag Helpers](taghelpers.md) +- [Troubleshooting](troubleshooting.md) diff --git a/articles/imagesharp.web/taghelpers.md b/articles/imagesharp.web/taghelpers.md new file mode 100644 index 000000000..a858a76ea --- /dev/null +++ b/articles/imagesharp.web/taghelpers.md @@ -0,0 +1,87 @@ +# Tag Helpers + +ImageSharp.Web includes Razor tag helpers so you can build image-processing URLs in strongly typed server-side markup instead of hand-concatenating query strings. The tag helpers also integrate with HMAC signing, which makes them the easiest way to generate safe image URLs in MVC and Razor Pages apps. + +## Enable the Tag Helpers + +Add the ImageSharp namespaces and tag helper registration to `_ViewImports.cshtml`: + +```html +@using SixLabors.ImageSharp +@using SixLabors.ImageSharp.Processing +@using SixLabors.ImageSharp.Web + +@addTagHelper *, SixLabors.ImageSharp.Web +``` + +That enables both [`ImageTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper) and [`HmacTokenTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.HmacTokenTagHelper). + +## Generate Command URLs with `ImageTagHelper` + +`ImageTagHelper` targets `` elements and converts `imagesharp-*` attributes into the corresponding query-string commands: + +```html +Hero image +``` + +That renders a `src` roughly like this: + +```text +images/hero.png?width=400&height=250&rmode=Crop&format=webp&quality=75 +``` + +The built-in typed values mirror the processor APIs: + +- Use [`ResizeMode`](xref:SixLabors.ImageSharp.Processing.ResizeMode) and [`AnchorPositionMode`](xref:SixLabors.ImageSharp.Processing.AnchorPositionMode) for resize modes and anchors. +- Use [`Color`](xref:SixLabors.ImageSharp.Color) for `imagesharp-rcolor` and `imagesharp-bgcolor`. +- Use [`Format`](xref:SixLabors.ImageSharp.Web.Format) for common output formats such as `Format.Jpg` and `Format.WebP`. +- Use [`Resampler`](xref:SixLabors.ImageSharp.Web.Resampler) for common resamplers such as `Resampler.Lanczos3` and `Resampler.NearestNeighbor`. + +The supported built-in attributes are: + +- `imagesharp-width`, `imagesharp-height`, `imagesharp-rmode`, `imagesharp-ranchor`, `imagesharp-rxy`, `imagesharp-rcolor`, `imagesharp-rsampler`, `imagesharp-compand`, and `imagesharp-orient` for resize behavior. +- `imagesharp-autoorient` for EXIF-based rotation and flipping. +- `imagesharp-format` for output format selection. +- `imagesharp-bgcolor` for flattening transparency. +- `imagesharp-quality` for JPEG and WebP quality. + +If the `` element does not already have literal `width` and `height` attributes, `ImageTagHelper` also writes them to the markup from the processing dimensions. That helps avoid layout shift for simple resize scenarios. + +## Local URLs Versus External URLs + +`ImageTagHelper` is intended for local application image URLs. It skips `http`, `ftp`, and `data` sources because ImageSharp.Web does not process those through the built-in local-path pipeline. + +## Automatic HMAC Generation + +[`HmacTokenTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.HmacTokenTagHelper) runs on `` and appends the `hmac` command automatically when [`ImageSharpMiddlewareOptions.HMACSecretKey`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.HMACSecretKey) is configured and the final `src` contains recognized commands. + +That means both of these patterns work: + +```html + + + +``` + +In the first case, `HmacTokenTagHelper` signs your handwritten command URL. In the second case, `ImageTagHelper` generates the command URL and `HmacTokenTagHelper` signs it afterward. + +## Extending the Tag Helper + +[`ImageTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper) is unsealed. If you add custom processors and want matching Razor syntax, inherit from it and override `AddProcessingCommands(...)` to append your own commands before the final `src` is emitted. + +## Related Topics + +- [Processing Commands](processingcommands.md) +- [Securing Requests](security.md) +- [Extensibility](extensibility.md) diff --git a/articles/imagesharp.web/troubleshooting.md b/articles/imagesharp.web/troubleshooting.md new file mode 100644 index 000000000..e5df8e3e4 --- /dev/null +++ b/articles/imagesharp.web/troubleshooting.md @@ -0,0 +1,83 @@ +# Troubleshooting + +Most ImageSharp.Web problems come down to one of five layers: middleware order, provider matching, command parsing, request signing, or cache configuration. This page groups the common failures that way so you can check the right layer first. + +## Query Strings Are Ignored + +If `/images/photo.jpg?width=400` behaves the same as `/images/photo.jpg`, check these first: + +- `app.UseImageSharp()` must run before `app.UseStaticFiles()`. +- The default [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) only processes requests that actually contain commands because it uses [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly). +- A provider may be matching the request before the provider you expected. Provider order matters. + +## I Get HTTP 400 After Enabling HMAC + +Once [`ImageSharpMiddlewareOptions.HMACSecretKey`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.HMACSecretKey) is configured, any request with recognized commands must include a valid `hmac` token. + +Useful checks: + +- Generate the token with [`RequestAuthorizationUtilities`](xref:SixLabors.ImageSharp.Web.RequestAuthorizationUtilities) instead of recreating the canonicalization logic by hand. +- Use [`CommandHandling.Sanitize`](xref:SixLabors.ImageSharp.Web.CommandHandling.Sanitize) when generating the token unless you intentionally need unsanitized commands. +- Make sure all app instances share the same secret key. +- Remember that unknown commands are stripped before validation, so signing a URL with extra application-specific keys will not match unless you remove them first or translate them in a custom request parser. + +## I Get a 404 or the Original Image Instead of a Processed One + +That usually means the source image was never resolved by the expected provider. + +Check these cases: + +- The file is outside the configured `ProviderRootPath`. +- The request path does not include the expected bucket or container prefix for the AWS or Azure providers. +- The source file extension is not recognized by the active ImageSharp format configuration. +- You switched to [`PresetOnlyQueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.PresetOnlyQueryCollectionRequestParser) and the request uses a missing or misspelled `preset` value. + +## Physical Cache or Provider Root Path Cannot Be Determined + +The physical provider and physical cache default to the web root when their root paths are `null`. If your app does not define a web root, configure both explicitly: + +- [`PhysicalFileSystemProviderOptions.ProviderRootPath`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProviderRootPath) +- [`PhysicalFileSystemCacheOptions.CacheRootPath`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCacheOptions.CacheRootPath) + +Relative paths are resolved against the application content root. + +## Tag Helpers Do Nothing + +If `imagesharp-*` attributes are not changing the rendered `src`, check these first: + +- `_ViewImports.cshtml` must contain `@addTagHelper *, SixLabors.ImageSharp.Web`. +- `ImageTagHelper` is for local application URLs and skips `http`, `ftp`, and `data` sources. +- Automatic HMAC generation only happens when `HMACSecretKey` is configured and the final URL contains recognized commands. + +## Parsed Values Differ Between Machines + +By default, ImageSharp.Web parses commands with invariant culture. If you set [`UseInvariantParsingCulture`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.UseInvariantParsingCulture) to `false`, separators and decimal parsing follow `CultureInfo.CurrentCulture`. + +That is useful for specialized local workflows, but it also means a value like `0.5` versus `0,5` can behave differently across environments. + +## Cached Output Does Not Refresh + +ImageSharp.Web keeps using a cached result until one of these changes: + +- the source last-write time changes; +- the cache entry ages beyond [`CacheMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheMaxAge); +- the cached entry disappears and the middleware has to regenerate it. + +If you are replacing source files in place, make sure the backing store actually updates the source last-write metadata the provider sees. + +## A Good Debugging Order + +When an ImageSharp.Web request misbehaves, this order is usually productive: + +1. Check middleware order. +2. Confirm which provider should own the request. +3. Confirm the parsed command set or preset name. +4. Check HMAC generation if signing is enabled. +5. Check cache roots, cache lifetime, and source last-write metadata. + +## Related Topics + +- [Getting Started](gettingstarted.md) +- [Configuration and Pipeline](configuration.md) +- [Securing Requests](security.md) +- [Extensibility](extensibility.md) diff --git a/articles/imagesharp/animatedgif.md b/articles/imagesharp/animatedgif.md deleted file mode 100644 index 09ecba327..000000000 --- a/articles/imagesharp/animatedgif.md +++ /dev/null @@ -1,92 +0,0 @@ -# Create an Animated GIF - -Creating an animated GIF in ImageSharp is really about building a multi-frame image on purpose. Once that mental model is in place, the rest of the API starts to feel straightforward: create frames, set per-frame metadata, configure the animation metadata, then save with the encoder you want. - -ImageSharp builds animated GIFs by creating a multi-frame [`Image`](xref:SixLabors.ImageSharp.Image`1), configuring GIF metadata, and then saving with [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) when you need encoder-specific control. When you start from [`Color`](xref:SixLabors.ImageSharp.Color) values, convert them to the target pixel type with `ToPixel()` before passing them to generic image constructors. - -For format background and palette tradeoffs, see [GIF and Animation](gif.md). This page focuses on the actual authoring workflow. - -## Build a Multi-Frame GIF - -The root frame is the first animation frame. Additional frames are appended through [`ImageFrameCollection.AddFrame()`](xref:SixLabors.ImageSharp.ImageFrameCollection.AddFrame(SixLabors.ImageSharp.ImageFrame)), which clones the source frame and requires it to match the image dimensions. - -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.PixelFormats; - -Color[] colors = -{ - Color.Orange, - Color.DeepSkyBlue, - Color.MediumSeaGreen -}; - -using Image gif = new(120, 120, colors[0].ToPixel()); - -GifMetadata gifMetadata = gif.Metadata.GetGifMetadata(); -gifMetadata.RepeatCount = 0; - -GifFrameMetadata rootFrameMetadata = gif.Frames.RootFrame.Metadata.GetGifMetadata(); -rootFrameMetadata.FrameDelay = 10; -rootFrameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground; - -for (int i = 1; i < colors.Length; i++) -{ - using Image frameImage = new(120, 120, colors[i].ToPixel()); - - GifFrameMetadata frameMetadata = frameImage.Frames.RootFrame.Metadata.GetGifMetadata(); - frameMetadata.FrameDelay = 10; - frameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground; - - gif.Frames.AddFrame(frameImage.Frames.RootFrame); -} - -gif.SaveAsGif("output.gif", new GifEncoder -{ - ColorTableMode = FrameColorTableMode.Global -}); -``` - -## Control Looping and Frame Timing - -[`GifMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata) stores image-level animation settings: - -- [`RepeatCount`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata.RepeatCount) controls looping. `0` means loop indefinitely. -- [`ColorTableMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata.ColorTableMode) describes whether the animation uses a global or local palette layout. - -[`GifFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata) stores per-frame settings: - -- [`FrameDelay`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.FrameDelay) is measured in hundredths of a second. -- [`DisposalMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.DisposalMode) controls how the previous frame is treated before the next frame is drawn. -- [`ColorTableMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.ColorTableMode) can override palette behavior for an individual frame. - -The most important disposal modes are: - -- [`DoNotDispose`](xref:SixLabors.ImageSharp.Formats.FrameDisposalMode.DoNotDispose) when later frames should draw over earlier content. -- [`RestoreToBackground`](xref:SixLabors.ImageSharp.Formats.FrameDisposalMode.RestoreToBackground) when a frame should be cleared before the next frame is shown. -- [`RestoreToPrevious`](xref:SixLabors.ImageSharp.Formats.FrameDisposalMode.RestoreToPrevious) when the previous composited state should be restored. - -## Palette Choice Still Matters - -GIF is always palette-based, so saving an animation is always a quantization step. [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder) inherits the quantization controls exposed by [`QuantizingImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder): - -- [`Quantizer`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.Quantizer) -- [`PixelSamplingStrategy`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.PixelSamplingStrategy) -- [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder.TransparentColorMode) - -In practice: - -- Use [`FrameColorTableMode.Global`](xref:SixLabors.ImageSharp.Formats.FrameColorTableMode.Global) when you want one shared palette across the animation, often for smaller files and more consistent colors across frames. -- Use [`FrameColorTableMode.Local`](xref:SixLabors.ImageSharp.Formats.FrameColorTableMode.Local) when frames differ enough that per-frame palettes produce better results. -- Choose an explicit quantizer when gradients, UI art, or brand colors matter. - -See [Quantization, Palettes, and Dithering](quantization.md) for the full quantization story. - -## Practical Guidance - -- Keep frame dimensions consistent with the GIF canvas size before adding them. -- Set `FrameDelay` and `DisposalMode` on every frame you care about rather than relying on defaults. -- Prefer a global palette for simple flat-color or UI-style animations. -- Consider [WebP](webp.md) instead of GIF when you need better compression or more modern animation behavior. diff --git a/articles/imagesharp/animations.md b/articles/imagesharp/animations.md new file mode 100644 index 000000000..566583ea2 --- /dev/null +++ b/articles/imagesharp/animations.md @@ -0,0 +1,145 @@ +# Working with Animations + +ImageSharp treats animation as a multi-frame [`Image`](xref:SixLabors.ImageSharp.Image`1). The authoring model is the same whether you save as GIF, animated PNG (APNG), or animated WebP: build the frame collection, set image-level animation metadata, set per-frame metadata, then save with the encoder for the format you want. + +You still work with full-size frames in memory, but ImageSharp's animated encoders optimize the output by de-duplicating unchanged pixels between frames and writing only the differing region for later frames where the format supports it. + +For format-specific background, palette, and compression tradeoffs, see [GIF](gif.md), [PNG](png.md), and [WebP](webp.md). This page focuses on the shared multi-frame workflow. + +## Build a Multi-Frame Animation + +The root frame is the first animation frame. Additional frames are appended through [`ImageFrameCollection.AddFrame()`](xref:SixLabors.ImageSharp.ImageFrameCollection.AddFrame(SixLabors.ImageSharp.ImageFrame)), which clones the source frame. In ImageSharp, animation frames must always match the image dimensions. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +Color[] colors = +{ + Color.Orange, + Color.DeepSkyBlue, + Color.MediumSeaGreen +}; + +using Image animation = new(120, 120, colors[0].ToPixel()); + +for (int i = 1; i < colors.Length; i++) +{ + using Image frameImage = new(120, 120, colors[i].ToPixel()); + animation.Frames.AddFrame(frameImage.Frames.RootFrame); +} +``` + +When you start from [`Color`](xref:SixLabors.ImageSharp.Color) values, convert them to the target pixel type with `ToPixel()` before passing them to generic image constructors. + +## ImageSharp Optimizes Later Frames + +When encoding GIF, APNG, or animated WebP, ImageSharp compares later frames with the previous composited result and trims unchanged pixels from the encoded output. In practice, that means you usually author full-canvas frames, but the encoder writes only the changed bounds for later frames when that produces an equivalent animation. + +This is especially helpful for sprite, UI, and cursor-style animations where only a small region changes from one frame to the next. + +## Configure GIF Metadata + +Use [`GifMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata) and [`GifFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata) when you are saving palette-based animation: + +- [`RepeatCount`](xref:SixLabors.ImageSharp.Formats.Gif.GifMetadata.RepeatCount) controls looping. `0` means loop indefinitely. +- [`FrameDelay`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.FrameDelay) is measured in hundredths of a second. +- [`DisposalMode`](xref:SixLabors.ImageSharp.Formats.Gif.GifFrameMetadata.DisposalMode) controls how the previous composited frame is treated before the next frame is shown. + +Starting from an existing `Image animation`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.PixelFormats; + +GifMetadata gifMetadata = animation.Metadata.GetGifMetadata(); +gifMetadata.RepeatCount = 0; + +foreach (ImageFrame frame in animation.Frames) +{ + GifFrameMetadata frameMetadata = frame.Metadata.GetGifMetadata(); + frameMetadata.FrameDelay = 10; + frameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground; +} + +animation.Save("output.gif", new GifEncoder +{ + ColorTableMode = FrameColorTableMode.Global +}); +``` + +GIF is always palette-based, so palette selection still matters. See [GIF](gif.md) and [Quantization, Palettes, and Dithering](quantization.md) for the full quantization story. + +## Configure APNG Metadata + +Use [`PngMetadata`](xref:SixLabors.ImageSharp.Formats.Png.PngMetadata) and [`PngFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Png.PngFrameMetadata) when you want animated PNG output: + +- [`RepeatCount`](xref:SixLabors.ImageSharp.Formats.Png.PngMetadata.RepeatCount) controls looping. +- [`AnimateRootFrame`](xref:SixLabors.ImageSharp.Formats.Png.PngMetadata.AnimateRootFrame) controls whether the root frame participates in the animation. +- [`FrameDelay`](xref:SixLabors.ImageSharp.Formats.Png.PngFrameMetadata.FrameDelay) is stored as a `Rational`. +- [`DisposalMode`](xref:SixLabors.ImageSharp.Formats.Png.PngFrameMetadata.DisposalMode) and [`BlendMode`](xref:SixLabors.ImageSharp.Formats.Png.PngFrameMetadata.BlendMode) control how frames compose. + +Continuing from the same `Image animation`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; + +PngMetadata pngMetadata = animation.Metadata.GetPngMetadata(); +pngMetadata.RepeatCount = 0; +pngMetadata.AnimateRootFrame = true; + +foreach (ImageFrame frame in animation.Frames) +{ + PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata(); + frameMetadata.FrameDelay = new Rational(1, 10); + frameMetadata.DisposalMode = FrameDisposalMode.DoNotDispose; + frameMetadata.BlendMode = FrameBlendMode.Over; +} + +animation.Save("output.png", new PngEncoder()); +``` + +## Configure Animated WebP Metadata + +Use [`WebpMetadata`](xref:SixLabors.ImageSharp.Formats.Webp.WebpMetadata) and [`WebpFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Webp.WebpFrameMetadata) when you want animated WebP output: + +- [`RepeatCount`](xref:SixLabors.ImageSharp.Formats.Webp.WebpMetadata.RepeatCount) controls looping. +- [`BackgroundColor`](xref:SixLabors.ImageSharp.Formats.Webp.WebpMetadata.BackgroundColor) is used by the format and matters when a frame uses `RestoreToBackground`. +- [`FrameDelay`](xref:SixLabors.ImageSharp.Formats.Webp.WebpFrameMetadata.FrameDelay) is measured in milliseconds. +- [`DisposalMode`](xref:SixLabors.ImageSharp.Formats.Webp.WebpFrameMetadata.DisposalMode) and [`BlendMode`](xref:SixLabors.ImageSharp.Formats.Webp.WebpFrameMetadata.BlendMode) control how frames compose. + +Continuing from the same `Image animation`: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.PixelFormats; + +WebpMetadata webpMetadata = animation.Metadata.GetWebpMetadata(); +webpMetadata.RepeatCount = 0; +webpMetadata.BackgroundColor = Color.Transparent; + +foreach (ImageFrame frame in animation.Frames) +{ + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + frameMetadata.FrameDelay = 100; + frameMetadata.DisposalMode = FrameDisposalMode.DoNotDispose; + frameMetadata.BlendMode = FrameBlendMode.Over; +} + +animation.Save("output.webp", new WebpEncoder()); +``` + +## Practical Guidance + +- ImageSharp animation frames must always be the same size as the animation canvas. +- Set timing and disposal or blend metadata on every frame you care about rather than relying on defaults. +- Choose GIF when broad legacy compatibility matters more than palette and compression tradeoffs. +- Choose APNG when you want PNG-style lossless color and alpha with explicit frame blending and disposal. +- Choose WebP when you want a modern animated format with flexible compression behavior. diff --git a/articles/imagesharp/gif.md b/articles/imagesharp/gif.md index 6330a39da..248b8e591 100644 --- a/articles/imagesharp/gif.md +++ b/articles/imagesharp/gif.md @@ -1,4 +1,4 @@ -# GIF and Animation +# GIF GIF is one of the oldest formats ImageSharp supports, and it comes with tradeoffs that matter more than many newcomers expect. It is still useful for simple animation and very broad compatibility, but because it is palette based, color reduction and frame metadata are part of the story from the start. @@ -131,4 +131,4 @@ GIF is usually a poor fit when: - You want efficient compression. - You need modern transparency behavior. -For a step-by-step recipe, see [Create an animated GIF](animatedgif.md). For a more modern animated format, see [WebP](webp.md). +For a step-by-step multi-frame workflow, see [Working with Animations](animations.md). For a more modern animated format, see [WebP](webp.md). diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index d05a36bd6..9ecec4c07 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -148,7 +148,7 @@ Use the format-specific guides for the common cases: - [JPEG](jpeg.md) for photographic output and quality-focused lossy compression. - [PNG](png.md) for lossless output, transparency, and APNG metadata. -- [GIF and Animation](gif.md) for palette-based animation workflows. +- [GIF](gif.md) for palette-based animation workflows. - [WebP](webp.md) for lossy, lossless, transparent, and animated WebP output. - [TIFF](tiff.md) for workflows where compression mode, pixel layout, and TIFF metadata matter. diff --git a/articles/imagesharp/processing.md b/articles/imagesharp/processing.md index 1113cffc8..0548479fb 100644 --- a/articles/imagesharp/processing.md +++ b/articles/imagesharp/processing.md @@ -75,7 +75,7 @@ As a rule of thumb: - [Rotate, Flip, and Auto-Orient](orientation.md) covers `AutoOrient()`, `Rotate()`, `Flip()`, and `RotateFlip()`. - [Color and Effects](colorandeffects.md) covers `Grayscale()`, `Sepia()`, `Brightness()`, `Contrast()`, `Hue()`, `Saturate()`, and `Opacity()`. - [Quantization, Palettes, and Dithering](quantization.md) covers `Quantize()`, palette selection, encoder quantizers, and dithering algorithms. -- [Create an animated GIF](animatedgif.md) covers a multi-frame workflow. +- [Working with Animations](animations.md) covers multi-frame workflows for GIF, APNG, and WebP. ## Related APIs diff --git a/articles/imagesharp/quantization.md b/articles/imagesharp/quantization.md index beba8e4b6..1eb28b07b 100644 --- a/articles/imagesharp/quantization.md +++ b/articles/imagesharp/quantization.md @@ -121,7 +121,7 @@ Transparency handling matters most for GIF, palette PNG, ICO, and CUR output. [` ## Related Topics -- [GIF and Animation](gif.md) +- [GIF](gif.md) - [PNG](png.md) - [Convert Between Formats](formatconversion.md) - [Read Image Info Without Decoding](identify.md) diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index 6a8ca1a28..a9b22ee6f 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -105,7 +105,7 @@ Use your hosting layer to enforce: - reverse proxy limits; and - service or container isolation for expensive workloads. -For ImageSharp.Web command signing, see [Securing Processing Commands in ImageSharp.Web](../imagesharp.web/processingcommands.md#securing-processing-commands). +For ImageSharp.Web command signing, see [Securing Requests in ImageSharp.Web](../imagesharp.web/security.md). ## Practical Security Defaults diff --git a/articles/toc.md b/articles/toc.md index 3c782bdd1..999eae8f1 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -7,7 +7,7 @@ ## [Image Formats](imagesharp/imageformats.md) ### [JPEG](imagesharp/jpeg.md) ### [PNG](imagesharp/png.md) -### [GIF and Animation](imagesharp/gif.md) +### [GIF](imagesharp/gif.md) ### [WebP](imagesharp/webp.md) ### [TIFF](imagesharp/tiff.md) ## [Processing Images](imagesharp/processing.md) @@ -16,7 +16,7 @@ ### [Rotate, Flip, and Auto-Orient](imagesharp/orientation.md) ### [Color and Effects](imagesharp/colorandeffects.md) ### [Quantization, Palettes, and Dithering](imagesharp/quantization.md) -### [Create an animated GIF](imagesharp/animatedgif.md) +### [Working with Animations](imagesharp/animations.md) ## [Working with Pixel Buffers](imagesharp/pixelbuffers.md) ## [Interop and Raw Memory](imagesharp/interop.md) ## [Configuration](imagesharp/configuration.md) @@ -35,9 +35,14 @@ # [ImageSharp.Web](imagesharp.web/index.md) ## [Getting Started](imagesharp.web/gettingstarted.md) -### [Processing Commands](imagesharp.web/processingcommands.md) -### [Image Providers](imagesharp.web/imageproviders.md) -### [Image Caches](imagesharp.web/imagecaches.md) +## [Configuration and Pipeline](imagesharp.web/configuration.md) +## [Processing Commands](imagesharp.web/processingcommands.md) +## [Image Providers](imagesharp.web/imageproviders.md) +## [Image Caches](imagesharp.web/imagecaches.md) +## [Securing Requests](imagesharp.web/security.md) +## [Tag Helpers](imagesharp.web/taghelpers.md) +## [Extensibility](imagesharp.web/extensibility.md) +## [Troubleshooting](imagesharp.web/troubleshooting.md) # [Fonts](fonts/index.md) ## [Loading Fonts and Collections](fonts/gettingstarted.md) From 9d67749376cc51d4b5ace2a816874fe4147f5277 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Apr 2026 21:39:43 +1000 Subject: [PATCH 09/21] Add new web encoder and ICC profile handling default info --- articles/imagesharp.web/configuration.md | 58 +++++++++++++++++-- articles/imagesharp.web/gettingstarted.md | 7 +++ articles/imagesharp.web/index.md | 2 +- articles/imagesharp.web/processingcommands.md | 4 ++ articles/imagesharp.web/troubleshooting.md | 11 ++++ ext/ImageSharp.Web | 2 +- templates/modern/public/main.css | 2 +- 7 files changed, 79 insertions(+), 7 deletions(-) diff --git a/articles/imagesharp.web/configuration.md b/articles/imagesharp.web/configuration.md index 6274fa213..9399d940d 100644 --- a/articles/imagesharp.web/configuration.md +++ b/articles/imagesharp.web/configuration.md @@ -20,19 +20,18 @@ That gives you a fully working middleware out of the box, but every one of those [`ImageSharpMiddlewareOptions`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions) controls the shared middleware behavior: - [`Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) is the underlying ImageSharp [`Configuration`](xref:SixLabors.ImageSharp.Configuration). +- By default, that `Configuration` is not raw `Configuration.Default`; ImageSharp.Web installs web-oriented JPEG, PNG, and WebP encoders into it. - [`MemoryStreamManager`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.MemoryStreamManager) controls pooled response streams. - [`UseInvariantParsingCulture`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.UseInvariantParsingCulture) controls whether command parsing is culture-invariant. - [`BrowserMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.BrowserMaxAge), [`CacheMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheMaxAge), and [`CacheHashLength`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheHashLength) control cache behavior. ```csharp -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Providers; builder.Services.AddImageSharp(options => { - options.Configuration = Configuration.Default.Clone(); options.UseInvariantParsingCulture = true; options.BrowserMaxAge = TimeSpan.FromDays(7); options.CacheMaxAge = TimeSpan.FromDays(30); @@ -51,7 +50,58 @@ builder.Services.AddImageSharp(options => }); ``` -Use a cloned ImageSharp configuration when you need a different format set, allocator behavior, or other base ImageSharp customization for the middleware. +If you do not need to change format registrations or encoder defaults, leave [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) alone. Replacing it opts you out of the middleware's built-in web defaults. + +## Default Encoder and ICC Behavior + +The default middleware [`Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) is a cloned ImageSharp configuration with web-oriented encoder registrations: + +- [`JpegEncoder`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder) uses `Quality = 75`, `Progressive = true`, `Interleaved = true`, and `ColorType = YCbCrRatio420`. +- [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder) uses `CompressionLevel = BestCompression` and `FilterMethod = Adaptive`. +- [`WebpEncoder`](xref:SixLabors.ImageSharp.Formats.Webp.WebpEncoder) uses `Quality = 75` and `Method = BestQuality`. + +Those registrations are used whenever the middleware saves processed output in JPEG, PNG, or WebP format, whether the format came from the source image or from the `format` command. + +If [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync) returns `null`, the middleware also creates fallback [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) for you. The ICC-profile behavior depends on whether you kept the default configuration: + +- With the default middleware configuration, [`DecoderOptions.ColorProfileHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.ColorProfileHandling) is [`ColorProfileHandling.Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert). +- If you replace `options.Configuration`, the fallback changes to [`ColorProfileHandling.Compact`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Compact). + +`Compact` only removes canonical sRGB ICC profile data. It does not convert non-sRGB source images. That distinction matters most when you transcode or resize JPEGs that arrive with CMYK or other non-sRGB profiles. + +## Customize Encoders Without Losing ICC Conversion + +If you want your own encoder registrations but still want the middleware to decode with [`ColorProfileHandling.Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert), clone the current configuration, replace the encoders you care about, then return explicit decoder options: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Web; + +builder.Services.AddImageSharp(options => +{ + Configuration configuration = options.Configuration.Clone(); + + configuration.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder + { + Quality = 82, + Progressive = true, + Interleaved = true, + ColorType = JpegColorType.YCbCrRatio420 + }); + + options.Configuration = configuration; + + options.OnBeforeLoadAsync = (_, _) => Task.FromResult(new() + { + Configuration = configuration, + ColorProfileHandling = ColorProfileHandling.Convert + }); +}); +``` + +Use this pattern when you want to keep ImageSharp.Web's ICC-conversion behavior but need different encoder quality, chroma subsampling, format registrations, allocator settings, or other base ImageSharp customization. ## Change Individual Pipeline Pieces @@ -99,7 +149,7 @@ That turns requests like `/images/photo.jpg?preset=thumb` into a controlled, nam [`ImageSharpMiddlewareOptions`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions) also exposes targeted callbacks for app-specific customization: - [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) runs after a provider has matched the request and after the command set has been sanitized, but before the source image is resolved. -- [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync) can return custom [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) before the source image is decoded. +- [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync) can return custom [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) before the source image is decoded. If it returns `null`, the middleware supplies defaults based on the current `Configuration`. - [`OnBeforeSaveAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeSaveAsync) can adjust the [`FormattedImage`](xref:SixLabors.ImageSharp.Web.FormattedImage) after processing but before encoding. - [`OnProcessedAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnProcessedAsync) receives an [`ImageProcessingContext`](xref:SixLabors.ImageSharp.Web.Middleware.ImageProcessingContext) after encoding but before the result is cached. - [`OnPrepareResponseAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnPrepareResponseAsync) runs after status code and headers are set but before the body is written. diff --git a/articles/imagesharp.web/gettingstarted.md b/articles/imagesharp.web/gettingstarted.md index fa28c2466..7c5afc7ac 100644 --- a/articles/imagesharp.web/gettingstarted.md +++ b/articles/imagesharp.web/gettingstarted.md @@ -30,6 +30,7 @@ app.Run(); - [`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) stores processed output under `wwwroot/is-cache` by default. - [`UriRelativeLowerInvariantCacheKey`](xref:SixLabors.ImageSharp.Web.Caching.UriRelativeLowerInvariantCacheKey) and [`SHA256CacheHash`](xref:SixLabors.ImageSharp.Web.Caching.SHA256CacheHash) create hashed cache filenames. - [`ResizeWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.ResizeWebProcessor), [`FormatWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.FormatWebProcessor), [`BackgroundColorWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.BackgroundColorWebProcessor), [`QualityWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.QualityWebProcessor), and [`AutoOrientWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.AutoOrientWebProcessor) provide the built-in command set. +- A middleware-specific ImageSharp [`Configuration`](xref:SixLabors.ImageSharp.Configuration) with web-oriented [`JpegEncoder`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder), [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder), and [`WebpEncoder`](xref:SixLabors.ImageSharp.Formats.Webp.WebpEncoder) defaults. With that setup in place, requests like these are processed automatically: @@ -39,6 +40,12 @@ With that setup in place, requests like these are processed automatically: /images/logo.png?bgcolor=white&format=jpg&quality=85 ``` +That default configuration is intentionally opinionated for web output. Processed JPEGs use quality `75` with progressive, interleaved `YCbCrRatio420` encoding, processed PNGs use `BestCompression` with adaptive filtering, and processed WebP output uses quality `75` with `BestQuality` encoding method. + +When you keep the default middleware configuration and do not return custom [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) from [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync), the middleware also decodes with [`ColorProfileHandling.Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert). That normalizes embedded ICC profiles for web-oriented re-encoding instead of blindly carrying source color encodings through the pipeline. + +If you later replace [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration), you also replace those encoder defaults. See [Configuration and Pipeline](configuration.md) for the details and the explicit ICC-profile override pattern. + ## A Useful Default Mental Model With the default [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider), plain file requests still fall through to static files because it uses [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly). That means: diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 5da3c286f..2977a2295 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -47,7 +47,7 @@ paket add SixLabors.ImageSharp.Web --version VERSION_NUMBER ## Start Here - [Getting Started](gettingstarted.md) covers the minimal ASP.NET Core setup and the default provider and cache behavior. -- [Configuration and Pipeline](configuration.md) explains what `AddImageSharp()` registers and how to replace or reorder the moving parts. +- [Configuration and Pipeline](configuration.md) explains what `AddImageSharp()` registers, the middleware's web-focused encoder defaults, ICC profile handling, and how to replace or reorder the moving parts. - [Processing Commands](processingcommands.md) documents the built-in resize, auto-orient, format, quality, and background-color commands. - [Image Providers](imageproviders.md) covers filesystem, Azure Blob Storage, and AWS S3 source images. - [Image Caches](imagecaches.md) covers the default physical cache, cloud cache backends, cache keys, and cache lifetime. diff --git a/articles/imagesharp.web/processingcommands.md b/articles/imagesharp.web/processingcommands.md index e1855a0af..16cada194 100644 --- a/articles/imagesharp.web/processingcommands.md +++ b/articles/imagesharp.web/processingcommands.md @@ -57,6 +57,8 @@ Use `autoorient=true` when you want the output pixels themselves to be normalize Any file extension registered with the active [`ImageFormatsManager`](xref:SixLabors.ImageSharp.Configuration.ImageFormatsManager) can be used here. The exact set therefore depends on the underlying ImageSharp configuration. +The selected format uses the encoder currently registered in [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration). With the default middleware configuration, that means `format=jpg`, `format=png`, and `format=webp` all use web-oriented encoder settings rather than the raw ImageSharp library defaults. + ## Quality [`QualityWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.QualityWebProcessor) controls encoder quality for JPEG and WebP output. @@ -69,6 +71,8 @@ Any file extension registered with the active [`ImageFormatsManager`](xref:SixLa Quality values are clamped by the target encoder. For WebP, values below `100` switch the encoder to lossy mode. +When no `quality` command is supplied, the default middleware configuration still encodes JPEG and WebP at quality `75`. If you replace [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration), you also change those no-query defaults. + ## Background Color [`BackgroundColorWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.BackgroundColorWebProcessor) fills transparent areas with a color. diff --git a/articles/imagesharp.web/troubleshooting.md b/articles/imagesharp.web/troubleshooting.md index e5df8e3e4..78de6d5d3 100644 --- a/articles/imagesharp.web/troubleshooting.md +++ b/articles/imagesharp.web/troubleshooting.md @@ -55,6 +55,17 @@ By default, ImageSharp.Web parses commands with invariant culture. If you set [` That is useful for specialized local workflows, but it also means a value like `0.5` versus `0,5` can behave differently across environments. +## Colors or Compression Changed After I Replaced `options.Configuration` + +Check whether you replaced [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) with `Configuration.Default.Clone()` or another custom configuration. + +That changes two things at once: + +- You replace the middleware's built-in JPEG, PNG, and WebP encoder registrations. +- If [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync) still returns `null`, decode falls back to [`ColorProfileHandling.Compact`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Compact) instead of [`ColorProfileHandling.Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert). + +If you want custom encoders and the original ICC-conversion behavior, clone the current middleware configuration, assign it back, and return explicit [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) from `OnBeforeLoadAsync`. + ## Cached Output Does Not Refresh ImageSharp.Web keeps using a cached result until one of these changes: diff --git a/ext/ImageSharp.Web b/ext/ImageSharp.Web index 037fa9a95..338c719cf 160000 --- a/ext/ImageSharp.Web +++ b/ext/ImageSharp.Web @@ -1 +1 @@ -Subproject commit 037fa9a95803ca4460c8935d1ca5bc02a08bfc60 +Subproject commit 338c719cf7a31ac471029d29d87df853a8b68d96 diff --git a/templates/modern/public/main.css b/templates/modern/public/main.css index 7057a12f8..972c5e4f4 100644 --- a/templates/modern/public/main.css +++ b/templates/modern/public/main.css @@ -137,7 +137,6 @@ header .navbar { flex-direction: column; min-width: 0; word-wrap: break-word; - background-color: #fff; background-clip: border-box; text-align: center; height: 100%; @@ -157,6 +156,7 @@ header .navbar { .product img { max-height: 150px; + margin-bottom: .5rem; } .product h5 { From 17e6efbbaa73008ac3be5cd492cbe5d19e76bb36 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Apr 2026 23:12:46 +1000 Subject: [PATCH 10/21] Document new auto-orient behavior in web --- articles/imagesharp.web/configuration.md | 29 +++++++++++++++---- articles/imagesharp.web/gettingstarted.md | 17 +++++++---- articles/imagesharp.web/imageproviders.md | 4 ++- articles/imagesharp.web/index.md | 4 +-- articles/imagesharp.web/processingcommands.md | 6 +++- articles/imagesharp.web/security.md | 5 +--- articles/imagesharp.web/taghelpers.md | 4 ++- articles/imagesharp.web/troubleshooting.md | 23 +++++++++++++-- ext/ImageSharp.Web | 2 +- 9 files changed, 70 insertions(+), 24 deletions(-) diff --git a/articles/imagesharp.web/configuration.md b/articles/imagesharp.web/configuration.md index 9399d940d..eadf96343 100644 --- a/articles/imagesharp.web/configuration.md +++ b/articles/imagesharp.web/configuration.md @@ -21,6 +21,7 @@ That gives you a fully working middleware out of the box, but every one of those - [`Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) is the underlying ImageSharp [`Configuration`](xref:SixLabors.ImageSharp.Configuration). - By default, that `Configuration` is not raw `Configuration.Default`; ImageSharp.Web installs web-oriented JPEG, PNG, and WebP encoders into it. +- [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) defaults to a callback that injects `autoorient=true` when the request does not already contain `autoorient`. - [`MemoryStreamManager`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.MemoryStreamManager) controls pooled response streams. - [`UseInvariantParsingCulture`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.UseInvariantParsingCulture) controls whether command parsing is culture-invariant. - [`BrowserMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.BrowserMaxAge), [`CacheMaxAge`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheMaxAge), and [`CacheHashLength`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheHashLength) control cache behavior. @@ -52,6 +53,21 @@ builder.Services.AddImageSharp(options => If you do not need to change format registrations or encoder defaults, leave [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) alone. Replacing it opts you out of the middleware's built-in web defaults. +Likewise, if you do not need custom command augmentation, leave [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) alone. Replacing it opts you out of the default EXIF-normalization behavior unless you chain the existing callback yourself. + +## Default Orientation Behavior + +The default [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) callback inserts `autoorient=true` when the request does not already specify `autoorient`. + +That makes EXIF normalization part of the default middleware behavior rather than an opt-in query-string feature. The main reason is web delivery: some browsers still ignore EXIF orientation in formats such as WebP, so relying on the encoded metadata alone does not produce consistent display results. + +Two details matter in practice: + +- `autoorient=false` still disables the behavior for that request because the middleware only inserts the command when it is absent. +- Replacing `OnParseCommandsAsync` with your own delegate removes the built-in insertion unless you invoke the previous delegate. + +With the out-of-the-box local filesystem setup, that also means commandless image URLs are usually processed and cached instead of falling through to static files unchanged. + ## Default Encoder and ICC Behavior The default middleware [`Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) is a cloned ImageSharp configuration with web-oriented encoder registrations: @@ -156,17 +172,21 @@ That turns requests like `/images/photo.jpg?preset=thumb` into a controlled, nam ```csharp using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Middleware; builder.Services.AddImageSharp(options => { - options.OnParseCommandsAsync = context => + Func defaultParse = options.OnParseCommandsAsync; + + options.OnParseCommandsAsync = async context => { + await defaultParse(context); + if (!context.Commands.Contains("format")) { context.Commands["format"] = "webp"; } - return Task.CompletedTask; }; options.OnPrepareResponseAsync = context => @@ -177,10 +197,7 @@ builder.Services.AddImageSharp(options => }); ``` -These callbacks are often the right tool when you need small workflow adjustments without inventing a custom provider, parser, or processor. - ->[!NOTE] ->`OnParseCommandsAsync` runs after HMAC generation. If you sign requests, keep any command mutations in that callback deterministic and within your own trust boundary. +These callbacks are often the right tool when you need small workflow adjustments without inventing a custom provider, parser, or processor. If you override `OnParseCommandsAsync`, preserve the existing delegate unless you intentionally want to remove the middleware's default `autoorient=true` insertion. ## Related Topics diff --git a/articles/imagesharp.web/gettingstarted.md b/articles/imagesharp.web/gettingstarted.md index 7c5afc7ac..d1835f224 100644 --- a/articles/imagesharp.web/gettingstarted.md +++ b/articles/imagesharp.web/gettingstarted.md @@ -19,7 +19,7 @@ app.UseStaticFiles(); app.Run(); ``` -`app.UseImageSharp()` must appear before `app.UseStaticFiles()`. If static files run first, requests such as `/images/photo.jpg?width=400` will be served directly from disk and ImageSharp.Web will never see them. +`app.UseImageSharp()` must appear before `app.UseStaticFiles()`. If static files run first, requests such as `/images/photo.jpg` or `/images/photo.jpg?width=400` will be served directly from disk and ImageSharp.Web will never see them. ## What the Default Registration Includes @@ -30,6 +30,7 @@ app.Run(); - [`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) stores processed output under `wwwroot/is-cache` by default. - [`UriRelativeLowerInvariantCacheKey`](xref:SixLabors.ImageSharp.Web.Caching.UriRelativeLowerInvariantCacheKey) and [`SHA256CacheHash`](xref:SixLabors.ImageSharp.Web.Caching.SHA256CacheHash) create hashed cache filenames. - [`ResizeWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.ResizeWebProcessor), [`FormatWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.FormatWebProcessor), [`BackgroundColorWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.BackgroundColorWebProcessor), [`QualityWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.QualityWebProcessor), and [`AutoOrientWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.AutoOrientWebProcessor) provide the built-in command set. +- A default [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) callback that inserts `autoorient=true` when the request does not already specify `autoorient`. - A middleware-specific ImageSharp [`Configuration`](xref:SixLabors.ImageSharp.Configuration) with web-oriented [`JpegEncoder`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder), [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder), and [`WebpEncoder`](xref:SixLabors.ImageSharp.Formats.Webp.WebpEncoder) defaults. With that setup in place, requests like these are processed automatically: @@ -42,18 +43,22 @@ With that setup in place, requests like these are processed automatically: That default configuration is intentionally opinionated for web output. Processed JPEGs use quality `75` with progressive, interleaved `YCbCrRatio420` encoding, processed PNGs use `BestCompression` with adaptive filtering, and processed WebP output uses quality `75` with `BestQuality` encoding method. +The default command path is opinionated too: ImageSharp.Web transparently adds `autoorient=true` unless the request already contains an `autoorient` value. That means processed output is EXIF-normalized by default, which is especially important for WebP delivery where browser orientation support is inconsistent. + When you keep the default middleware configuration and do not return custom [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) from [`OnBeforeLoadAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnBeforeLoadAsync), the middleware also decodes with [`ColorProfileHandling.Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert). That normalizes embedded ICC profiles for web-oriented re-encoding instead of blindly carrying source color encodings through the pipeline. -If you later replace [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration), you also replace those encoder defaults. See [Configuration and Pipeline](configuration.md) for the details and the explicit ICC-profile override pattern. +If you later replace [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration), you also replace those encoder defaults. If you replace [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync), you replace the default auto-orientation injection unless you explicitly preserve it. See [Configuration and Pipeline](configuration.md) for both patterns. ## A Useful Default Mental Model -With the default [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider), plain file requests still fall through to static files because it uses [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly). That means: +With the default [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider), the provider itself still uses [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly), but the default middleware callback inserts `autoorient=true` when no `autoorient` command is present. In practice that means: + +- `/images/photo.jpg` is intercepted, auto-oriented, cached, and served by ImageSharp.Web. +- `/images/photo.jpg?width=400` is also intercepted and processed by ImageSharp.Web. -- `/images/photo.jpg` is served by ASP.NET Core static files. -- `/images/photo.jpg?width=400` is intercepted and processed by ImageSharp.Web. +That default favors display correctness over passthrough behavior, especially for formats such as WebP where browser EXIF-orientation support is unreliable. -This is usually the behavior you want for local images because it keeps the unmodified path fast and predictable. +If you want passthrough behavior that only processes URLs that already contain commands, you must replace [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) or otherwise bypass the middleware for those paths. `ProcessingBehavior.CommandOnly` by itself is not enough while the default auto-orientation callback is active. ## Configure the Physical Provider and Cache diff --git a/articles/imagesharp.web/imageproviders.md b/articles/imagesharp.web/imageproviders.md index d4b13f9c9..f8ad15f93 100644 --- a/articles/imagesharp.web/imageproviders.md +++ b/articles/imagesharp.web/imageproviders.md @@ -10,7 +10,7 @@ That means provider order matters. If two providers can both match the same path - It resolves images from the web root by default. - [`PhysicalFileSystemProviderOptions.ProviderRootPath`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProviderRootPath) can be `null`, absolute, or relative to the application content root. -- [`PhysicalFileSystemProviderOptions.ProcessingBehavior`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProcessingBehavior) defaults to [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly), so commandless requests still fall through to static files. +- [`PhysicalFileSystemProviderOptions.ProcessingBehavior`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProcessingBehavior) still defaults to [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly), but the default middleware callback injects `autoorient=true` when the request does not already contain `autoorient`, so local image requests are usually processed anyway. ```csharp using SixLabors.ImageSharp.Web; @@ -26,6 +26,8 @@ builder.Services.AddImageSharp() If you want a provider fixed to `IWebHostEnvironment.WebRootFileProvider` with no extra options, [`WebRootImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.WebRootImageProvider) is also available. +If you want truly command-only processing for local files, changing `ProcessingBehavior` is no longer sufficient on its own. You must also replace or suppress the default [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) behavior that inserts `autoorient=true`. + ## Provider Matching and Ordering ImageSharp.Web stops at the first provider whose `Match` function returns `true`. It does not continue searching if that provider later decides the request is invalid, so keep these rules in mind: diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 2977a2295..93d94b200 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -47,8 +47,8 @@ paket add SixLabors.ImageSharp.Web --version VERSION_NUMBER ## Start Here - [Getting Started](gettingstarted.md) covers the minimal ASP.NET Core setup and the default provider and cache behavior. -- [Configuration and Pipeline](configuration.md) explains what `AddImageSharp()` registers, the middleware's web-focused encoder defaults, ICC profile handling, and how to replace or reorder the moving parts. -- [Processing Commands](processingcommands.md) documents the built-in resize, auto-orient, format, quality, and background-color commands. +- [Configuration and Pipeline](configuration.md) explains what `AddImageSharp()` registers, the middleware's default auto-orientation behavior, web-focused encoder defaults, ICC profile handling, and how to replace or reorder the moving parts. +- [Processing Commands](processingcommands.md) documents the built-in resize, auto-orient, format, quality, and background-color commands, including which ones are implicit by default. - [Image Providers](imageproviders.md) covers filesystem, Azure Blob Storage, and AWS S3 source images. - [Image Caches](imagecaches.md) covers the default physical cache, cloud cache backends, cache keys, and cache lifetime. - [Securing Requests](security.md) explains HMAC signing and preset-only parsing. diff --git a/articles/imagesharp.web/processingcommands.md b/articles/imagesharp.web/processingcommands.md index 16cada194..73c5b05e9 100644 --- a/articles/imagesharp.web/processingcommands.md +++ b/articles/imagesharp.web/processingcommands.md @@ -8,6 +8,7 @@ The default [`QueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Comma - If the same command key appears more than once, the last value wins. - Unknown commands are stripped before HMAC validation and before the processor pipeline runs. +- If `autoorient` is absent, the default middleware callback inserts `autoorient=true` before processing continues. - Processors run in the order their first recognized command appears in the request, not in a hard-coded global order. - Values are parsed with invariant culture by default. If you turn that off, parsing follows `CultureInfo.CurrentCulture`. @@ -41,9 +42,12 @@ Resize commands are handled by [`ResizeWebProcessor`](xref:SixLabors.ImageSharp. ```text /images/photo.jpg?autoorient=true /images/photo.jpg?autoorient=true&width=300&height=200&rmode=crop +/images/photo.jpg?autoorient=false ``` -Use `autoorient=true` when you want the output pixels themselves to be normalized to the display orientation. +ImageSharp.Web behaves as though `autoorient=true` were present unless you explicitly provide `autoorient=false`. That means processed output is EXIF-normalized by default, which avoids format- and browser-specific orientation inconsistencies during web delivery. + +Use `autoorient=false` only when you intentionally want to preserve the original pixel orientation and rely on EXIF metadata downstream. ## Format diff --git a/articles/imagesharp.web/security.md b/articles/imagesharp.web/security.md index dbe5d92d7..e91cd1ff6 100644 --- a/articles/imagesharp.web/security.md +++ b/articles/imagesharp.web/security.md @@ -16,7 +16,7 @@ builder.Services.AddImageSharp(options => }); ``` -Once a non-empty secret key is configured, any request that still contains recognized commands after sanitization must also include a matching `hmac` query parameter. If the token is missing or invalid, the middleware returns HTTP 400. +Once a non-empty secret key is configured, ImageSharp command URLs must also include a matching `hmac` query parameter. If the token is missing or invalid, the middleware returns HTTP 400. Use one stable secret across all app instances that must validate the same URLs. Rotating the secret invalidates previously generated signed URLs. @@ -30,9 +30,6 @@ By default, ImageSharp.Web computes HMAC-SHA256 over a lower-invariant relative That behavior is important because the middleware strips unknown commands before validation. The easiest way to stay in sync with the server is to let ImageSharp.Web compute the token for you instead of re-implementing the canonicalization rules yourself. ->[!NOTE] ->`OnParseCommandsAsync` runs after the middleware computes the HMAC candidate value. If you mutate commands there, treat that callback as trusted server-side logic rather than part of the client-signable contract. - ## Generate Signed URLs on the Server [`RequestAuthorizationUtilities`](xref:SixLabors.ImageSharp.Web.RequestAuthorizationUtilities) is the simplest server-side API for generating a valid token: diff --git a/articles/imagesharp.web/taghelpers.md b/articles/imagesharp.web/taghelpers.md index a858a76ea..c2813935f 100644 --- a/articles/imagesharp.web/taghelpers.md +++ b/articles/imagesharp.web/taghelpers.md @@ -47,13 +47,15 @@ The built-in typed values mirror the processor APIs: The supported built-in attributes are: - `imagesharp-width`, `imagesharp-height`, `imagesharp-rmode`, `imagesharp-ranchor`, `imagesharp-rxy`, `imagesharp-rcolor`, `imagesharp-rsampler`, `imagesharp-compand`, and `imagesharp-orient` for resize behavior. -- `imagesharp-autoorient` for EXIF-based rotation and flipping. +- `imagesharp-autoorient` for explicitly controlling EXIF-based rotation and flipping. - `imagesharp-format` for output format selection. - `imagesharp-bgcolor` for flattening transparency. - `imagesharp-quality` for JPEG and WebP quality. If the `` element does not already have literal `width` and `height` attributes, `ImageTagHelper` also writes them to the markup from the processing dimensions. That helps avoid layout shift for simple resize scenarios. +Because the middleware injects `autoorient=true` by default, you usually do not need `imagesharp-autoorient="true"` just to get correctly oriented output. The main reason to set the attribute explicitly is to opt out with `imagesharp-autoorient="false"` or to make the behavior explicit in markup. + ## Local URLs Versus External URLs `ImageTagHelper` is intended for local application image URLs. It skips `http`, `ftp`, and `data` sources because ImageSharp.Web does not process those through the built-in local-path pipeline. diff --git a/articles/imagesharp.web/troubleshooting.md b/articles/imagesharp.web/troubleshooting.md index 78de6d5d3..db7fee9d1 100644 --- a/articles/imagesharp.web/troubleshooting.md +++ b/articles/imagesharp.web/troubleshooting.md @@ -7,12 +7,12 @@ Most ImageSharp.Web problems come down to one of five layers: middleware order, If `/images/photo.jpg?width=400` behaves the same as `/images/photo.jpg`, check these first: - `app.UseImageSharp()` must run before `app.UseStaticFiles()`. -- The default [`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) only processes requests that actually contain commands because it uses [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly). +- If you replaced [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync), make sure you did not accidentally remove the default `autoorient=true` insertion or other command mutations you rely on. - A provider may be matching the request before the provider you expected. Provider order matters. ## I Get HTTP 400 After Enabling HMAC -Once [`ImageSharpMiddlewareOptions.HMACSecretKey`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.HMACSecretKey) is configured, any request with recognized commands must include a valid `hmac` token. +Once [`ImageSharpMiddlewareOptions.HMACSecretKey`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.HMACSecretKey) is configured, requests that include ImageSharp commands must include a valid `hmac` token. Useful checks: @@ -55,6 +55,25 @@ By default, ImageSharp.Web parses commands with invariant culture. If you set [` That is useful for specialized local workflows, but it also means a value like `0.5` versus `0,5` can behave differently across environments. +## Images Stopped Auto-Rotating After I Customized `OnParseCommandsAsync` + +The default [`OnParseCommandsAsync`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.OnParseCommandsAsync) callback inserts `autoorient=true` when the request does not already contain `autoorient`. + +If you assign your own callback without chaining the previous delegate, you remove that default behavior. Preserve the existing callback first unless you intentionally want to opt out: + +```csharp +using SixLabors.ImageSharp.Web.Middleware; + +Func defaultParse = options.OnParseCommandsAsync; + +options.OnParseCommandsAsync = async context => +{ + await defaultParse(context); + + // Your additional command mutations here. +}; +``` + ## Colors or Compression Changed After I Replaced `options.Configuration` Check whether you replaced [`ImageSharpMiddlewareOptions.Configuration`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.Configuration) with `Configuration.Default.Clone()` or another custom configuration. diff --git a/ext/ImageSharp.Web b/ext/ImageSharp.Web index 338c719cf..fefde84b7 160000 --- a/ext/ImageSharp.Web +++ b/ext/ImageSharp.Web @@ -1 +1 @@ -Subproject commit 338c719cf7a31ac471029d29d87df853a8b68d96 +Subproject commit fefde84b756d0b8783cfe63b824526afc0f6a164 From b14fe1e0e1ac67f09714ec103c6fa0511a42cc9a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Apr 2026 22:48:16 +1000 Subject: [PATCH 11/21] Update to latest quantizer APIs --- articles/imagesharp/gif.md | 4 +++- articles/imagesharp/png.md | 2 ++ articles/imagesharp/quantization.md | 15 ++++++++++++--- ext/Fonts | 2 +- ext/ImageSharp | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/articles/imagesharp/gif.md b/articles/imagesharp/gif.md index 248b8e591..5c388559b 100644 --- a/articles/imagesharp/gif.md +++ b/articles/imagesharp/gif.md @@ -66,10 +66,12 @@ The main knobs are: Common choices include: -- [`OctreeQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.OctreeQuantizer) for a solid general-purpose adaptive palette. +- [`HexadecatreeQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.HexadecatreeQuantizer) for a solid general-purpose adaptive palette. - [`WuQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.WuQuantizer) when you want a high-quality adaptive palette with configurable [`QuantizerOptions`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.QuantizerOptions). - [`PaletteQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.PaletteQuantizer) when you need to lock output to a known palette. +`QuantizerOptions` also exposes [`ColorMatchingMode`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.ColorMatchingMode) with the simplified `Coarse` and `Exact` choices for palette matching. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; diff --git a/articles/imagesharp/png.md b/articles/imagesharp/png.md index 60cbb8931..ef2b77a71 100644 --- a/articles/imagesharp/png.md +++ b/articles/imagesharp/png.md @@ -60,6 +60,8 @@ When you choose palette PNG output, ImageSharp uses the same quantization buildi - [`PixelSamplingStrategy`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.PixelSamplingStrategy) controls how pixels are sampled when building the palette. - [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder.TransparentColorMode) controls how fully transparent pixels are normalized during encoding. +If you pass a quantizer with custom [`QuantizerOptions`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.QuantizerOptions), palette matching is configured through [`ColorMatchingMode`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.ColorMatchingMode), which offers the `Coarse` and `Exact` choices. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; diff --git a/articles/imagesharp/quantization.md b/articles/imagesharp/quantization.md index 1eb28b07b..0dc944806 100644 --- a/articles/imagesharp/quantization.md +++ b/articles/imagesharp/quantization.md @@ -17,7 +17,7 @@ Use quantization when you want smaller palette-based outputs, fixed-color brandi ## Quantize as a Processing Step -The default [`Quantize()`](xref:SixLabors.ImageSharp.Processing.QuantizeExtensions.Quantize*) overload uses [`KnownQuantizers.Octree`](xref:SixLabors.ImageSharp.Processing.KnownQuantizers.Octree), which is a fast, good general-purpose adaptive quantizer. +The default [`Quantize()`](xref:SixLabors.ImageSharp.Processing.QuantizeExtensions.Quantize*) overload uses [`KnownQuantizers.Hexadecatree`](xref:SixLabors.ImageSharp.Processing.KnownQuantizers.Hexadecatree), which is a fast, good general-purpose adaptive quantizer. ```csharp using SixLabors.ImageSharp; @@ -40,7 +40,7 @@ This remaps the image content to a smaller palette before you save it. That can [`KnownQuantizers`](xref:SixLabors.ImageSharp.Processing.KnownQuantizers) exposes reusable built-in choices: -- `KnownQuantizers.Octree` for a fast adaptive quantizer with solid general results. +- `KnownQuantizers.Hexadecatree` for a fast adaptive quantizer with solid general results. - `KnownQuantizers.Wu` for high-quality adaptive palette generation. - `KnownQuantizers.WebSafe` for the fixed web-safe palette. - `KnownQuantizers.Werner` for the fixed Werner palette. @@ -48,7 +48,7 @@ This remaps the image content to a smaller palette before you save it. That can When you need more control, create a quantizer directly: - [`WuQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.WuQuantizer) for adaptive palette generation with configurable [`QuantizerOptions`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.QuantizerOptions). -- [`OctreeQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.OctreeQuantizer) for fast adaptive quantization. +- [`HexadecatreeQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.HexadecatreeQuantizer) for fast adaptive quantization. - [`PaletteQuantizer`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.PaletteQuantizer) when you want to force output to a known palette. ```csharp @@ -76,8 +76,16 @@ image.Mutate(x => x.Quantize(new PaletteQuantizer(brandPalette))); - `MaxColors` limits the palette size. - `Dither` selects the dithering algorithm. - `DitherScale` adjusts how strongly dithering is applied. +- `ColorMatchingMode` chooses how pixels are matched back to palette entries after the palette has been built. - `TransparencyThreshold` and `TransparentColorMode` affect how transparent pixels are reduced into the palette. +[`ColorMatchingMode`](xref:SixLabors.ImageSharp.Processing.Processors.Quantization.ColorMatchingMode) has two built-in choices: + +- `Coarse` is the default and favors speed. +- `Exact` uses more precise palette matching for cases where the extra accuracy matters more than throughput. + +The API surface is intentionally small here: pick the quantizer that builds the palette you want, then choose either `Coarse` or `Exact` for the palette-matching pass. + [`KnownDitherings`](xref:SixLabors.ImageSharp.Processing.KnownDitherings) exposes the built-in dithering algorithms, including ordered Bayer variants and error-diffusion algorithms such as Floyd-Steinberg, Atkinson, Burks, Jarvis-Judice-Ninke, and Stucki. Set `Dither = null` when you want flatter output with no dithering pattern. Keep dithering enabled when you want to hide banding in gradients or other smooth transitions. @@ -103,6 +111,7 @@ image.Save("output-indexed.png", new PngEncoder Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = 128, + ColorMatchingMode = ColorMatchingMode.Exact, Dither = KnownDitherings.FloydSteinberg, DitherScale = 0.75F, TransparentColorMode = TransparentColorMode.Preserve diff --git a/ext/Fonts b/ext/Fonts index 739606071..4ca2629f3 160000 --- a/ext/Fonts +++ b/ext/Fonts @@ -1 +1 @@ -Subproject commit 739606071151a3c8d4dbcd3c1d1b7e5ccb61e024 +Subproject commit 4ca2629f3bf40e194b7aa51ca66bac406b802eab diff --git a/ext/ImageSharp b/ext/ImageSharp index b10a7eda4..b6c4bb824 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit b10a7eda4b354a5ca65c13090fc8b53a24ee9aed +Subproject commit b6c4bb824c4331d20ce663a45270859f3d053c9a From b018dfa1ae39ab8d284b72488db42846cf329472 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Apr 2026 23:45:22 +1000 Subject: [PATCH 12/21] Document ImageInfo.GetPixelMemorySize usage --- articles/imagesharp/identify.md | 22 +++++++++++++++++++++- articles/imagesharp/loadingandsaving.md | 5 +++-- articles/imagesharp/security.md | 4 ++++ articles/imagesharp/troubleshooting.md | 2 ++ ext/ImageSharp | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/articles/imagesharp/identify.md b/articles/imagesharp/identify.md index 9b6c49370..33c4c934c 100644 --- a/articles/imagesharp/identify.md +++ b/articles/imagesharp/identify.md @@ -1,6 +1,6 @@ # Read Image Info Without Decoding -When you are working with uploads, queues, or validation rules, fully decoding every image is often unnecessary work. `Image.Identify()` and `Image.DetectFormat()` let you answer the early questions first: what is this file, how large is it, how many frames does it have, and what kind of pixel data does it claim to contain? +When you are working with uploads, queues, or validation rules, fully decoding every image is often unnecessary work. `Image.Identify()` and `Image.DetectFormat()` let you answer the early questions first: what is this file, how large is it, how many frames does it have, what kind of pixel data does it claim to contain, and how much pixel memory might a full decode require? ## Read Dimensions, Frame Count, and Pixel Info @@ -14,8 +14,27 @@ ImageInfo imageInfo = Image.Identify("input.webp"); Console.WriteLine($"{imageInfo.Width}x{imageInfo.Height}"); Console.WriteLine($"Frames: {imageInfo.FrameCount}"); Console.WriteLine($"Bits per pixel: {imageInfo.PixelType.BitsPerPixel}"); +Console.WriteLine($"Estimated pixel memory: {imageInfo.GetPixelMemorySize():N0} bytes"); ``` +## Estimate Pixel Memory Before Decoding + +[`ImageInfo.GetPixelMemorySize()`](xref:SixLabors.ImageSharp.ImageInfo.GetPixelMemorySize) reports the estimated in-memory size of the decoded pixel data represented by the identified image. + +```csharp +using SixLabors.ImageSharp; + +ImageInfo imageInfo = Image.Identify("input.gif"); +long pixelBytes = imageInfo.GetPixelMemorySize(); + +if (pixelBytes > 256L * 1024 * 1024) +{ + throw new InvalidOperationException("Image is too large to decode safely."); +} +``` + +This is especially useful for upload validation and other untrusted-input workflows. A file can be small on disk but still expand into a very large decoded pixel budget, especially for multi-frame formats such as GIF, animated WebP, or TIFF. If frame metadata is available, the reported size includes all frames. + ## Inspect the Encoded Pixel Type [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) gives you the encoded pixel characteristics reported by the format metadata. This is more than a single bit-depth number. [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo) can tell you whether the source is indexed, grayscale, RGB, alpha-bearing, or higher precision: @@ -73,6 +92,7 @@ Console.WriteLine(imageInfo.Height); - `Image.Identify()` is usually much cheaper than `Image.Load()` for inspection-only workflows. - `ImageInfo.Metadata` still gives you access to metadata without allocating a full pixel buffer. - `ImageInfo.PixelType` includes color model, alpha behavior, bit depth, and component precision without decoding the full image. +- `ImageInfo.GetPixelMemorySize()` estimates decoded pixel memory before you commit to a full load. - `Image.DetectFormat()` is focused on encoded format detection, while `Image.Identify()` returns the broader inspection result. For more detail, see [Loading, Identifying, and Saving](loadingandsaving.md), [Working with Metadata](metadata.md), [Convert Between Formats](formatconversion.md), and [Pixel Formats](pixelformats.md). diff --git a/articles/imagesharp/loadingandsaving.md b/articles/imagesharp/loadingandsaving.md index 8e5237e08..9a3481ef7 100644 --- a/articles/imagesharp/loadingandsaving.md +++ b/articles/imagesharp/loadingandsaving.md @@ -41,7 +41,7 @@ Use the async overloads when your application already uses asynchronous I/O, for ## Identify Without Decoding Pixel Data -Use `Image.Identify()` when you only need dimensions, pixel information, or metadata: +Use `Image.Identify()` when you only need dimensions, pixel information, metadata, or a quick decoded memory estimate: ```csharp using SixLabors.ImageSharp; @@ -51,9 +51,10 @@ ImageInfo imageInfo = Image.Identify("input.jpg"); Console.WriteLine($"{imageInfo.Width}x{imageInfo.Height}"); Console.WriteLine($"Bits per pixel: {imageInfo.PixelType.BitsPerPixel}"); Console.WriteLine($"Frames: {imageInfo.FrameCount}"); +Console.WriteLine($"Estimated pixel memory: {imageInfo.GetPixelMemorySize():N0} bytes"); ``` -This avoids allocating the full pixel buffer and is usually the right choice for validation, metadata extraction, and thumbnail planning. +This avoids allocating the full pixel buffer and is usually the right choice for validation, metadata extraction, thumbnail planning, and rejecting images whose decoded pixel budget is too large for your workload. ## Detect the Encoded Format diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index a9b22ee6f..f085db7ac 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -14,10 +14,13 @@ ImageInfo info = Image.Identify("upload.bin"); Console.WriteLine($"{info.Width}x{info.Height}"); Console.WriteLine($"Frames: {info.FrameCount}"); Console.WriteLine($"Bits per pixel: {info.PixelType.BitsPerPixel}"); +Console.WriteLine($"Estimated pixel memory: {info.GetPixelMemorySize():N0} bytes"); ``` This lets you reject obviously unsuitable files before allocating the full decoded image buffers. +[`ImageInfo.GetPixelMemorySize()`](xref:SixLabors.ImageSharp.ImageInfo.GetPixelMemorySize) is particularly useful here. It gives you a decoded pixel-memory estimate up front, which helps protect services against inputs that are cheap to upload but expensive to expand into memory, especially when many frames are involved. + ## Reduce Decode Cost with DecoderOptions [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) is the main place to constrain decode behavior: @@ -110,6 +113,7 @@ For ImageSharp.Web command signing, see [Securing Requests in ImageSharp.Web](.. ## Practical Security Defaults - Use `Identify()` first whenever a full decode is not necessary. +- Use `GetPixelMemorySize()` during identify-based preflight when you need a decoded memory budget check. - Use `TargetSize`, `MaxFrames`, and `SkipMetadata` to shrink decode cost up front. - Prefer `IgnoreNone` or the default `IgnoreNonCritical` over broader error ignoring on untrusted inputs. - Restrict the enabled format modules when your workload only needs a few codecs. diff --git a/articles/imagesharp/troubleshooting.md b/articles/imagesharp/troubleshooting.md index 893c0ac0d..408b87ca1 100644 --- a/articles/imagesharp/troubleshooting.md +++ b/articles/imagesharp/troubleshooting.md @@ -57,6 +57,8 @@ using Image image = Image.Load(options, stream); If decoding fails with an [`InvalidImageContentException`](xref:SixLabors.ImageSharp.InvalidImageContentException) that wraps an [`InvalidMemoryOperationException`](xref:SixLabors.ImageSharp.Memory.InvalidMemoryOperationException), the requested image size or frame set may be beyond the allocator limits or practical memory budget. +Before loading, run `Identify(...)` and inspect [`ImageInfo.GetPixelMemorySize()`](xref:SixLabors.ImageSharp.ImageInfo.GetPixelMemorySize). That gives you a decoded pixel-memory estimate up front and is often the fastest way to spot small encoded files that expand into very large multi-frame allocations. + Ways to reduce decode cost: - use [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) when a smaller decode is acceptable; diff --git a/ext/ImageSharp b/ext/ImageSharp index b6c4bb824..66f21f7ae 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit b6c4bb824c4331d20ce663a45270859f3d053c9a +Subproject commit 66f21f7ae53140e8489aa5b694b53926e30b10bb From 10924039196b9931005ccae231a88b5bf14ce97b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 16 Apr 2026 20:56:05 +1000 Subject: [PATCH 13/21] Add format docs --- articles/imagesharp/bmp.md | 94 +++++++++++++++++++++++++ articles/imagesharp/cur.md | 78 ++++++++++++++++++++ articles/imagesharp/exr.md | 80 +++++++++++++++++++++ articles/imagesharp/formatconversion.md | 2 +- articles/imagesharp/ico.md | 80 +++++++++++++++++++++ articles/imagesharp/imageformats.md | 21 +++--- articles/imagesharp/metadata.md | 2 +- articles/imagesharp/pbm.md | 81 +++++++++++++++++++++ articles/imagesharp/qoi.md | 71 +++++++++++++++++++ articles/imagesharp/tga.md | 75 ++++++++++++++++++++ articles/toc.md | 7 ++ ext/Fonts | 2 +- ext/ImageSharp | 2 +- 13 files changed, 583 insertions(+), 12 deletions(-) create mode 100644 articles/imagesharp/bmp.md create mode 100644 articles/imagesharp/cur.md create mode 100644 articles/imagesharp/exr.md create mode 100644 articles/imagesharp/ico.md create mode 100644 articles/imagesharp/pbm.md create mode 100644 articles/imagesharp/qoi.md create mode 100644 articles/imagesharp/tga.md diff --git a/articles/imagesharp/bmp.md b/articles/imagesharp/bmp.md new file mode 100644 index 000000000..a9d7b3725 --- /dev/null +++ b/articles/imagesharp/bmp.md @@ -0,0 +1,94 @@ +# BMP + +BMP is the classic Windows bitmap format. It is simple, broadly recognized, and sometimes useful when you need predictable bit-depth control or interoperability with older Windows-oriented tools. + +ImageSharp exposes BMP-specific APIs through [`BmpEncoder`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpEncoder), [`BmpDecoderOptions`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpDecoderOptions), and [`BmpMetadata`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpMetadata). + +## Format Characteristics + +BMP is best thought of as a straightforward bitmap container rather than a delivery format optimized for file size. + +A few practical implications: + +- ImageSharp can write BMP output at 1, 2, 4, 8, 16, 24, or 32 bits per pixel. +- Lower bit-depth BMP output is palette based, so encoding can reduce colors rather than preserving full true-color data. +- `SupportTransparency` only applies when writing 32-bit BMP output. +- BMP is easy to exchange with older tooling, but it is usually much larger than PNG, WebP, or QOI for the same image. + +## Save as BMP + +Use [`BmpEncoder`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpEncoder) when you want explicit control over BMP bit depth: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Bmp; + +using Image image = Image.Load("input.png"); + +image.Save("output.bmp", new BmpEncoder +{ + BitsPerPixel = BmpBitsPerPixel.Bit32, + SupportTransparency = true +}); +``` + +## Key BMP Encoder Options + +The most commonly used `BmpEncoder` options are: + +- `BitsPerPixel` controls the encoded BMP bit depth. +- `SupportTransparency` enables BMP alpha support for 32-bit output. +- `Quantizer` and `PixelSamplingStrategy` matter when you target indexed BMP output such as 1, 4, or 8 bits per pixel. + +## Read BMP Metadata + +Use `GetBmpMetadata()` to inspect BMP-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Bmp; + +using Image image = Image.Load("input.bmp"); + +BmpMetadata bmpMetadata = image.Metadata.GetBmpMetadata(); + +Console.WriteLine(bmpMetadata.BitsPerPixel); +Console.WriteLine(bmpMetadata.InfoHeaderType); +``` + +`BmpMetadata` includes values such as: + +- `BitsPerPixel` +- `InfoHeaderType` +- `ColorTable` + +## BMP-Specific Decode Options + +ImageSharp also exposes [`BmpDecoderOptions`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpDecoderOptions) when you need to control how skipped pixels in RLE-compressed BMP data are interpreted: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Bmp; + +BmpDecoderOptions options = new() +{ + RleSkippedPixelHandling = RleSkippedPixelHandling.Transparent +}; + +using Image image = Image.Load(options, "input.bmp"); +``` + +## When to Use BMP + +BMP is usually worth considering when: + +- You need explicit low-level BMP bit-depth control. +- You are interoperating with Windows-oriented tools or older software that expects BMP input. +- File size is a secondary concern. + +BMP is usually a poor fit when: + +- You need compact files for storage or delivery. +- You want a modern web-oriented format. + +For most application and web output, [PNG](png.md), [WebP](webp.md), or [QOI](qoi.md) are usually better starting points. diff --git a/articles/imagesharp/cur.md b/articles/imagesharp/cur.md new file mode 100644 index 000000000..e700a40d1 --- /dev/null +++ b/articles/imagesharp/cur.md @@ -0,0 +1,78 @@ +# CUR + +CUR is the Windows cursor format. It is closely related to ICO, but it carries cursor-specific hotspot information so the runtime knows which pixel is the active click point. + +ImageSharp exposes CUR-specific APIs through [`CurEncoder`](xref:SixLabors.ImageSharp.Formats.Cur.CurEncoder), [`CurMetadata`](xref:SixLabors.ImageSharp.Formats.Cur.CurMetadata), and [`CurFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Cur.CurFrameMetadata). + +## Format Characteristics + +CUR is best thought of as a cursor container rather than a normal image file format. + +A few practical implications: + +- Existing CUR files can contain one or more cursor images. +- Cursor-specific metadata lives primarily on [`CurFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Cur.CurFrameMetadata). +- `HotspotX` and `HotspotY` are the key extra values that distinguish cursor assets from icons. +- CUR is useful when you need Windows cursor output with hotspot metadata, not when you need a general-purpose image format. + +## Save as CUR + +Use [`CurEncoder`](xref:SixLabors.ImageSharp.Formats.Cur.CurEncoder) when you want Windows cursor output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = Image.Load("cursor-source.png"); + +CurFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetCurMetadata(); +frameMetadata.HotspotX = 4; +frameMetadata.HotspotY = 4; + +image.Save("pointer.cur", new CurEncoder()); +``` + +## CUR Frame Metadata + +The most useful CUR-specific values live on [`CurFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Cur.CurFrameMetadata): + +- `HotspotX` and `HotspotY` control the cursor hotspot coordinates. +- `EncodingWidth` and `EncodingHeight` describe the encoded cursor dimensions for that frame. +- `Compression` and `BmpBitsPerPixel` describe how the frame is stored. + +[`CurMetadata`](xref:SixLabors.ImageSharp.Formats.Cur.CurMetadata) mirrors the root frame's compression, bit depth, and color-table information at the image level. + +## Read CUR Metadata + +Use `Image.Identify()` when you want cursor metadata without a full decode: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Cur; + +ImageInfo info = Image.Identify("pointer.cur"); + +Console.WriteLine($"Embedded cursor images: {info.FrameMetadataCollection.Count}"); + +CurMetadata curMetadata = info.Metadata.GetCurMetadata(); +CurFrameMetadata firstFrame = info.FrameMetadataCollection[0].GetCurMetadata(); + +Console.WriteLine(curMetadata.Compression); +Console.WriteLine(firstFrame.HotspotX); +Console.WriteLine(firstFrame.HotspotY); +``` + +## When to Use CUR + +CUR is usually worth considering when: + +- You need a Windows cursor file. +- The hotspot position is part of the asset contract. + +CUR is usually a poor fit when: + +- You are storing a normal image rather than a cursor asset. +- You want broad compatibility outside Windows cursor workflows. + +For Windows icon assets without cursor hotspots, see [ICO](ico.md). diff --git a/articles/imagesharp/exr.md b/articles/imagesharp/exr.md new file mode 100644 index 000000000..554551cb9 --- /dev/null +++ b/articles/imagesharp/exr.md @@ -0,0 +1,80 @@ +# OpenEXR + +OpenEXR is the format to reach for when dynamic range and channel precision matter more than browser compatibility. It is most at home in rendering, compositing, HDR capture, and other imaging pipelines where half-float or float data is part of the workflow. + +ImageSharp supports OpenEXR read and write workflows and exposes EXR-specific metadata through [`ExrMetadata`](xref:SixLabors.ImageSharp.Formats.Exr.ExrMetadata). + +## Format Characteristics + +OpenEXR is best thought of as a high-precision interchange format rather than a delivery format. + +A few practical implications: + +- OpenEXR is common in VFX, rendering, compositing, and HDR-oriented workflows. +- ImageSharp tracks EXR pixel storage through [`ExrPixelType`](xref:SixLabors.ImageSharp.Formats.Exr.Constants.ExrPixelType) and image layout through [`ExrImageDataType`](xref:SixLabors.ImageSharp.Formats.Exr.Constants.ExrImageDataType). +- The current decoder supports uncompressed, ZIP, ZIPS, RLE, and B44-compressed EXR files. +- The current encoder supports uncompressed, ZIP, and ZIPS output. +- OpenEXR is usually not the best choice for browser-facing assets. + +## Save as OpenEXR + +Use [`ExrEncoder`](xref:SixLabors.ImageSharp.Formats.Exr.ExrEncoder) when you want to control how EXR data is written: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; + +using Image image = Image.Load("input.png"); + +image.Save("output.exr", new ExrEncoder +{ + PixelType = ExrPixelType.Half, + Compression = ExrCompression.Zip +}); +``` + +## Key OpenEXR Encoder Options + +The most commonly used `ExrEncoder` options are: + +- `PixelType` controls whether channels are written as `Half`, `Float`, or `UnsignedInt`. +- `Compression` controls the current EXR encoder compression mode. Use `None`, `Zip`, or `Zips`. + +## Read OpenEXR Metadata + +Use `GetExrMetadata()` to inspect EXR-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Exr; + +using Image image = Image.Load("input.exr"); + +ExrMetadata exrMetadata = image.Metadata.GetExrMetadata(); + +Console.WriteLine(exrMetadata.PixelType); +Console.WriteLine(exrMetadata.ImageDataType); +Console.WriteLine(exrMetadata.Compression); +``` + +`ExrMetadata` includes values such as: + +- `PixelType` +- `ImageDataType` +- `Compression` + +## When to Use OpenEXR + +OpenEXR is usually worth considering when: + +- You need HDR or higher-precision image data in a rendering or imaging pipeline. +- You want floating-point or half-float channel storage. +- You care about EXR-specific compression and channel-layout metadata. + +OpenEXR is usually a poor fit when: + +- The output is primarily for browsers or ordinary app delivery. +- You want the broadest ecosystem compatibility for day-to-day assets. + +For everyday application and web output, [PNG](png.md), [JPEG](jpeg.md), [WebP](webp.md), and [TIFF](tiff.md) are usually easier starting points. diff --git a/articles/imagesharp/formatconversion.md b/articles/imagesharp/formatconversion.md index d2b02b09c..55241932b 100644 --- a/articles/imagesharp/formatconversion.md +++ b/articles/imagesharp/formatconversion.md @@ -24,7 +24,7 @@ Before converting, [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify - [`AlphaRepresentation`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.AlphaRepresentation) - [`ComponentInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.ComponentInfo) for component count and precision -This is useful when you need to decide whether to flatten transparency for JPEG, keep higher-precision data in PNG or TIFF, or preserve indexed workflows where the target format supports them. +This is useful when you need to decide whether to flatten transparency for JPEG, keep higher-precision data in PNG, TIFF, or OpenEXR, or preserve indexed workflows where the target format supports them. ## Convert PNG to JPEG diff --git a/articles/imagesharp/ico.md b/articles/imagesharp/ico.md new file mode 100644 index 000000000..08f109579 --- /dev/null +++ b/articles/imagesharp/ico.md @@ -0,0 +1,80 @@ +# ICO + +ICO is the Windows icon container format. It is designed to carry icon image data rather than act as a general-purpose picture format, and ImageSharp exposes both image-level and frame-level ICO metadata because individual embedded icon images can vary. + +ImageSharp exposes ICO-specific APIs through [`IcoEncoder`](xref:SixLabors.ImageSharp.Formats.Ico.IcoEncoder), [`IcoMetadata`](xref:SixLabors.ImageSharp.Formats.Ico.IcoMetadata), and [`IcoFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Ico.IcoFrameMetadata). + +## Format Characteristics + +ICO is best thought of as a container for one or more icon images. + +A few practical implications: + +- Existing ICO files can contain one or more embedded icon images. +- ImageSharp exposes per-frame icon details such as `Compression`, `BmpBitsPerPixel`, `EncodingWidth`, and `EncodingHeight`. +- Frame compression is represented through [`IconFrameCompression`](xref:SixLabors.ImageSharp.Formats.Icon.IconFrameCompression). +- ICO is a Windows asset format, not a general interchange format for ordinary images. + +## Save as ICO + +Use [`IcoEncoder`](xref:SixLabors.ImageSharp.Formats.Ico.IcoEncoder) when you want Windows icon output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = Image.Load("icon-source.png"); + +IcoFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetIcoMetadata(); +frameMetadata.Compression = IconFrameCompression.Png; +frameMetadata.EncodingWidth = 64; +frameMetadata.EncodingHeight = 64; + +image.Save("app.ico", new IcoEncoder()); +``` + +## ICO Frame Metadata + +The most useful ICO-specific values live on [`IcoFrameMetadata`](xref:SixLabors.ImageSharp.Formats.Ico.IcoFrameMetadata): + +- `Compression` controls whether the encoded frame uses BMP or PNG storage. +- `BmpBitsPerPixel` controls the BMP bit depth when the frame is stored as BMP. +- `EncodingWidth` and `EncodingHeight` describe the encoded icon dimensions for that frame. + +[`IcoMetadata`](xref:SixLabors.ImageSharp.Formats.Ico.IcoMetadata) mirrors the root frame's compression, bit depth, and color-table information at the image level. + +## Read ICO Metadata + +Use `Image.Identify()` when you want to inspect the icon container without decoding every embedded image: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Ico; + +ImageInfo info = Image.Identify("app.ico"); + +Console.WriteLine($"Embedded images: {info.FrameMetadataCollection.Count}"); + +IcoMetadata icoMetadata = info.Metadata.GetIcoMetadata(); +IcoFrameMetadata firstFrame = info.FrameMetadataCollection[0].GetIcoMetadata(); + +Console.WriteLine(icoMetadata.Compression); +Console.WriteLine(firstFrame.EncodingWidth); +Console.WriteLine(firstFrame.EncodingHeight); +``` + +## When to Use ICO + +ICO is usually worth considering when: + +- You need a Windows icon file. +- You care about icon-specific frame metadata such as encoded icon dimensions or frame compression. + +ICO is usually a poor fit when: + +- You are storing ordinary images rather than icons. +- You want a broadly portable web or application image format. + +For ordinary image delivery or storage, [PNG](png.md), [WebP](webp.md), and [JPEG](jpeg.md) are usually better choices. diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index 9ecec4c07..b60882825 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -1,6 +1,6 @@ # Image Formats -ImageSharp keeps the in-memory image model separate from the file format on disk. That means the same processing code can work across JPEG, PNG, WebP, TIFF, GIF, and the other built-in codecs, while the encoder and metadata layers handle the format-specific details at the edges. +ImageSharp keeps the in-memory image model separate from the file format on disk. That means the same processing code can work across JPEG, PNG, WebP, TIFF, OpenEXR, GIF, and the other built-in codecs, while the encoder and metadata layers handle the format-specific details at the edges. This page is the format map for the library: which built-in formats ship by default, what each one is good at, and where to go next for format-specific guidance. @@ -12,6 +12,7 @@ The source of truth for the built-in format list is [`Configuration`](xref:SixLa | --- | --- | --- | | BMP | [`BmpFormat`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpFormat) | Read and write | | CUR | [`CurFormat`](xref:SixLabors.ImageSharp.Formats.Cur.CurFormat) | Read and write | +| EXR | [`ExrFormat`](xref:SixLabors.ImageSharp.Formats.Exr.ExrFormat) | Read and write | | GIF | [`GifFormat`](xref:SixLabors.ImageSharp.Formats.Gif.GifFormat) | Read and write | | ICO | [`IcoFormat`](xref:SixLabors.ImageSharp.Formats.Ico.IcoFormat) | Read and write | | JPEG | [`JpegFormat`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegFormat) | Read and write | @@ -33,11 +34,13 @@ If you only need a quick rule of thumb: - GIF is mainly useful for simple palette-based animation and legacy compatibility. - WebP covers lossy, lossless, transparency, and animation in one format family. - TIFF is primarily for archival, print, interchange, and imaging-pipeline workflows. +- OpenEXR is the format to consider for HDR and higher-precision imaging pipelines. Another way to think about it: - Lossy formats: JPEG, lossy WebP. - Lossless formats: PNG, lossless WebP, TIFF, QOI, BMP. +- Higher-precision and HDR workflows: OpenEXR and TIFF. - Transparency-friendly formats: PNG, WebP, TIFF, TGA, QOI. - Animation-friendly formats: GIF, animated PNG workflows through [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder), and animated WebP. @@ -100,6 +103,7 @@ ImageSharp also provides format-specific helpers: - `image.SaveAsBmp()` (shortcut for `image.Save(new BmpEncoder())`) - `image.SaveAsCur()` (shortcut for `image.Save(new CurEncoder())`) +- `image.SaveAsExr()` (shortcut for `image.Save(new ExrEncoder())`) - `image.SaveAsGif()` (shortcut for `image.Save(new GifEncoder())`) - `image.SaveAsIco()` (shortcut for `image.Save(new IcoEncoder())`) - `image.SaveAsJpeg()` (shortcut for `image.Save(new JpegEncoder())`) @@ -144,22 +148,23 @@ For a format-agnostic guide to palettes and dithered output, see [Quantization, ## Format Guides -Use the format-specific guides for the common cases: +Use the format-specific guides for the common cases and specialized workflows: - [JPEG](jpeg.md) for photographic output and quality-focused lossy compression. - [PNG](png.md) for lossless output, transparency, and APNG metadata. - [GIF](gif.md) for palette-based animation workflows. - [WebP](webp.md) for lossy, lossless, transparent, and animated WebP output. - [TIFF](tiff.md) for workflows where compression mode, pixel layout, and TIFF metadata matter. +- [OpenEXR](exr.md) for HDR and higher-precision imaging workflows. The less commonly used built-in formats still have valid niches: -- BMP is simple and broadly understood, but usually much larger than modern alternatives. -- ICO stores Windows icon files, often with multiple embedded image sizes. -- CUR stores Windows cursor files and hotspot metadata. -- PBM is useful for Netpbm-family workflows and simple interchange scenarios. -- TGA appears most often in graphics and content-pipeline tooling. -- QOI is a fast, simple lossless format with a much smaller ecosystem than PNG or WebP. +- [BMP](bmp.md) is simple and broadly understood, but usually much larger than modern alternatives. +- [ICO](ico.md) stores Windows icon files, often with one or more embedded icon images. +- [CUR](cur.md) stores Windows cursor files and hotspot metadata. +- [PBM](pbm.md) covers PBM/PGM/PPM-style Netpbm-family workflows and simple interchange scenarios. +- [TGA](tga.md) appears most often in graphics and content-pipeline tooling. +- [QOI](qoi.md) is a fast, simple lossless format with a much smaller ecosystem than PNG or WebP. ## Custom Format Registration diff --git a/articles/imagesharp/metadata.md b/articles/imagesharp/metadata.md index 7b39d1469..6fcd572d7 100644 --- a/articles/imagesharp/metadata.md +++ b/articles/imagesharp/metadata.md @@ -49,7 +49,7 @@ JpegMetadata jpegMetadata = image.Metadata.GetJpegMetadata(); PngMetadata pngMetadata = image.Metadata.GetPngMetadata(); ``` -Similar helpers exist for other built-in formats, including GIF, TIFF, and WebP. +Similar helpers exist for other built-in formats, including EXR, GIF, TIFF, and WebP. ## Access Frame Metadata diff --git a/articles/imagesharp/pbm.md b/articles/imagesharp/pbm.md new file mode 100644 index 000000000..28738a601 --- /dev/null +++ b/articles/imagesharp/pbm.md @@ -0,0 +1,81 @@ +# PBM / PGM / PPM + +In ImageSharp, [`PbmFormat`](xref:SixLabors.ImageSharp.Formats.Pbm.PbmFormat) covers the Netpbm PNM family: PBM for black-and-white images, PGM for grayscale images, and PPM for RGB images. These formats are intentionally simple and are often used for straightforward interchange or tooling pipelines. + +ImageSharp exposes PNM-specific APIs through [`PbmEncoder`](xref:SixLabors.ImageSharp.Formats.Pbm.PbmEncoder) and [`PbmMetadata`](xref:SixLabors.ImageSharp.Formats.Pbm.PbmMetadata). + +## Format Characteristics + +The PNM family is best thought of as a simple interchange family rather than a compact delivery format. + +A few practical implications: + +- `PbmColorType.BlackAndWhite` maps to PBM output. +- `PbmColorType.Grayscale` maps to PGM output. +- `PbmColorType.Rgb` maps to PPM output. +- `PbmEncoding` lets you choose plain-text or binary pixel encoding. +- `PbmComponentType` lets you choose 1-bit black-and-white, 8-bit components, or 16-bit components depending on the target subformat. + +## Save as PBM / PGM / PPM + +Use [`PbmEncoder`](xref:SixLabors.ImageSharp.Formats.Pbm.PbmEncoder) when you want to choose the exact PNM subformat and encoding style: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Pbm; + +using Image image = Image.Load("input.png"); + +image.Save("output.ppm", new PbmEncoder +{ + ColorType = PbmColorType.Rgb, + ComponentType = PbmComponentType.Byte, + Encoding = PbmEncoding.Binary +}); +``` + +## Key PNM Encoder Options + +The most commonly used `PbmEncoder` options are: + +- `ColorType` selects PBM, PGM, or PPM style output. +- `ComponentType` selects 1-bit, 8-bit, or 16-bit component storage where that subformat allows it. +- `Encoding` selects plain-text or binary pixel encoding. + +## Read PNM Metadata + +Use `GetPbmMetadata()` to inspect PNM-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Pbm; + +using Image image = Image.Load("input.ppm"); + +PbmMetadata pbmMetadata = image.Metadata.GetPbmMetadata(); + +Console.WriteLine(pbmMetadata.ColorType); +Console.WriteLine(pbmMetadata.ComponentType); +Console.WriteLine(pbmMetadata.Encoding); +``` + +`PbmMetadata` includes values such as: + +- `ColorType` +- `ComponentType` +- `Encoding` + +## When to Use PBM / PGM / PPM + +The Netpbm family is usually worth considering when: + +- You need a very simple interchange format. +- Human-readable plain-text image data is useful for debugging or tooling. +- You are working with existing Netpbm-style workflows. + +It is usually a poor fit when: + +- File size matters. +- You need richer metadata, transparency, or modern delivery characteristics. + +For more compact or full-featured output, start with [PNG](png.md), [WebP](webp.md), or [QOI](qoi.md). diff --git a/articles/imagesharp/qoi.md b/articles/imagesharp/qoi.md new file mode 100644 index 000000000..3696090fb --- /dev/null +++ b/articles/imagesharp/qoi.md @@ -0,0 +1,71 @@ +# QOI + +QOI, the Quite OK Image Format, is a simple lossless image format designed around easy implementation and fast encode/decode loops. In ImageSharp, it is a compact option when you want lossless RGB or RGBA output without the broader feature surface of PNG. + +ImageSharp exposes QOI-specific APIs through [`QoiEncoder`](xref:SixLabors.ImageSharp.Formats.Qoi.QoiEncoder) and [`QoiMetadata`](xref:SixLabors.ImageSharp.Formats.Qoi.QoiMetadata). + +## Format Characteristics + +QOI is best thought of as a small, focused lossless format. + +A few practical implications: + +- QOI is lossless. +- The format stores image channel count as RGB or RGBA. +- The format stores a simple color-space flag. +- In ImageSharp, `Channels` and `ColorSpace` are informative metadata. They do not change how the pixel chunks themselves are encoded. +- QOI has a much smaller ecosystem than PNG or WebP. + +## Save as QOI + +Use [`QoiEncoder`](xref:SixLabors.ImageSharp.Formats.Qoi.QoiEncoder) when you want QOI output: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Qoi; + +using Image image = Image.Load("input.png"); + +image.Save("output.qoi", new QoiEncoder +{ + Channels = QoiChannels.Rgba, + ColorSpace = QoiColorSpace.SrgbWithLinearAlpha +}); +``` + +## Key QOI Metadata + +The most useful QOI-specific values are: + +- `Channels`, which records whether the image is RGB or RGBA. +- `ColorSpace`, which records whether the image is tagged as sRGB with linear alpha or all-channels-linear. + +## Read QOI Metadata + +Use `GetQoiMetadata()` to inspect QOI-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Qoi; + +using Image image = Image.Load("input.qoi"); + +QoiMetadata qoiMetadata = image.Metadata.GetQoiMetadata(); + +Console.WriteLine(qoiMetadata.Channels); +Console.WriteLine(qoiMetadata.ColorSpace); +``` + +## When to Use QOI + +QOI is usually worth considering when: + +- You want a simple lossless format in a controlled pipeline. +- Fast, straightforward encoding and decoding matters more than ecosystem breadth. + +QOI is usually a poor fit when: + +- You need broad browser or tool compatibility. +- You need richer metadata or more mature ecosystem support. + +For wider compatibility, [PNG](png.md) and [WebP](webp.md) are usually better starting points. diff --git a/articles/imagesharp/tga.md b/articles/imagesharp/tga.md new file mode 100644 index 000000000..622a3951d --- /dev/null +++ b/articles/imagesharp/tga.md @@ -0,0 +1,75 @@ +# TGA + +TGA, or Truevision TGA, is a straightforward raster format that still shows up in graphics tooling and content pipelines. It is less about delivery to browsers and more about simple pixel storage with familiar bit-depth and compression choices. + +ImageSharp exposes TGA-specific APIs through [`TgaEncoder`](xref:SixLabors.ImageSharp.Formats.Tga.TgaEncoder) and [`TgaMetadata`](xref:SixLabors.ImageSharp.Formats.Tga.TgaMetadata). + +## Format Characteristics + +TGA is best thought of as a simple raster format for tooling and interchange. + +A few practical implications: + +- ImageSharp can write TGA output at 8, 16, 24, or 32 bits per pixel. +- ImageSharp supports uncompressed output or run-length encoded output through `TgaCompression`. +- `TgaMetadata` exposes encoded bit depth and alpha-channel bit information. +- TGA is often useful in asset pipelines, but it is rarely the best choice for browser-facing delivery. + +## Save as TGA + +Use [`TgaEncoder`](xref:SixLabors.ImageSharp.Formats.Tga.TgaEncoder) when you want explicit TGA bit-depth and compression control: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Tga; + +using Image image = Image.Load("input.png"); + +image.Save("output.tga", new TgaEncoder +{ + BitsPerPixel = TgaBitsPerPixel.Bit32, + Compression = TgaCompression.RunLength +}); +``` + +## Key TGA Encoder Options + +The most commonly used `TgaEncoder` options are: + +- `BitsPerPixel` controls the encoded TGA bit depth. +- `Compression` switches between uncompressed and run-length encoded output. + +## Read TGA Metadata + +Use `GetTgaMetadata()` to inspect TGA-specific metadata: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Tga; + +using Image image = Image.Load("input.tga"); + +TgaMetadata tgaMetadata = image.Metadata.GetTgaMetadata(); + +Console.WriteLine(tgaMetadata.BitsPerPixel); +Console.WriteLine(tgaMetadata.AlphaChannelBits); +``` + +`TgaMetadata` includes values such as: + +- `BitsPerPixel` +- `AlphaChannelBits` + +## When to Use TGA + +TGA is usually worth considering when: + +- You are working with graphics or content-pipeline tooling that expects TGA. +- You want a simple raster format with predictable bit-depth choices. + +TGA is usually a poor fit when: + +- The output is primarily intended for browsers or compact delivery. +- You need richer metadata or broader ecosystem support. + +For ordinary web or application output, [PNG](png.md), [JPEG](jpeg.md), and [WebP](webp.md) are usually better starting points. diff --git a/articles/toc.md b/articles/toc.md index 999eae8f1..8a45843cf 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -10,6 +10,13 @@ ### [GIF](imagesharp/gif.md) ### [WebP](imagesharp/webp.md) ### [TIFF](imagesharp/tiff.md) +### [OpenEXR](imagesharp/exr.md) +### [BMP](imagesharp/bmp.md) +### [ICO](imagesharp/ico.md) +### [CUR](imagesharp/cur.md) +### [PBM](imagesharp/pbm.md) +### [TGA](imagesharp/tga.md) +### [QOI](imagesharp/qoi.md) ## [Processing Images](imagesharp/processing.md) ### [Resizing Images](imagesharp/resize.md) ### [Crop, Pad, and Canvas](imagesharp/cropandcanvas.md) diff --git a/ext/Fonts b/ext/Fonts index 4ca2629f3..4b403a735 160000 --- a/ext/Fonts +++ b/ext/Fonts @@ -1 +1 @@ -Subproject commit 4ca2629f3bf40e194b7aa51ca66bac406b802eab +Subproject commit 4b403a7357c00f5e17856be57f4de8469d31a49a diff --git a/ext/ImageSharp b/ext/ImageSharp index 66f21f7ae..b3f37932b 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit 66f21f7ae53140e8489aa5b694b53926e30b10bb +Subproject commit b3f37932b6c79b8d7ad902f1eee09d240b7b3b4d From 7a055663fa6cfc67aba9e51815a2e7cef0ba7ea9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 17 Apr 2026 23:32:33 +1000 Subject: [PATCH 14/21] Document accumulative allocation limit --- articles/imagesharp/configuration.md | 2 +- articles/imagesharp/memorymanagement.md | 7 +++++-- articles/imagesharp/security.md | 5 ++++- articles/imagesharp/troubleshooting.md | 2 +- ext/ImageSharp | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/articles/imagesharp/configuration.md b/articles/imagesharp/configuration.md index 80c226acd..913602704 100644 --- a/articles/imagesharp/configuration.md +++ b/articles/imagesharp/configuration.md @@ -29,7 +29,7 @@ This pattern is usually better than mutating [`Configuration.Default`](xref:SixL The main knobs on [`Configuration`](xref:SixLabors.ImageSharp.Configuration) are: - [`ImageFormatsManager`](xref:SixLabors.ImageSharp.Configuration.ImageFormatsManager) for format registration, encoders, decoders, and detectors. -- [`MemoryAllocator`](xref:SixLabors.ImageSharp.Configuration.MemoryAllocator) for pooled buffer allocation. +- [`MemoryAllocator`](xref:SixLabors.ImageSharp.Configuration.MemoryAllocator) for pooled buffer allocation and custom allocator limits. - [`MaxDegreeOfParallelism`](xref:SixLabors.ImageSharp.Configuration.MaxDegreeOfParallelism) for row and processor parallelism. - [`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) for interop-oriented contiguous buffers. - [`StreamProcessingBufferSize`](xref:SixLabors.ImageSharp.Configuration.StreamProcessingBufferSize) for stream copy buffer size. diff --git a/articles/imagesharp/memorymanagement.md b/articles/imagesharp/memorymanagement.md index b23d653dc..40db76b83 100644 --- a/articles/imagesharp/memorymanagement.md +++ b/articles/imagesharp/memorymanagement.md @@ -22,11 +22,14 @@ Configuration config = Configuration.Default.Clone(); config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { MaximumPoolSizeMegabytes = 128, - AllocationLimitMegabytes = 1024 + AllocationLimitMegabytes = 1024, + AccumulativeAllocationLimitMegabytes = 2048 }); ``` -[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow. +[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow. [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) caps the allocator's total live allocations across all active owners and memory groups. + +Use [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) when you want multiple concurrent images, frames, or intermediate buffers to share one fixed allocator budget. Disposing the image or memory owner releases that reservation so later allocations can succeed. When both limits are set, keep the accumulative limit at or above the single-allocation limit. ## Prefer Contiguous Buffers Only When You Need Them diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index f085db7ac..c4c48d1a7 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -90,10 +90,13 @@ Configuration config = Configuration.Default.Clone(); config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { // Roughly limits the workload to about 64 megapixels of Rgba32 data. - AllocationLimitMegabytes = 256 + AllocationLimitMegabytes = 256, + AccumulativeAllocationLimitMegabytes = 512 }); ``` +[`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) limits the size of any one live allocation group. [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) limits the total live memory reserved through that allocator, which is useful when several requests or frames may overlap. + This is one of the most important safeguards for services that handle arbitrary uploads. For broader guidance on allocator behavior and tradeoffs, see [Memory Management](memorymanagement.md). ## Put Outer Limits Around Streams and Requests diff --git a/articles/imagesharp/troubleshooting.md b/articles/imagesharp/troubleshooting.md index 408b87ca1..15765f1f2 100644 --- a/articles/imagesharp/troubleshooting.md +++ b/articles/imagesharp/troubleshooting.md @@ -64,7 +64,7 @@ Ways to reduce decode cost: - use [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) when a smaller decode is acceptable; - use [`DecoderOptions.MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) to cap animated formats; - use [`DecoderOptions.SkipMetadata`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SkipMetadata) when metadata is not needed; -- adjust [`MemoryAllocatorOptions.AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) if you truly need a larger allocator budget. +- adjust [`MemoryAllocatorOptions.AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) for a larger per-allocation budget, or [`MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) for a larger total live allocator budget. Also avoid turning on [`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) unless you explicitly need contiguous memory for interop. diff --git a/ext/ImageSharp b/ext/ImageSharp index b3f37932b..c5624b534 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit b3f37932b6c79b8d7ad902f1eee09d240b7b3b4d +Subproject commit c5624b534e6e51b5bdeed1aae5b1d7c3a9c330ae From 4af6abfcd393cb56fe65a6d442d2c8aaa23740f5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 10 May 2026 14:35:54 +1000 Subject: [PATCH 15/21] Update to latest APIs, begin canvas --- articles/fonts/caretsandselection.md | 146 +++++++ articles/fonts/checkglyphcoverage.md | 10 +- articles/fonts/colorfonts.md | 34 +- articles/fonts/customrendering.md | 30 +- articles/fonts/fallbackfonts.md | 38 +- articles/fonts/fittexttowidth.md | 2 +- articles/fonts/fontmetadata.md | 27 +- articles/fonts/fontmetrics.md | 40 +- articles/fonts/gettingstarted.md | 30 +- articles/fonts/hintingandshaping.md | 38 +- articles/fonts/index.md | 5 +- articles/fonts/inspectfontfiles.md | 8 +- articles/fonts/listsystemfonts.md | 2 +- articles/fonts/measuringtext.md | 100 +++-- articles/fonts/opentypefeatures.md | 44 +- articles/fonts/systemfonts.md | 24 +- articles/fonts/textblock.md | 112 +++++ articles/fonts/texthittesting.md | 161 +++++++ articles/fonts/textlayout.md | 116 ++++-- articles/fonts/troubleshooting.md | 36 +- articles/fonts/unicode.md | 70 +++- articles/fonts/useopentypefeatures.md | 15 +- articles/fonts/variablefonts.md | 22 +- articles/imagesharp.drawing/annotations.md | 47 +++ articles/imagesharp.drawing/badge.md | 53 +++ articles/imagesharp.drawing/brushesandpens.md | 272 ++++++++++++ articles/imagesharp.drawing/canvas.md | 386 +++++++++++++++++ .../imagesharp.drawing/clipimagetoshape.md | 49 +++ .../clippingregionslayers.md | 101 +++++ articles/imagesharp.drawing/gettingstarted.md | 173 +++++--- .../imagesharp.drawing/imagesandprocessing.md | 97 +++++ articles/imagesharp.drawing/index.md | 29 +- articles/imagesharp.drawing/pathsandshapes.md | 320 ++++++++++++++ articles/imagesharp.drawing/primitives.md | 86 ++++ articles/imagesharp.drawing/recipes.md | 19 + articles/imagesharp.drawing/softshadow.md | 38 ++ articles/imagesharp.drawing/text.md | 300 +++++++++++++ .../transformsandcomposition.md | 116 ++++++ .../imagesharp.drawing/troubleshooting.md | 144 +++++++ articles/imagesharp.drawing/watermark.md | 41 ++ articles/imagesharp.drawing/webgpu.md | 393 ++++++++++++++++++ articles/imagesharp.web/configuration.md | 14 +- articles/imagesharp.web/extensibility.md | 10 +- articles/imagesharp.web/imagecaches.md | 10 +- articles/imagesharp.web/imageproviders.md | 16 +- articles/imagesharp.web/index.md | 2 +- articles/imagesharp/gettingstarted.md | 24 +- articles/imagesharp/gif.md | 4 +- articles/imagesharp/imageformats.md | 38 +- articles/imagesharp/index.md | 2 +- articles/imagesharp/memorymanagement.md | 7 +- articles/imagesharp/security.md | 16 +- articles/imagesharp/troubleshooting.md | 2 +- articles/polygonclipper/booleanoperations.md | 10 +- articles/polygonclipper/gettingstarted.md | 12 +- articles/polygonclipper/index.md | 8 +- articles/polygonclipper/normalization.md | 4 +- .../polygonclipper/polygonsandcontours.md | 22 +- articles/polygonclipper/stroking.md | 17 +- articles/toc.md | 19 + ext/Fonts | 2 +- ext/ImageSharp | 2 +- ext/PolygonClipper | 2 +- 63 files changed, 3570 insertions(+), 447 deletions(-) create mode 100644 articles/fonts/caretsandselection.md create mode 100644 articles/fonts/textblock.md create mode 100644 articles/fonts/texthittesting.md create mode 100644 articles/imagesharp.drawing/annotations.md create mode 100644 articles/imagesharp.drawing/badge.md create mode 100644 articles/imagesharp.drawing/brushesandpens.md create mode 100644 articles/imagesharp.drawing/canvas.md create mode 100644 articles/imagesharp.drawing/clipimagetoshape.md create mode 100644 articles/imagesharp.drawing/clippingregionslayers.md create mode 100644 articles/imagesharp.drawing/imagesandprocessing.md create mode 100644 articles/imagesharp.drawing/pathsandshapes.md create mode 100644 articles/imagesharp.drawing/primitives.md create mode 100644 articles/imagesharp.drawing/recipes.md create mode 100644 articles/imagesharp.drawing/softshadow.md create mode 100644 articles/imagesharp.drawing/text.md create mode 100644 articles/imagesharp.drawing/transformsandcomposition.md create mode 100644 articles/imagesharp.drawing/troubleshooting.md create mode 100644 articles/imagesharp.drawing/watermark.md create mode 100644 articles/imagesharp.drawing/webgpu.md diff --git a/articles/fonts/caretsandselection.md b/articles/fonts/caretsandselection.md new file mode 100644 index 000000000..a82ed53cd --- /dev/null +++ b/articles/fonts/caretsandselection.md @@ -0,0 +1,146 @@ +# Selection and Bidi Drag + +Once you can hit-test a point and place a caret, the next step is painting selection ranges. Fonts returns selection geometry as a list of rectangles in visual order so editor-style UIs can paint browser-shaped selections without reimplementing bidi or line-box rules. + +For the underlying types — [`TextHit`](xref:SixLabors.Fonts.TextHit), [`CaretPosition`](xref:SixLabors.Fonts.CaretPosition), [`CaretPlacement`](xref:SixLabors.Fonts.CaretPlacement), and [`CaretMovement`](xref:SixLabors.Fonts.CaretMovement) — see [Hit Testing and Caret Movement](texthittesting.md). + +### The shape of a selection + +[`GetSelectionBounds(...)`](xref:SixLabors.Fonts.TextMetrics.GetSelectionBounds*) returns `ReadOnlyMemory`. Use `.Span` when drawing, and store the memory itself if the selection needs to be retained alongside other layout state. + +```csharp +using System; +using SixLabors.Fonts; + +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); + +foreach (FontRectangle rectangle in selection.Span) +{ + FillSelectionRectangle(rectangle); +} +``` + +A single logical selection can be visually discontinuous inside one line when it crosses bidi runs. Returning multiple rectangles allows browser-style selection where the unselected visual gap stays unpainted. + +Do not sort, union, or merge the returned rectangles unless the UI explicitly wants a different visual. + +### Pointer selection + +For pointer drags, hit-test both endpoints and pass the hits to the selection API. The [`TextHit`](xref:SixLabors.Fonts.TextHit) overload converts both endpoints to logical insertion indices for you. + +```csharp +using System.Numerics; +using SixLabors.Fonts; + +TextHit anchor = metrics.HitTest(new Vector2(downX, downY)); +TextHit focus = metrics.HitTest(new Vector2(moveX, moveY)); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +This keeps trailing-edge and bidi handling inside the library. + +### Keyboard selection + +For keyboard selection, keep an anchor caret fixed and move the focus caret. Shift+Right-style behavior updates only the focus caret. + +```csharp +using SixLabors.Fonts; + +CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); +CaretPosition focus = anchor; + +focus = metrics.MoveCaret(focus, CaretMovement.Next); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +Selecting whole words via keyboard is the same shape: move the focus by `NextWord` or `PreviousWord`. + +### Word selection + +For double-click word selection, find the word containing the hit and ask for its selection bounds. + +```csharp +using SixLabors.Fonts; + +TextHit hit = metrics.HitTest(doubleClickPosition); +WordMetrics word = metrics.GetWordMetrics(hit); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(word); +``` + +The [`GraphemeMetrics`](xref:SixLabors.Fonts.GraphemeMetrics) overload selects exactly one grapheme, which is useful for caret-region overlays: + +```csharp +using SixLabors.Fonts; + +GraphemeMetrics grapheme = metrics.GraphemeMetrics[index]; +ReadOnlyMemory selection = metrics.GetSelectionBounds(grapheme); +``` + +### Bidi drag selection + +Consider a left-to-right paragraph whose source text is: + +```text +Tall שלום עرب +``` + +The right-to-left run can paint with Arabic before Hebrew. When a user drags from the left edge of `Tall` toward the Hebrew word, the visual selection can become split: + +```text +[Tall ] עرب [שלום] +``` + +Application code should not manually decide which physical edge of the Hebrew glyph means "before" or "after". The hit-test result already carries the logical insertion index, and the selection result is already split into the visual rectangles that should be painted. + +```csharp +using SixLabors.Fonts; + +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +ReadOnlyMemory rectangles = metrics.GetSelectionBounds(anchor, focus); +``` + +Just paint every rectangle. The library produces the correct visual gaps. + +### Hard line breaks + +Hard line breaks that end non-empty lines are trimmed with trailing breaking whitespace. Hard line breaks that own a blank line remain in the metrics and contribute their own selection rectangle so the blank line still highlights when the selection crosses it. + +For text with two hard breaks in the middle: + +```text +Tall عرب שלום + +Small مرحبا שלום +``` + +A full selection paints three visual rows: the first text line, the blank line, and the second text line. The line break that ends a non-empty line does not add a separate painted box; the line break that owns the blank line does. Callers should not special-case this — paint the rectangles `GetSelectionBounds` returns. + +Consumers that inspect individual graphemes can use [`GraphemeMetrics.IsLineBreak`](xref:SixLabors.Fonts.GraphemeMetrics.IsLineBreak) to identify the blank-line hard breaks that remain in the metrics. + +In `TextInteractionMode.Editor`, a hard break that ends the text produces an additional blank line so a selection can extend past the final newline; `TextInteractionMode.Paragraph` omits that trailing blank line. See [Hit Testing and Caret Movement](texthittesting.md) for the full mode comparison. + +### Per-line selection + +[`LineLayout`](xref:SixLabors.Fonts.LineLayout) exposes the same selection overloads when the caller knows the selection is line-local: + +```csharp +using SixLabors.Fonts; + +LineLayout line = layouts.Span[lineIndex]; + +ReadOnlyMemory selection = line.GetSelectionBounds(anchor, focus); +ReadOnlyMemory wordSelection = line.GetSelectionBounds(word); +``` + +Use the full [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) overloads for selections that can cross line boundaries; use [`LineLayout`](xref:SixLabors.Fonts.LineLayout) only when interaction is bounded to one line. + +### Stable line-box geometry + +Per-line selection uses the line-box height rather than per-glyph height, which matches normal text editor and browser behavior: selecting mixed font sizes on the same line paints a consistent line-height rectangle rather than one rectangle per glyph height. The selection geometry stays visually stable across mixed fonts and font sizes. + +For a wider tour of the measurement model and how line metrics are derived, see [Measuring Text](measuringtext.md). diff --git a/articles/fonts/checkglyphcoverage.md b/articles/fonts/checkglyphcoverage.md index 87c5877e2..b90fa31b8 100644 --- a/articles/fonts/checkglyphcoverage.md +++ b/articles/fonts/checkglyphcoverage.md @@ -1,6 +1,6 @@ # Check Glyph Coverage Before Choosing Fallbacks -Before you wire up fallback families, it helps to know what your primary font can already cover. This recipe shows a quick way to probe individual scalar values or scan a string so you can make fallback decisions based on actual glyph coverage instead of guesswork. +Before you wire up fallback families, it helps to know what your primary font can already cover. This recipe shows a quick way to probe individual scalar values with [`Font.TryGetGlyphs(...)`](xref:SixLabors.Fonts.Font.TryGetGlyphs*) or scan a string so you can make fallback decisions based on actual glyph coverage instead of guesswork. ### Check individual code points @@ -11,8 +11,8 @@ using SixLabors.Fonts.Unicode; Font font = SystemFonts.CreateFont("Segoe UI", 16); bool hasLatinA = font.TryGetGlyphs(new CodePoint('A'), out _); -bool hasOmega = font.TryGetGlyphs(new CodePoint(0x03A9), out _); -bool hasEmoji = font.TryGetGlyphs(new CodePoint(0x1F600), out _); +bool hasOmega = font.TryGetGlyphs(new CodePoint(0x03A9), out _); // Ω GREEK CAPITAL LETTER OMEGA +bool hasEmoji = font.TryGetGlyphs(new CodePoint(0x1F600), out _); // 😀 GRINNING FACE ``` ### Scan a whole string for missing glyphs @@ -22,7 +22,7 @@ using System.Collections.Generic; using SixLabors.Fonts; using SixLabors.Fonts.Unicode; -string text = "Hello 123 \u0645\u0631\u062D\u0628\u0627 \uD83D\uDE00"; +string text = "Hello 123 مرحبا 😀"; Font font = SystemFonts.CreateFont("Segoe UI", 16); List missing = new(); @@ -37,6 +37,6 @@ foreach (CodePoint codePoint in text.AsSpan().EnumerateCodePoints()) This is a simple way to decide whether you need `FallbackFontFamilies` before you measure or render the text. -If you want a broader face-level view instead of checking a specific string, use `Font.FontMetrics.GetAvailableCodePoints()`. +If you want a broader face-level view instead of checking a specific string, use [`Font.FontMetrics.GetAvailableCodePoints()`](xref:SixLabors.Fonts.FontMetrics.GetAvailableCodePoints*). For the conceptual fallback guidance, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). For face-level coverage inspection, see [Font Metrics](fontmetrics.md). diff --git a/articles/fonts/colorfonts.md b/articles/fonts/colorfonts.md index f47d600d5..1e3d0eb9d 100644 --- a/articles/fonts/colorfonts.md +++ b/articles/fonts/colorfonts.md @@ -1,6 +1,6 @@ # Color Fonts -Color fonts are one of the clearest signs of how much richer modern text rendering has become. Instead of a single monochrome outline, a glyph can carry layers, gradients, or even SVG content, and Fonts exposes that support explicitly. +Color fonts are one of the clearest signs of how much richer modern text rendering has become. Instead of a single monochrome outline, a glyph can carry layers, gradients, or even SVG content, and Fonts exposes that support explicitly through [`ColorFontSupport`](xref:SixLabors.Fonts.ColorFontSupport). Fonts has comprehensive support for the major OpenType color-font technologies it exposes publicly: @@ -10,7 +10,7 @@ Fonts has comprehensive support for the major OpenType color-font technologies i ### Enable or restrict color-font support -`TextOptions.ColorFontSupport` controls which color-font technologies are honored during layout and rendering. +[`TextOptions.ColorFontSupport`](xref:SixLabors.Fonts.TextOptions.ColorFontSupport) controls which color-font technologies are honored during layout and rendering. ```csharp using SixLabors.Fonts; @@ -25,11 +25,11 @@ TextOptions options = new(font) }; ``` -`TextOptions` enables all three by default, so you usually only need to set this property when you want to disable color glyphs or restrict the allowed formats. +[`TextOptions`](xref:SixLabors.Fonts.TextOptions) enables all three by default, so you usually only need to set this property when you want to disable color glyphs or restrict the allowed formats. ### Force monochrome output -Set `ColorFontSupport.None` when you want color-font-capable text to fall back to monochrome outline rendering. +Set [`ColorFontSupport.None`](xref:SixLabors.Fonts.ColorFontSupport.None) when you want color-font-capable text to fall back to monochrome outline rendering. ```csharp using SixLabors.Fonts; @@ -46,28 +46,28 @@ TextOptions options = new(font) ### What happens in custom renderers -When a resolved glyph is a painted color glyph, Fonts streams it through `IGlyphRenderer` as one or more layers. +When a resolved glyph is a painted color glyph, Fonts streams it through [`IGlyphRenderer`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer) as one or more layers. That means custom renderers should pay attention to: -- `GlyphRendererParameters.GlyphType` -- `BeginLayer(...)` -- `Paint` -- `FillRule` -- `ClipQuad` +- [`GlyphRendererParameters.GlyphType`](xref:SixLabors.Fonts.Rendering.GlyphRendererParameters.GlyphType) +- [`BeginLayer(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginLayer*) +- [`Paint`](xref:SixLabors.Fonts.Rendering.Paint) +- [`FillRule`](xref:SixLabors.Fonts.Rendering.FillRule) +- [`ClipQuad`](xref:SixLabors.Fonts.ClipQuad) Depending on the font technology in use, the `Paint` passed to `BeginLayer(...)` may be: -- `SolidPaint` -- `LinearGradientPaint` -- `RadialGradientPaint` -- `SweepGradientPaint` +- [`SolidPaint`](xref:SixLabors.Fonts.Rendering.SolidPaint) +- [`LinearGradientPaint`](xref:SixLabors.Fonts.Rendering.LinearGradientPaint) +- [`RadialGradientPaint`](xref:SixLabors.Fonts.Rendering.RadialGradientPaint) +- [`SweepGradientPaint`](xref:SixLabors.Fonts.Rendering.SweepGradientPaint) If your renderer ignores paint information, the glyph can still be drawn, but it will no longer preserve the font's intended color presentation. ### Inspect color glyphs directly -If you need to inspect a glyph without running full text layout, use `Font.TryGetGlyphs(...)` with explicit color support. +If you need to inspect a glyph without running full text layout, use [`Font.TryGetGlyphs(...)`](xref:SixLabors.Fonts.Font.TryGetGlyphs*) with explicit color support. ```csharp using SixLabors.Fonts; @@ -78,7 +78,7 @@ FontFamily family = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); Font font = family.CreateFont(32); if (font.TryGetGlyphs( - new CodePoint(0x1F600), + new CodePoint(0x1F600), // 😀 GRINNING FACE ColorFontSupport.ColrV1 | ColorFontSupport.ColrV0 | ColorFontSupport.Svg, out Glyph? glyph)) { @@ -100,6 +100,6 @@ Fonts resolves those technologies into a common painted-glyph rendering flow, wh Color-font support is part of text layout, not just final painting. If you measure text with one `ColorFontSupport` configuration and render with another, you can create drift between the measured and rendered result. -Use the same `TextOptions` instance for both `TextMeasurer` and `TextRenderer` when you want a guaranteed match. +Use the same [`TextOptions`](xref:SixLabors.Fonts.TextOptions) instance for both [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) and [`TextRenderer`](xref:SixLabors.Fonts.Rendering.TextRenderer) when you want a guaranteed match. For renderer implementation details, see [Custom Rendering](customrendering.md). For fallback across multiple families, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). diff --git a/articles/fonts/customrendering.md b/articles/fonts/customrendering.md index e97f49325..4b4ec2f7f 100644 --- a/articles/fonts/customrendering.md +++ b/articles/fonts/customrendering.md @@ -5,7 +5,7 @@ Most developers meet Fonts through [ImageSharp.Drawing](../imagesharp.drawing/index.md), where the rendering surface is already handled for you. This page is for the next step down: when you want Fonts to do the shaping and glyph decomposition, but you want to decide how those glyphs are painted or exported. -Custom rendering in Fonts is built around `IGlyphRenderer`. `TextRenderer.RenderTextTo(...)` performs layout and shaping, then sends the result to your renderer as glyphs, layers, figures, and path commands. +Custom rendering in Fonts is built around [`IGlyphRenderer`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer). [`TextRenderer.RenderTextTo(...)`](xref:SixLabors.Fonts.Rendering.TextRenderer.RenderTextTo*) performs layout and shaping, then sends the result to your renderer as glyphs, layers, figures, and path commands. ### When to use it @@ -18,18 +18,18 @@ Custom rendering is useful when you want to: ### Rendering flow -The callbacks are delivered in this order: +For monochrome outline glyphs, the path callbacks are delivered inside [`BeginGlyph(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginGlyph*) / [`EndGlyph()`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.EndGlyph*): 1. `BeginText(...)` 2. `BeginGlyph(...)` -3. `BeginLayer(...)` -4. `BeginFigure()`, `MoveTo(...)`, `LineTo(...)`, `QuadraticBezierTo(...)`, `CubicBezierTo(...)`, `ArcTo(...)`, `EndFigure()` -5. `EndLayer()` -6. `EndGlyph()` -7. `SetDecoration(...)` for any decorations requested by `EnabledDecorations()` -8. `EndText()` +3. `BeginFigure()`, `MoveTo(...)`, `LineTo(...)`, `QuadraticBezierTo(...)`, `CubicBezierTo(...)`, `ArcTo(...)`, `EndFigure()` +4. `EndGlyph()` +5. `SetDecoration(...)` for any decorations requested by `EnabledDecorations()` +6. `EndText()` -`BeginGlyph(...)` receives `GlyphRendererParameters`, which identify the glyph instance being rendered, including the glyph ID, the glyph's `CodePoint` value, font style, point size, DPI, layout mode, and active `TextRun`. Return `false` from `BeginGlyph(...)` if you want to skip rendering that glyph. +Painted color glyphs add [`BeginLayer(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginLayer*) / [`EndLayer()`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.EndLayer*) around each painted layer between [`BeginGlyph(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginGlyph*) and [`EndGlyph()`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.EndGlyph*). + +[`BeginGlyph(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginGlyph*) receives [`GlyphRendererParameters`](xref:SixLabors.Fonts.Rendering.GlyphRendererParameters), which identify the glyph instance being rendered, including the glyph ID, the glyph's [`CodePoint`](xref:SixLabors.Fonts.Unicode.CodePoint) value, font style, point size, DPI, layout mode, and active [`TextRun`](xref:SixLabors.Fonts.TextRun). Return `false` from `BeginGlyph(...)` if you want to skip rendering that glyph. ### A minimal renderer @@ -121,21 +121,21 @@ Replace `"Segoe UI"` with any installed family that exists on your machine. ### Layers, paints, and color fonts -`BeginLayer(...)` is where Fonts communicates how the current glyph layer should be filled: +[`BeginLayer(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginLayer*) is where Fonts communicates how the current glyph layer should be filled: -- `paint` may be `null` for outline-only content -- `SolidPaint` represents a single color -- `LinearGradientPaint`, `RadialGradientPaint`, and `SweepGradientPaint` are used for richer color-font layers +- `paint` may be `null` when a painted layer does not specify paint information +- [`SolidPaint`](xref:SixLabors.Fonts.Rendering.SolidPaint) represents a single color +- [`LinearGradientPaint`](xref:SixLabors.Fonts.Rendering.LinearGradientPaint), [`RadialGradientPaint`](xref:SixLabors.Fonts.Rendering.RadialGradientPaint), and [`SweepGradientPaint`](xref:SixLabors.Fonts.Rendering.SweepGradientPaint) are used for richer color-font layers - `fillRule` tells you how the path should be filled - `clipBounds` provides an optional clip quad for the layer -If your renderer only supports monochrome output, you can ignore `paint` and render every layer with your own brush. If you want color-font output, honor both `ColorFontSupport` in `TextOptions` and the `Paint` information delivered to `BeginLayer(...)`. +If your renderer only supports monochrome output, you can ignore `paint` when a painted layer is delivered and fill that layer with your own brush. If you want color-font output, honor both [`ColorFontSupport`](xref:SixLabors.Fonts.TextOptions.ColorFontSupport) in [`TextOptions`](xref:SixLabors.Fonts.TextOptions) and the [`Paint`](xref:SixLabors.Fonts.Rendering.Paint) information delivered to [`BeginLayer(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.BeginLayer*). See [Color Fonts](colorfonts.md) for a fuller guide to `ColorFontSupport`, painted glyphs, and the different color-font technologies that Fonts can surface. ### Decorations -Decorations are opt-in. Return the decorations you care about from `EnabledDecorations()`, and Fonts will call `SetDecoration(...)` after the glyph geometry has been emitted. +Decorations are opt-in. Return the decorations you care about from [`EnabledDecorations()`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.EnabledDecorations*) and Fonts will call [`SetDecoration(...)`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer.SetDecoration*) after the glyph geometry has been emitted. ```csharp public TextDecorations EnabledDecorations() diff --git a/articles/fonts/fallbackfonts.md b/articles/fonts/fallbackfonts.md index 904af25b8..fe10e8d61 100644 --- a/articles/fonts/fallbackfonts.md +++ b/articles/fonts/fallbackfonts.md @@ -2,13 +2,13 @@ Real text rarely stays inside one script or one font. User names, emoji, CJK text, math, and symbols all show up in the same application, so fallback is what turns a nice Latin-only demo into a text stack that survives real-world input. -Fonts handles that through `TextOptions.FallbackFontFamilies`. +Fonts handles that through [`TextOptions.FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies). -When the primary `Font` does not contain a glyph for part of the text, the layout engine searches the fallback families in order and uses the first family that can supply the missing glyphs. +When the primary [`Font`](xref:SixLabors.Fonts.Font) does not contain a glyph for part of the text, the layout engine searches the fallback families in order and uses the first family that can supply the missing glyphs. ### Use families, not fonts -Fallback is configured with `FontFamily` instances, not `Font` instances. +Fallback is configured with [`FontFamily`](xref:SixLabors.Fonts.FontFamily) instances, not [`Font`](xref:SixLabors.Fonts.Font) instances. ```csharp using SixLabors.Fonts; @@ -20,7 +20,7 @@ FontFamily emoji = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); TextOptions options = new(latin.CreateFont(16)) { - FallbackFontFamilies = new[] { arabic, emoji } + FallbackFontFamilies = [arabic, emoji] }; ``` @@ -46,28 +46,28 @@ FontFamily latin = collection.Add("fonts/NotoSans-Regular.ttf"); FontFamily arabic = collection.Add("fonts/NotoSansArabic-Regular.ttf"); FontFamily emoji = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); -string text = "Status: ready \U0001F600 \u0645\u0631\u062D\u0628\u0627"; +string text = "Status: ready 😀 مرحبا"; TextOptions options = new(latin.CreateFont(18)) { - FallbackFontFamilies = new[] { arabic, emoji }, + FallbackFontFamilies = [arabic, emoji], TextDirection = TextDirection.Auto, ColorFontSupport = ColorFontSupport.ColrV1 | ColorFontSupport.Svg }; ``` -`TextDirection.Auto` lets the layout engine determine whether a run should flow left-to-right or right-to-left. `ColorFontSupport` matters when one of your fallback families is a color emoji font. +[`TextDirection.Auto`](xref:SixLabors.Fonts.TextDirection.Auto) lets the layout engine determine whether a run should flow left-to-right or right-to-left. [`ColorFontSupport`](xref:SixLabors.Fonts.TextOptions.ColorFontSupport) matters when one of your fallback families is a color emoji font. ### Fallback is not the same as explicit styling Use fallback fonts when the goal is "use another family if the current one cannot render this text". -Use `TextRuns` when the goal is "this specific range should use a different font even if the base font could render it". +Use [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) when the goal is "this specific range should use a different font even if the base font could render it". ```csharp using SixLabors.Fonts; -const string text = "Latin title \u0627\u0644\u0639\u0631\u0628\u064A\u0629"; +const string text = "Latin title العربية"; FontCollection collection = new(); FontFamily latin = collection.Add("fonts/NotoSans-Regular.ttf"); @@ -75,36 +75,36 @@ FontFamily arabic = collection.Add("fonts/NotoSansArabic-Regular.ttf"); TextOptions options = new(latin.CreateFont(18)) { - FallbackFontFamilies = new[] { arabic }, - TextRuns = new[] - { + FallbackFontFamilies = [arabic], + TextRuns = + [ new TextRun { Start = 12, End = 19, Font = arabic.CreateFont(18) } - } + ] }; ``` -The fallback list helps with missing glyphs. `TextRuns` gives you deliberate control over which grapheme ranges use which fonts. +The fallback list helps with missing glyphs. [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) gives you deliberate control over which grapheme ranges use which fonts. ### Wrapping and script behavior Multilingual text often benefits from layout settings beyond just fallback families: -- `TextDirection.Auto` for mixed LTR and RTL content -- `WordBreaking.KeepAll` or `WordBreaking.BreakWord` for CJK-heavy text -- `LayoutMode` for vertical scripts or mixed vertical presentation +- [`TextDirection.Auto`](xref:SixLabors.Fonts.TextDirection.Auto) for mixed LTR and RTL content +- [`WordBreaking.KeepAll`](xref:SixLabors.Fonts.WordBreaking.KeepAll) or [`WordBreaking.BreakWord`](xref:SixLabors.Fonts.WordBreaking.BreakWord) for CJK-heavy text +- [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) for vertical scripts or mixed vertical presentation If a script needs shaping support, make sure the selected font actually supports that script. Fallback can only help if one of the supplied families contains the needed glyphs and shaping data. ### Common pitfalls - A fallback family will not be used if the primary font already has a glyph for that Unicode scalar value, even if you would prefer the fallback font's design. -- `TextRuns` use grapheme indices, not UTF-16 code-unit indices. -- Emoji color layers are only used if `ColorFontSupport` allows them. +- [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) use grapheme indices, not UTF-16 code-unit indices. +- Emoji color layers are only used if [`ColorFontSupport`](xref:SixLabors.Fonts.TextOptions.ColorFontSupport) allows them. - Mixing many broad-coverage fonts can make fallback order hard to reason about. If layout still looks wrong after fallback is configured, see [Troubleshooting](troubleshooting.md). diff --git a/articles/fonts/fittexttowidth.md b/articles/fonts/fittexttowidth.md index a41914c58..105d60458 100644 --- a/articles/fonts/fittexttowidth.md +++ b/articles/fonts/fittexttowidth.md @@ -5,7 +5,7 @@ Fitting text into a fixed width is one of those jobs that sounds simple until yo For single-line text, the usual pattern is: 1. start with a candidate font size -2. measure with `TextMeasurer.MeasureAdvance(...)` +2. measure with [`TextMeasurer.MeasureAdvance(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureAdvance*) 3. reduce the size until the width fits ```csharp diff --git a/articles/fonts/fontmetadata.md b/articles/fonts/fontmetadata.md index b6b177bc5..89154258d 100644 --- a/articles/fonts/fontmetadata.md +++ b/articles/fonts/fontmetadata.md @@ -1,10 +1,10 @@ # Font Metadata and Inspection -Sometimes you need to inspect a font long before you care about laying text out with it. Maybe you are building an importer, a picker, or a diagnostics tool. `FontDescription` is the lightweight part of the API for that job. +Sometimes you need to inspect a font long before you care about laying text out with it. Maybe you are building an importer, a picker, or a diagnostics tool. [`FontDescription`](xref:SixLabors.Fonts.FontDescription) is the lightweight part of the API for that job. ### Read metadata without loading the font for layout -Use `FontDescription.LoadDescription(...)` when you only need descriptive information from a single font file or stream. +Use [`FontDescription.LoadDescription(...)`](xref:SixLabors.Fonts.FontDescription.LoadDescription*) when you only need descriptive information from a single font file or stream. ```csharp using System.Globalization; @@ -23,7 +23,7 @@ This is a better fit than `FontCollection.Add(...)` when you are building font p ### Work with localized names -`FontDescription` exposes both invariant and culture-aware name accessors: +[`FontDescription`](xref:SixLabors.Fonts.FontDescription) exposes both invariant and culture-aware name accessors: - `FontNameInvariantCulture` - `FontFamilyInvariantCulture` @@ -44,7 +44,7 @@ string familyName = description.FontFamily(english); ### Read additional name-table entries -Use `GetNameById(...)` with `KnownNameIds` when you need more than the basic family and subfamily fields. +Use [`GetNameById(...)`](xref:SixLabors.Fonts.FontDescription.GetNameById*) with [`KnownNameIds`](xref:SixLabors.Fonts.WellKnownIds.KnownNameIds) when you need more than the basic family and subfamily fields. Common values include: @@ -69,25 +69,26 @@ string sample = description.GetNameById(CultureInfo.InvariantCulture, KnownNameI ### Inspect font collections -Use `FontDescription.LoadFontCollectionDescriptions(...)` when a file contains multiple faces, such as a `.ttc` collection. +Use [`FontDescription.LoadFontCollectionDescriptions(...)`](xref:SixLabors.Fonts.FontDescription.LoadFontCollectionDescriptions*) when a file contains multiple faces, such as a `.ttc` collection. ```csharp +using System; using SixLabors.Fonts; -FontDescription[] descriptions = +ReadOnlyMemory descriptions = FontDescription.LoadFontCollectionDescriptions("fonts/NotoSansCJK-Regular.ttc"); ``` -If you are loading a collection into a `FontCollection`, the `AddCollection(...)` overloads can also return the descriptions that were discovered during the load. +If you are loading a collection into a [`FontCollection`](xref:SixLabors.Fonts.FontCollection), the [`AddCollection(...)`](xref:SixLabors.Fonts.FontCollection.AddCollection*) overloads can also return the descriptions that were discovered during the load. ### Inspect loaded families and fonts Once a family has been loaded, there are a few additional inspection helpers worth knowing about: -- `FontFamily.GetAvailableStyles()` lists the styles currently available for that family in the collection -- `FontFamily.TryGetPaths(...)` returns source file paths when the family came from filesystem-backed fonts -- `Font.TryGetPath(...)` returns the backing file path for a concrete font instance when one exists -- `Font.FontMetrics.Description` exposes the same `FontDescription` for the resolved face +- [`FontFamily.GetAvailableStyles()`](xref:SixLabors.Fonts.FontFamily.GetAvailableStyles*) lists the styles currently available for that family in the collection +- [`FontFamily.TryGetPaths(...)`](xref:SixLabors.Fonts.FontFamily.TryGetPaths*) returns source file paths when the family came from filesystem-backed fonts +- [`Font.TryGetPath(...)`](xref:SixLabors.Fonts.Font.TryGetPath*) returns the backing file path for a concrete font instance when one exists +- [`Font.FontMetrics.Description`](xref:SixLabors.Fonts.FontMetrics.Description) exposes the same [`FontDescription`](xref:SixLabors.Fonts.FontDescription) for the resolved face ```csharp using System; @@ -96,7 +97,7 @@ using SixLabors.Fonts; FontCollection collection = new(); FontFamily family = collection.Add("fonts/SourceSans3-Regular.ttf"); -foreach (FontStyle style in family.GetAvailableStyles()) +foreach (FontStyle style in family.GetAvailableStyles().Span) { Console.WriteLine(style); } @@ -107,7 +108,7 @@ FontDescription description = font.FontMetrics.Description; ### What `Style` means -`FontDescription.Style` is the resolved `FontStyle` for that face. Fonts derives it from the face metadata in the font tables, so it is a useful quick check when you want to know whether a face is marked as bold, italic, or both. +[`FontDescription.Style`](xref:SixLabors.Fonts.FontDescription.Style) is the resolved [`FontStyle`](xref:SixLabors.Fonts.FontStyle) for that face. Fonts derives it from the face metadata in the font tables, so it is a useful quick check when you want to know whether a face is marked as bold, italic, or both. For loading fonts into collections, see [Loading Fonts and Collections](gettingstarted.md). For working with installed machine fonts, see [System Fonts](systemfonts.md). diff --git a/articles/fonts/fontmetrics.md b/articles/fonts/fontmetrics.md index 5ba84c425..06def22b5 100644 --- a/articles/fonts/fontmetrics.md +++ b/articles/fonts/fontmetrics.md @@ -1,8 +1,8 @@ # Font Metrics -`FontDescription` tells you what a face is called. `FontMetrics` tells you how that face behaves. +[`FontDescription`](xref:SixLabors.Fonts.FontDescription) tells you what a face is called. [`FontMetrics`](xref:SixLabors.Fonts.FontMetrics) tells you how that face behaves. -Once you know what a font is, the next question is usually how it behaves. `FontMetrics` is where you inspect the measurements and coverage data that explain line spacing, decoration placement, variation support, and glyph availability. +Once you know what a font is, the next question is usually how it behaves. [`FontMetrics`](xref:SixLabors.Fonts.FontMetrics) is where you inspect the measurements and coverage data that explain line spacing, decoration placement, variation support, and glyph availability. ### How to get `FontMetrics` @@ -38,15 +38,15 @@ if (family.TryGetMetrics(FontStyle.Regular, out FontMetrics? metrics)) The core identity and scaling properties are: -- `Description` for the face metadata -- `UnitsPerEm` for the design-space resolution of the font -- `ScaleFactor` for the face-level unit-to-point scaling used by glyph metrics +- [`Description`](xref:SixLabors.Fonts.FontMetrics.Description) for the face metadata +- [`UnitsPerEm`](xref:SixLabors.Fonts.FontMetrics.UnitsPerEm) for the design-space resolution of the font +- [`ScaleFactor`](xref:SixLabors.Fonts.FontMetrics.ScaleFactor) for the face-level unit-to-point scaling used by glyph metrics `UnitsPerEm` is the important anchor for understanding almost every other metric on the typeface. Values like ascenders, underline positions, or glyph advances are stored in font units and should be interpreted relative to that em square. ### Horizontal and vertical metrics -`FontMetrics` exposes both `HorizontalMetrics` and `VerticalMetrics`. +[`FontMetrics`](xref:SixLabors.Fonts.FontMetrics) exposes both [`HorizontalMetrics`](xref:SixLabors.Fonts.FontMetrics.HorizontalMetrics) and [`VerticalMetrics`](xref:SixLabors.Fonts.FontMetrics.VerticalMetrics). Both headers provide the same core fields: @@ -124,7 +124,7 @@ These values are expressed in font units, not pixels. ### Decoration and script-positioning metrics -`FontMetrics` also exposes the face-level metrics that support decoration and typographic adjustments: +[`FontMetrics`](xref:SixLabors.Fonts.FontMetrics) also exposes the face-level metrics that support decoration and typographic adjustments: - `UnderlinePosition` - `UnderlineThickness` @@ -144,7 +144,7 @@ These are useful when you are building your own renderer, diagnostics, or typogr ### Variable-font support -`FontMetrics.TryGetVariationAxes(...)` lets you inspect the variation axes that the resolved face supports. +[`FontMetrics.TryGetVariationAxes(...)`](xref:SixLabors.Fonts.FontMetrics.TryGetVariationAxes*) lets you inspect the variation axes that the resolved face supports. ```csharp using System; @@ -155,16 +155,16 @@ FontCollection collection = new(); FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); Font font = family.CreateFont(16); -if (font.FontMetrics.TryGetVariationAxes(out VariationAxis[]? axes)) +if (font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)) { - foreach (VariationAxis axis in axes) + foreach (VariationAxis axis in axes.Span) { Console.WriteLine($"{axis.Tag}: {axis.Min}..{axis.Max} (default {axis.Default})"); } } ``` -Each `VariationAxis` gives you: +Each [`VariationAxis`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.Variations.VariationAxis) gives you: - `Name` - `Tag` @@ -172,26 +172,26 @@ Each `VariationAxis` gives you: - `Max` - `Default` -The registered tags in `KnownVariationAxes` such as `wght`, `wdth`, `opsz`, `slnt`, and `ital` are useful when you want to relate those exposed axes back to font creation with `FontVariation`. +The registered tags in [`KnownVariationAxes`](xref:SixLabors.Fonts.KnownVariationAxes) such as `wght`, `wdth`, `opsz`, `slnt`, and `ital` are useful when you want to relate those exposed axes back to font creation with [`FontVariation`](xref:SixLabors.Fonts.FontVariation). ### Code-point coverage -Use `GetAvailableCodePoints()` when you need to know which Unicode scalar values the face can map directly. +Use [`GetAvailableCodePoints()`](xref:SixLabors.Fonts.FontMetrics.GetAvailableCodePoints*) when you need to know which Unicode scalar values the face can map directly. ```csharp -using System.Collections.Generic; +using System; using SixLabors.Fonts; using SixLabors.Fonts.Unicode; Font font = SystemFonts.CreateFont("Segoe UI", 16); -IReadOnlyList codePoints = font.FontMetrics.GetAvailableCodePoints(); +ReadOnlyMemory codePoints = font.FontMetrics.GetAvailableCodePoints(); ``` This is useful for diagnostics, glyph coverage tooling, fallback decisions, and script-support inspection. ### Inspect glyph metrics directly -If you need glyph-level inspection without going through full text layout, use `TryGetGlyphMetrics(...)`. +If you need glyph-level inspection without going through full text layout, use [`TryGetGlyphMetrics(...)`](xref:SixLabors.Fonts.FontMetrics.TryGetGlyphMetrics*). ```csharp using SixLabors.Fonts; @@ -205,7 +205,7 @@ if (font.FontMetrics.TryGetGlyphMetrics( TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out GlyphMetrics? glyphMetrics)) + out FontGlyphMetrics? glyphMetrics)) { float width = glyphMetrics.Width; ushort advance = glyphMetrics.AdvanceWidth; @@ -213,13 +213,13 @@ if (font.FontMetrics.TryGetGlyphMetrics( } ``` -This is the lower-level face inspection API behind the higher-level `Font.TryGetGlyphs(...)` helpers. +This is the lower-level face inspection API behind the higher-level [`Font.TryGetGlyphs(...)`](xref:SixLabors.Fonts.Font.TryGetGlyphs*) helpers. ### When to use `FontMetrics` vs `FontDescription` -Use `FontDescription` when you care about names and face identity. +Use [`FontDescription`](xref:SixLabors.Fonts.FontDescription) when you care about names and face identity. -Use `FontMetrics` when you care about: +Use [`FontMetrics`](xref:SixLabors.Fonts.FontMetrics) when you care about: - line and em metrics - underline and strikeout placement diff --git a/articles/fonts/gettingstarted.md b/articles/fonts/gettingstarted.md index 2af6a92a4..a0621b52a 100644 --- a/articles/fonts/gettingstarted.md +++ b/articles/fonts/gettingstarted.md @@ -4,14 +4,14 @@ The quickest way to get comfortable with Fonts is to separate three ideas: where The main types you will meet first are: -- `FontCollection` stores the families you load. -- `FontFamily` represents a family and the styles available for it. -- `Font` represents a concrete instance of a family at a given point size, style, and optional variation settings. -- `SystemFonts` gives you access to the fonts installed on the current machine. +- [`FontCollection`](xref:SixLabors.Fonts.FontCollection) stores the families you load. +- [`FontFamily`](xref:SixLabors.Fonts.FontFamily) represents a family and the styles available for it. +- [`Font`](xref:SixLabors.Fonts.Font) represents a concrete instance of a family at a given point size, style, and optional variation settings. +- [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts) gives you access to the fonts installed on the current machine. ### Load a single font -Use `FontCollection.Add(...)` when you want to register an individual font file such as a `.ttf`, `.otf`, `.woff`, or `.woff2`. +Use [`FontCollection.Add(...)`](xref:SixLabors.Fonts.FontCollection.Add*) when you want to register an individual font file such as a `.ttf`, `.otf`, `.woff`, or `.woff2`. ```csharp using SixLabors.Fonts; @@ -21,7 +21,7 @@ FontFamily family = collection.Add("fonts/SourceSans3-Regular.ttf"); Font font = family.CreateFont(16, FontStyle.Regular); ``` -`Font.Size` is expressed in points. Measurement and rendering are then converted to pixels using `TextOptions.Dpi`. +[`Font.Size`](xref:SixLabors.Fonts.Font.Size) is expressed in points. Measurement and rendering are then converted to pixels using [`TextOptions.Dpi`](xref:SixLabors.Fonts.TextOptions.Dpi). ### Load from a stream and inspect metadata @@ -40,11 +40,11 @@ string familyName = description.FontFamilyInvariantCulture; Font font = family.CreateFont(16); ``` -If you only need metadata, use `FontDescription.LoadDescription(...)` or `FontDescription.LoadFontCollectionDescriptions(...)` instead of adding the font to a collection. See [Font Metadata and Inspection](fontmetadata.md) for more detail. +If you only need metadata, use [`FontDescription.LoadDescription(...)`](xref:SixLabors.Fonts.FontDescription.LoadDescription*) or [`FontDescription.LoadFontCollectionDescriptions(...)`](xref:SixLabors.Fonts.FontDescription.LoadFontCollectionDescriptions*) instead of adding the font to a collection. See [Font Metadata and Inspection](fontmetadata.md) for more detail. ### Load a font collection -Use `AddCollection(...)` for files that contain multiple faces, such as `.ttc` collections. +Use [`AddCollection(...)`](xref:SixLabors.Fonts.FontCollection.AddCollection*) for files that contain multiple faces, such as `.ttc` collections. ```csharp using SixLabors.Fonts; @@ -55,7 +55,7 @@ var families = collection.AddCollection("fonts/NotoSansCJK-Regular.ttc"); ### Resolve families by name -Once fonts are loaded, resolve a family with `Get(...)` or `TryGet(...)`. +Once fonts are loaded, resolve a family with [`Get(...)`](xref:SixLabors.Fonts.FontCollection.Get*) or [`TryGet(...)`](xref:SixLabors.Fonts.FontCollection.TryGet*). ```csharp using SixLabors.Fonts; @@ -69,16 +69,16 @@ if (collection.TryGet("Source Sans 3", out FontFamily textFamily) && { TextOptions options = new(textFamily.CreateFont(16)) { - FallbackFontFamilies = new[] { emojiFamily } + FallbackFontFamilies = [emojiFamily] }; } ``` -`FallbackFontFamilies` is a list of `FontFamily` instances, not `Font` instances. Fonts are created after the fallback family is selected for a run. +[`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) is a list of [`FontFamily`](xref:SixLabors.Fonts.FontFamily) instances, not [`Font`](xref:SixLabors.Fonts.Font) instances. Fonts are created after the fallback family is selected for a run. ### Use system fonts -If you want to work with fonts installed on the current machine, use `SystemFonts`. +If you want to work with fonts installed on the current machine, use [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts). ```csharp using SixLabors.Fonts; @@ -99,13 +99,13 @@ collection.AddSystemFonts(); collection.Add("fonts/BrandSans-Regular.ttf"); ``` -When you need localized family-name lookup, use `AddWithCulture(...)`, `GetByCulture(...)`, or `TryGetByCulture(...)`. +When you need localized family-name lookup, use [`AddWithCulture(...)`](xref:SixLabors.Fonts.FontCollection.AddWithCulture*), [`GetByCulture(...)`](xref:SixLabors.Fonts.FontCollection.GetByCulture*), or [`TryGetByCulture(...)`](xref:SixLabors.Fonts.FontCollection.TryGetByCulture*). See [System Fonts](systemfonts.md) for the fuller system-font API surface, including enumeration, culture-aware lookup, and `SearchDirectories`. ### Create variable-font instances -Variable fonts are exposed through `FontVariation` and `KnownVariationAxes`. +Variable fonts are exposed through [`FontVariation`](xref:SixLabors.Fonts.FontVariation) and [`KnownVariationAxes`](xref:SixLabors.Fonts.KnownVariationAxes). ```csharp using SixLabors.Fonts; @@ -119,7 +119,7 @@ Font font = family.CreateFont( new FontVariation(KnownVariationAxes.OpticalSize, 16)); ``` -The active variation values become part of the `Font` instance, so the same family can be reused to create multiple design-space instances. +The active variation values become part of the [`Font`](xref:SixLabors.Fonts.Font) instance, so the same family can be reused to create multiple design-space instances. ### Next steps diff --git a/articles/fonts/hintingandshaping.md b/articles/fonts/hintingandshaping.md index f2584ef0e..a18cfd477 100644 --- a/articles/fonts/hintingandshaping.md +++ b/articles/fonts/hintingandshaping.md @@ -18,7 +18,7 @@ Fonts has comprehensive support for both, but the scope is different: | Input | Unicode text, script, direction, font selection, OpenType features | A concrete glyph outline, size, and DPI | | Output | The final glyph sequence and glyph positions | A grid-fitted outline for raster-oriented rendering | | Main goal | Correct text layout and glyph choice | Better small-size screen rendering | -| Controlled by | `TextDirection`, `FeatureTags`, `KerningMode`, `Tracking`, `TextRuns`, `FallbackFontFamilies`, `LayoutMode` | `HintingMode` | +| Controlled by | [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection), [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags), [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode), [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking), [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns), [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies), [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) | [`HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) | ### What shaping means @@ -50,21 +50,21 @@ That shaping support includes: - kerning, ligatures, fractions, tabular figures, vertical alternates, and other OpenType feature-driven behaviors - font fallback and per-range font selection through `FallbackFontFamilies` and `TextRuns` -This is why measurement and rendering stay aligned when you use the same `TextOptions` instance for both. Fonts measures shaped text, not a simplified pre-layout approximation. +This is why measurement and rendering stay aligned when you use the same [`TextOptions`](xref:SixLabors.Fonts.TextOptions) instance for both. Fonts measures shaped text, not a simplified pre-layout approximation. ### What you control in shaping The main shaping controls are: -- `TextDirection` to force left-to-right, right-to-left, or automatic bidi resolution -- `LayoutMode` for horizontal and vertical layout behavior -- `FeatureTags` to request additional OpenType features such as fractions or tabular figures -- `KerningMode` to enable or disable font-provided kerning during shaping -- `Tracking` to add uniform letter spacing after the font's own spacing behavior -- `FallbackFontFamilies` when the main font does not cover every glyph you need -- `TextRuns` when different text ranges need different fonts, attributes, or decorations +- [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection) to force left-to-right, right-to-left, or automatic bidi resolution +- [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) for horizontal and vertical layout behavior +- [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) to request additional OpenType features such as fractions or tabular figures +- [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode) to enable or disable font-provided kerning during shaping +- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) to add uniform letter spacing after the font's own spacing behavior +- [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) when the main font does not cover every glyph you need +- [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) when different text ranges need different fonts, attributes, or decorations -Required script shaping still happens automatically. `FeatureTags` is for extra typographic features you want to request on top of that baseline shaping behavior. +Required script shaping still happens automatically. [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) is for extra typographic features you want to request on top of that baseline shaping behavior. ```csharp using SixLabors.Fonts; @@ -75,11 +75,11 @@ TextOptions options = new(font) { TextDirection = TextDirection.Auto, KerningMode = KerningMode.Standard, - FeatureTags = new Tag[] - { + FeatureTags = + [ KnownFeatureTags.Fractions, KnownFeatureTags.TabularFigures - } + ] }; FontRectangle bounds = TextMeasurer.MeasureAdvance("9/2", options); @@ -103,7 +103,7 @@ At larger sizes the difference is usually much smaller because the outline alrea Within the scope of TrueType outlines, Fonts has comprehensive hinting support. -`TextOptions.HintingMode` controls whether that hinting path is active: +[`TextOptions.HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) controls whether that hinting path is active: - `HintingMode.None` leaves outlines unhinted - `HintingMode.Standard` applies the library's FreeType v40-compatible TrueType hinting behavior @@ -118,7 +118,7 @@ The hinting pipeline in Fonts includes: - `cvar`-driven control-value adjustments for variable TrueType fonts before hinting runs - hinted contour-point resolution for GPOS anchor data when the font uses contour-point anchors -This is specifically a TrueType feature. Fonts only applies this hinting path to TrueType glyph data, so CFF and CFF2 outlines do not gain extra hinting behavior from `HintingMode.Standard`. +This is specifically a TrueType feature. Fonts only applies this hinting path to TrueType glyph data, so CFF and CFF2 outlines do not gain extra hinting behavior from [`HintingMode.Standard`](xref:SixLabors.Fonts.HintingMode.Standard). ```csharp using SixLabors.Fonts; @@ -150,10 +150,10 @@ Shaping does not grid-fit outlines. It decides the typographic result that hinti ### Practical guidance -- Use `TextDirection.Auto` unless you have a specific reason to force directionality. -- Use `FallbackFontFamilies` for multilingual text, emoji, and scripts your main font does not cover. -- Use `FeatureTags` for discretionary features such as fractions, stylistic sets, or tabular figures. -- Use `HintingMode.Standard` when rendering small TrueType UI text and leave it off when you want the raw outline behavior. +- Use [`TextDirection.Auto`](xref:SixLabors.Fonts.TextDirection.Auto) unless you have a specific reason to force directionality. +- Use [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) for multilingual text, emoji, and scripts your main font does not cover. +- Use [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) for discretionary features such as fractions, stylistic sets, or tabular figures. +- Use [`HintingMode.Standard`](xref:SixLabors.Fonts.HintingMode.Standard) when rendering small TrueType UI text and leave it off when you want the raw outline behavior. - Treat shaping as a typography and layout concern. - Treat hinting as a size-dependent TrueType raster-quality concern. diff --git a/articles/fonts/index.md b/articles/fonts/index.md index 7876073a7..3970d5732 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -1,7 +1,7 @@ # Introduction ### What is Fonts? -Fonts is the part of the Six Labors stack that handles font loading, text measurement, layout, shaping, and custom text rendering. +Fonts is the high-performance part of the Six Labors stack that handles font loading, text measurement, layout, shaping, and custom text rendering. If you are new to the library, the easiest way to think about it is in layers: load families, create concrete `Font` instances, then measure or render text with `TextOptions`. The rest of this section is organized around that path so you can start simple and move into shaping, Unicode, fallback, and custom rendering only when you need them. @@ -57,6 +57,9 @@ If you are new to Fonts, start with [Loading Fonts and Collections](gettingstart - [Font Metadata and Inspection](fontmetadata.md) - [Font Metrics](fontmetrics.md) - [Measuring Text](measuringtext.md) +- [Prepared Text with TextBlock](textblock.md) +- [Hit Testing and Caret Movement](texthittesting.md) +- [Selection and Bidi Drag](caretsandselection.md) - [Text Layout and Options](textlayout.md) - [OpenType Features](opentypefeatures.md) - [Hinting and Shaping](hintingandshaping.md) diff --git a/articles/fonts/inspectfontfiles.md b/articles/fonts/inspectfontfiles.md index fad976bd9..5b14cfe61 100644 --- a/articles/fonts/inspectfontfiles.md +++ b/articles/fonts/inspectfontfiles.md @@ -1,6 +1,6 @@ # Inspect Font Files and Collections -This recipe is a good starting point when you have a font file in hand and want to learn what it contains before you add it to your app's normal font collection. +This recipe is a good starting point when you have a font file in hand and want to learn what it contains before you add it to your app's normal font collection with [`FontDescription`](xref:SixLabors.Fonts.FontDescription). ### Read a single font file @@ -25,15 +25,15 @@ This is useful for import tools, font pickers, diagnostics, and file-inspection using System; using SixLabors.Fonts; -FontDescription[] descriptions = +ReadOnlyMemory descriptions = FontDescription.LoadFontCollectionDescriptions("fonts/NotoSansCJK-Regular.ttc"); -foreach (FontDescription description in descriptions) +foreach (FontDescription description in descriptions.Span) { Console.WriteLine(description.FontNameInvariantCulture); } ``` -If you do want to load the collection afterward, use `FontCollection.AddCollection(...)`. +If you do want to load the collection afterward, use [`FontCollection.AddCollection(...)`](xref:SixLabors.Fonts.FontCollection.AddCollection*). For the broader metadata API, see [Font Metadata and Inspection](fontmetadata.md). diff --git a/articles/fonts/listsystemfonts.md b/articles/fonts/listsystemfonts.md index 829d55998..6b4e51079 100644 --- a/articles/fonts/listsystemfonts.md +++ b/articles/fonts/listsystemfonts.md @@ -1,6 +1,6 @@ # List System Fonts and Resolve by Culture -This recipe is useful when you want a quick picture of what the current machine can actually provide through `SystemFonts`, whether for diagnostics, UI pickers, or culture-aware name resolution. +This recipe is useful when you want a quick picture of what the current machine can actually provide through [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts), whether for diagnostics, UI pickers, or culture-aware name resolution. ### List installed families diff --git a/articles/fonts/measuringtext.md b/articles/fonts/measuringtext.md index ddd47643b..284aa0d5e 100644 --- a/articles/fonts/measuringtext.md +++ b/articles/fonts/measuringtext.md @@ -1,13 +1,18 @@ # Measuring Text -Measurement is often the point where text layout stops being abstract and starts affecting a real UI. `TextMeasurer` lets you run the same shaping and layout engine that rendering uses, which means you can decide widths, line breaks, placements, and bounds before anything is drawn. +Measurement is often the point where text layout stops being abstract and starts affecting a real UI. [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) lets you run the same shaping and layout engine that rendering uses, which means you can decide widths, line breaks, placements, and bounds before anything is drawn. + +The measurement APIs come in three layers: + +- [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer): one-shot convenience methods for measuring a string. Best for ad-hoc work. +- [`TextBlock`](xref:SixLabors.Fonts.TextBlock): prepares a string once, then measures or renders it repeatedly at different wrapping lengths. See [Prepared Text with TextBlock](textblock.md). +- [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics): the full measurement object returned by [`TextMeasurer.Measure(...)`](xref:SixLabors.Fonts.TextMeasurer.Measure*) or [`TextBlock.Measure(...)`](xref:SixLabors.Fonts.TextBlock.Measure*). Keep this when callers need several measurements, hit testing, carets, or selection geometry from the same laid-out text. ### Choose the right measurement -- `MeasureAdvance(...)` returns the logical advance rectangle from layout, including line height and advance. -- `MeasureBounds(...)` returns only the tight rendered glyph ink bounds. -- `MeasureRenderableBounds(...)` returns the union of the logical advance rectangle and the glyph ink bounds. -- `MeasureSize(...)` returns the rendered width and height normalized to `(0, 0)`. +- [`MeasureAdvance(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureAdvance*) returns the logical advance rectangle from layout, including line height and advance. +- [`MeasureBounds(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureBounds*) returns only the tight rendered glyph ink bounds. +- [`MeasureRenderableBounds(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureRenderableBounds*) returns the union of the logical advance rectangle and the glyph ink bounds. The important distinction is that glyph geometry and layout geometry are not the same thing. Glyphs can overshoot the logical advance box, and the logical advance box can also include space that no glyph pixels occupy. @@ -25,7 +30,6 @@ TextOptions options = new(font) FontRectangle advance = TextMeasurer.MeasureAdvance("Hello world", options); FontRectangle bounds = TextMeasurer.MeasureBounds("Hello world", options); FontRectangle renderable = TextMeasurer.MeasureRenderableBounds("Hello world", options); -FontRectangle size = TextMeasurer.MeasureSize("Hello world", options); ``` Replace `"Segoe UI"` with any installed family that exists on your machine. @@ -42,43 +46,41 @@ Use `MeasureRenderableBounds(...)` when you need the full rendered area that com `MeasureRenderableBounds(...)` returns a larger conceptual rectangle when needed: it includes the full logical advance rectangle from layout and then expands that rectangle to also include any glyph ink that extends beyond it. -`MeasureSize(...)` is the rendered glyph-bounds measurement normalized to width and height only. - If you need a rectangle that can safely contain both the typographic layout box and any glyph overshoot, prefer `MeasureRenderableBounds(...)`. -### Measure per-character entries +### Measure per-entry data -Fonts can also expose measurements for each laid-out entry. +`TextMeasurer` exposes three per-entry collections. Each answers a different layout question and is independent of the others. ```csharp using System; using SixLabors.Fonts; Font font = SystemFonts.CreateFont("Segoe UI", 18); -TextOptions options = new(font); - -if (TextMeasurer.TryMeasureCharacterBounds("Hello", options, out ReadOnlySpan bounds)) +TextOptions options = new(font) { - GlyphBounds first = bounds[0]; -} + WrappingLength = 320 +}; + +ReadOnlyMemory graphemes = TextMeasurer.GetGraphemeMetrics("Hello world", options); +ReadOnlyMemory words = TextMeasurer.GetWordMetrics("Hello world", options); +ReadOnlyMemory glyphs = TextMeasurer.GetGlyphMetrics("Hello world", options); ``` +- [`GraphemeMetrics`](xref:SixLabors.Fonts.GraphemeMetrics) is the unit for text interaction: hit testing, caret positioning, range selection, and UI overlays. Use [`Advance`](xref:SixLabors.Fonts.GraphemeMetrics.Advance) for hit targets and selection geometry; [`Bounds`](xref:SixLabors.Fonts.GraphemeMetrics.Bounds) is the rendered ink only and can be empty or overhang. +- [`WordMetrics`](xref:SixLabors.Fonts.WordMetrics) describes one Unicode word-boundary segment from UAX #29, including separators. [`GraphemeStart`](xref:SixLabors.Fonts.WordMetrics.GraphemeStart) and [`StringStart`](xref:SixLabors.Fonts.WordMetrics.StringStart) are inclusive; [`GraphemeEnd`](xref:SixLabors.Fonts.WordMetrics.GraphemeEnd) and [`StringEnd`](xref:SixLabors.Fonts.WordMetrics.StringEnd) are exclusive. +- [`GlyphMetrics`](xref:SixLabors.Fonts.GlyphMetrics) exposes laid-out glyph entries for rendering diagnostics or glyph-level visualization. Do not use them as character or caret positions: ligatures, decomposition, fallback, emoji, and combining marks mean one grapheme can map to multiple glyph entries. + These APIs measure laid-out output, not raw UTF-16 code units, so do not assume a one-to-one mapping with the original string in the presence of shaping, ligatures, or complex scripts. If you need a refresher on the difference between UTF-16 code units, `CodePoint` values, and graphemes, see [Unicode, Code Points, and Graphemes](unicode.md). -Available per-entry methods include: - -- `TryMeasureCharacterAdvances(...)` -- `TryMeasureCharacterSizes(...)` -- `TryMeasureCharacterBounds(...)` -- `TryMeasureCharacterRenderableBounds(...)` - ### Measure lines -When you care about wrapped text, use `CountLines(...)` and `GetLineMetrics(...)`. +When you care about wrapped text, use [`CountLines(...)`](xref:SixLabors.Fonts.TextMeasurer.CountLines*) and [`GetLineMetrics(...)`](xref:SixLabors.Fonts.TextMeasurer.GetLineMetrics*). ```csharp +using System; using SixLabors.Fonts; Font font = SystemFonts.CreateFont("Segoe UI", 18); @@ -88,20 +90,62 @@ TextOptions options = new(font) }; int lineCount = TextMeasurer.CountLines("Hello world from Fonts", options); -LineMetrics[] lines = TextMeasurer.GetLineMetrics("Hello world from Fonts", options); +ReadOnlyMemory lines = TextMeasurer.GetLineMetrics("Hello world from Fonts", options); ``` -Each `LineMetrics` entry includes: +Each [`LineMetrics`](xref:SixLabors.Fonts.LineMetrics) entry includes: - `Ascender`: the ascender guide position within the line box. This marks where tall glyphs such as `H` or `l` typically rise to. - `Baseline`: the baseline position within the line box. This is the line most glyphs sit on. - `Descender`: the descender guide position within the line box. This marks where descending glyph parts such as `g`, `p`, or `y` typically fall to. - `LineHeight`: the total height of the line box after line spacing has been applied. -- `Start`: the aligned start position of the line in the primary flow direction. -- `Extent`: the size of the line in the primary flow direction. +- `Start`: the positioned line-box origin in pixel units. +- `Extent`: the positioned line-box size in pixel units. +- `StringIndex`, `GraphemeIndex`, `GraphemeCount`: the source-text range owned by the line. `GraphemeCount` is not a glyph count. + +`Start` and `Extent` are full `Vector2` values. Selection and caret APIs use the line box for the cross-axis size, which matches normal text editor and browser behavior: selecting mixed font sizes on the same line paints a consistent line-height rectangle rather than one rectangle per glyph height. + +### Capture the full measurement with `TextMetrics` -In horizontal layouts, `Start` is the X position and `Extent` is the line width. In vertical layouts, `Start` is the Y position and `Extent` is the line height. +When a single layout pass needs to feed several questions — overall size, per-line metrics, per-grapheme positions, hit testing, carets, and selection — measure once and keep the returned [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics). + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 320 +}; + +TextMetrics metrics = TextMeasurer.Measure("Hello world", options); + +FontRectangle advance = metrics.Advance; +FontRectangle bounds = metrics.Bounds; +FontRectangle renderable = metrics.RenderableBounds; +int lineCount = metrics.LineCount; + +ReadOnlySpan lines = metrics.LineMetrics; +ReadOnlySpan graphemes = metrics.GraphemeMetrics; +ReadOnlySpan words = metrics.WordMetrics; +``` + +Line and grapheme collections are in final layout order; for bidi text and reverse line-order layout modes, that can differ from source order. Word collections are in source order because word-boundary navigation is a logical operation. + +[`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) returns the per-entry collections as `ReadOnlySpan` because the metrics object owns their lifetime. The [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) and [`TextBlock`](xref:SixLabors.Fonts.TextBlock) methods return `ReadOnlyMemory` because those snapshots can be stored alongside other layout state. Use `.Span` when drawing. + +The same object exposes interaction APIs: + +```csharp +TextHit hit = metrics.HitTest(point); +CaretPosition caret = metrics.GetCaretPosition(hit); +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +See [Hit Testing and Caret Movement](texthittesting.md) and [Selection and Bidi Drag](caretsandselection.md) for the full editor-style interaction surface. ### Keep measurement and rendering aligned Always measure with the same `TextOptions` that you intend to render with. `Dpi`, `LineSpacing`, `WrappingLength`, `TextDirection`, `LayoutMode`, `KerningMode`, `Tracking`, `FeatureTags`, `TextRuns`, and fallback fonts all affect the final layout. + +For repeated measurement of the same string at different wrapping lengths, prefer [`TextBlock`](xref:SixLabors.Fonts.TextBlock) over calling [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) multiple times — it shapes the text once and varies wrapping per call. diff --git a/articles/fonts/opentypefeatures.md b/articles/fonts/opentypefeatures.md index 66237c573..0b98c3b6d 100644 --- a/articles/fonts/opentypefeatures.md +++ b/articles/fonts/opentypefeatures.md @@ -1,17 +1,17 @@ # OpenType Features -Fonts already applies the OpenType features that are required for correct shaping and layout. `TextOptions.FeatureTags` is where you ask for the extra typographic touches a font may support, such as tabular figures, fractions, stylistic alternates, or discretionary ligatures. +Fonts already applies the OpenType features that are required for correct shaping and layout. [`TextOptions.FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) is where you ask for the extra typographic touches a font may support, such as tabular figures, fractions, stylistic alternates, or discretionary ligatures. That makes it a typography control, not a substitute for the shaping engine. ### How `FeatureTags` works -`TextOptions.FeatureTags` is an `IReadOnlyList`. +[`TextOptions.FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) is an `IReadOnlyList`. You can populate it with: -- named values from `KnownFeatureTags` -- raw four-character tags parsed with `Tag.Parse(...)` +- named values from [`KnownFeatureTags`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.KnownFeatureTags) +- raw four-character tags parsed with [`Tag.Parse(...)`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.Tag.Parse*) ```csharp using SixLabors.Fonts; @@ -20,12 +20,15 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] - { + FeatureTags = + [ KnownFeatureTags.Fractions, KnownFeatureTags.TabularFigures, + + // 'ss01' is the first of OpenType's stylistic sets (ss01..ss20), + // which a font can use to expose an alternate glyph design. Tag.Parse("ss01") - } + ] }; ``` @@ -44,7 +47,7 @@ Use explicit feature tags for discretionary typographic behavior such as: - case-sensitive punctuation - vertical alternates -Do not think of `FeatureTags` as a way to manually replace the shaping engine. Core script shaping, bidi handling, and other required layout behavior are already handled by Fonts. +Do not think of [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) as a way to manually replace the shaping engine. Core script shaping, bidi handling, and other required layout behavior are already handled by Fonts. ### Common feature examples @@ -55,9 +58,9 @@ using SixLabors.Fonts; using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); - TextOptions options = new(font) +TextOptions options = new(font) { - FeatureTags = new Tag[] { KnownFeatureTags.Fractions } + FeatureTags = [KnownFeatureTags.Fractions] }; ``` @@ -70,7 +73,7 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] { KnownFeatureTags.TabularFigures } + FeatureTags = [KnownFeatureTags.TabularFigures] }; ``` @@ -83,11 +86,11 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] - { + FeatureTags = + [ KnownFeatureTags.OldstyleFigures, KnownFeatureTags.DiscretionaryLigatures - } + ] }; ``` @@ -100,15 +103,18 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] { Tag.Parse("ss01") } + + // 'ss01' is the first of OpenType's stylistic sets (ss01..ss20), + // which a font can use to expose an alternate glyph design. + FeatureTags = [Tag.Parse("ss01")] }; ``` ### Named tags vs raw tags -Prefer the `KnownFeatureTags` enum when the feature already has a named constant in the library. Use `Tag.Parse(...)` for raw feature tags that you know exist in the target font but that you want to specify directly in your code. +Prefer the [`KnownFeatureTags`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.KnownFeatureTags) enum when the feature already has a named constant in the library. Use [`Tag.Parse(...)`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.Tag.Parse*) for raw feature tags that you know exist in the target font but that you want to specify directly in your code. -`Tag.Parse(...)` expects a four-character tag such as `"liga"`, `"frac"`, or `"ss01"`. +[`Tag.Parse(...)`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.Tag.Parse*) expects a four-character tag such as `"liga"`, `"frac"`, or `"ss01"`. ### Feature tags and layout @@ -116,8 +122,8 @@ Feature requests participate in shaping, so they affect both measurement and ren ### Vertical layout -Some OpenType features are especially relevant in vertical layout, such as `KnownFeatureTags.VerticalAlternates`, `KnownFeatureTags.VerticalAlternatesAndRotation`, and `KnownFeatureTags.VerticalAlternatesForRotation`. +Some OpenType features are especially relevant in vertical layout, such as [`KnownFeatureTags.VerticalAlternates`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.KnownFeatureTags.VerticalAlternates), [`KnownFeatureTags.VerticalAlternatesAndRotation`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.KnownFeatureTags.VerticalAlternatesAndRotation), and [`KnownFeatureTags.VerticalAlternatesForRotation`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.KnownFeatureTags.VerticalAlternatesForRotation). -Those work alongside `LayoutMode`; they do not replace it. +Those work alongside [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode); they do not replace it. For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For the broader shaping pipeline, see [Hinting and Shaping](hintingandshaping.md). diff --git a/articles/fonts/systemfonts.md b/articles/fonts/systemfonts.md index ccdf99383..bdaa89a5e 100644 --- a/articles/fonts/systemfonts.md +++ b/articles/fonts/systemfonts.md @@ -1,17 +1,17 @@ # System Fonts -System fonts are convenient because they let you get moving without shipping font files yourself. They also come with the tradeoff that the available families depend on the machine you are running on, so this page treats portability as part of the topic rather than an afterthought. +[`SystemFonts`](xref:SixLabors.Fonts.SystemFonts) are convenient because they let you get moving without shipping font files yourself. They also come with the tradeoff that the available families depend on the machine you are running on, so this page treats portability as part of the topic rather than an afterthought. -Use it when you want to work with platform fonts directly instead of loading files into your own `FontCollection`. +Use it when you want to work with platform fonts directly instead of loading files into your own [`FontCollection`](xref:SixLabors.Fonts.FontCollection). ### What `SystemFonts` exposes The main entry points are: -- `SystemFonts.Families` to enumerate installed families -- `SystemFonts.Get(...)` and `SystemFonts.TryGet(...)` to resolve a family by invariant name -- `SystemFonts.CreateFont(...)` to create a `Font` directly -- `SystemFonts.Collection` when you also need access to the searched directories +- [`SystemFonts.Families`](xref:SixLabors.Fonts.SystemFonts.Families) to enumerate installed families +- [`SystemFonts.Get(...)`](xref:SixLabors.Fonts.SystemFonts.Get*) and [`SystemFonts.TryGet(...)`](xref:SixLabors.Fonts.SystemFonts.TryGet*) to resolve a family by invariant name +- [`SystemFonts.CreateFont(...)`](xref:SixLabors.Fonts.SystemFonts.CreateFont*) to create a [`Font`](xref:SixLabors.Fonts.Font) directly +- [`SystemFonts.Collection`](xref:SixLabors.Fonts.SystemFonts.Collection) when you also need access to the searched directories ```csharp using SixLabors.Fonts; @@ -24,7 +24,7 @@ Replace `"Segoe UI"` with any installed family that exists on your machine. ### Enumerate available families -Use `SystemFonts.Families` when you want to inspect what the current environment actually exposes. +Use [`SystemFonts.Families`](xref:SixLabors.Fonts.SystemFonts.Families) when you want to inspect what the current environment actually exposes. ```csharp using System; @@ -38,7 +38,7 @@ foreach (FontFamily family in SystemFonts.Families) ### Use culture-aware lookup -Font family names can vary by culture, so `SystemFonts` also exposes the same culture-aware lookup helpers as `FontCollection`. +Font family names can vary by culture, so [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts) also exposes the same culture-aware lookup helpers as [`FontCollection`](xref:SixLabors.Fonts.FontCollection). ```csharp using System.Globalization; @@ -52,11 +52,11 @@ if (SystemFonts.TryGetByCulture("Yu Gothic", japanese, out FontFamily family)) } ``` -You can also create a font directly with the culture-aware `CreateFont(...)` overloads. +You can also create a font directly with the culture-aware [`CreateFont(...)`](xref:SixLabors.Fonts.SystemFonts.CreateFont*) overloads. ### Merge system fonts into your own collection -If you want your own custom fonts and the machine fonts in one lookup surface, copy the system font set into a `FontCollection`. +If you want your own custom fonts and the machine fonts in one lookup surface, copy the system font set into a [`FontCollection`](xref:SixLabors.Fonts.FontCollection). ```csharp using SixLabors.Fonts; @@ -79,7 +79,7 @@ collection.AddSystemFonts(metric => ### Search directories -`SystemFonts.Collection` implements `IReadOnlySystemFontCollection`, which exposes `SearchDirectories`. +[`SystemFonts.Collection`](xref:SixLabors.Fonts.SystemFonts.Collection) implements [`IReadOnlySystemFontCollection`](xref:SixLabors.Fonts.IReadOnlySystemFontCollection), which exposes [`SearchDirectories`](xref:SixLabors.Fonts.IReadOnlySystemFontCollection.SearchDirectories). That is useful for diagnostics and for understanding where the current process looked for fonts. @@ -99,6 +99,6 @@ The available system fonts are environment-specific. - Windows, Linux, macOS, containers, and CI agents will often expose different families. - A family name that exists on your dev machine may not exist in production. -- If predictable output matters, prefer shipping the fonts you need and loading them into a `FontCollection`. +- If predictable output matters, prefer shipping the fonts you need and loading them into a [`FontCollection`](xref:SixLabors.Fonts.FontCollection). For file-based loading, see [Loading Fonts and Collections](gettingstarted.md). For metadata-only inspection, see [Font Metadata and Inspection](fontmetadata.md). diff --git a/articles/fonts/textblock.md b/articles/fonts/textblock.md new file mode 100644 index 000000000..b24f43cc5 --- /dev/null +++ b/articles/fonts/textblock.md @@ -0,0 +1,112 @@ +# Prepared Text with TextBlock + +[`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) is the shortest path from a string to a measurement, but every call shapes the text from scratch. [`TextBlock`](xref:SixLabors.Fonts.TextBlock) does the wrapping-independent work once, then lets you measure, render, and inspect the same text repeatedly at different wrapping lengths. + +Use [`TextBlock`](xref:SixLabors.Fonts.TextBlock) whenever the same string will be measured, wrapped, drawn, or inspected more than once: rich-text editors, layout panels that resize, anything that needs both a measurement pass and a render pass. + +### Construct once, vary the wrapping length + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + Origin = new System.Numerics.Vector2(20, 30) +}; + +TextBlock block = new("Hello, world!", options); + +TextMetrics narrow = block.Measure(240); +TextMetrics wide = block.Measure(480); +``` + +[`TextOptions.WrappingLength`](xref:SixLabors.Fonts.TextOptions.WrappingLength) is ignored by the constructor. Pass the wrapping length to each operation instead, and use `-1` to disable wrapping for that call. + +```csharp +TextMetrics unwrapped = block.Measure(-1); +``` + +### Detail APIs + +[`TextBlock`](xref:SixLabors.Fonts.TextBlock) exposes the same per-entry collections that [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) does, for callers that do not need the full measurement object: + +```csharp +using System; +using SixLabors.Fonts; + +ReadOnlyMemory lines = block.GetLineMetrics(320); +ReadOnlyMemory graphemes = block.GetGraphemeMetrics(320); +ReadOnlyMemory words = block.GetWordMetrics(320); +ReadOnlyMemory glyphs = block.GetGlyphMetrics(320); +``` + +Method-returned collections use `ReadOnlyMemory` because they are snapshots a caller may store with their own layout state. Owner-backed properties such as `TextMetrics.LineMetrics` and `LineLayout.GraphemeMetrics` use `ReadOnlySpan` because the owner already controls the lifetime. + +### Per-line layout + +When the UI needs line-local data, use [`GetLineLayouts(...)`](xref:SixLabors.Fonts.TextBlock.GetLineLayouts*) or [`EnumerateLineLayouts()`](xref:SixLabors.Fonts.TextBlock.EnumerateLineLayouts*). Both produce [`LineLayout`](xref:SixLabors.Fonts.LineLayout) instances that mirror the interaction surface of [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) for a single line — hit testing, caret positioning, caret movement, word lookup, and selection bounds — but they position those lines in different coordinate spaces. + +#### Block coordinates with `GetLineLayouts` + +[`GetLineLayouts(...)`](xref:SixLabors.Fonts.TextBlock.GetLineLayouts*) lays out the whole block as one unit. Lines stack in their natural flow direction starting from [`TextOptions.Origin`](xref:SixLabors.Fonts.TextOptions.Origin), so each successive line's [`LineMetrics.Start`](xref:SixLabors.Fonts.LineMetrics.Start) includes the cumulative advance of the lines that came before it. + +```csharp +using SixLabors.Fonts; + +ReadOnlyMemory layouts = block.GetLineLayouts(320); + +foreach (LineLayout line in layouts.Span) +{ + LineMetrics lineMetrics = line.LineMetrics; + ReadOnlySpan lineGraphemes = line.GraphemeMetrics; + ReadOnlyMemory lineGlyphs = line.GetGlyphMetrics(); +} +``` + +Use this when the whole block paints into one rectangle and you want the returned geometry to be ready to draw without any further offsetting. + +#### Line-local coordinates with `EnumerateLineLayouts` + +[`EnumerateLineLayouts()`](xref:SixLabors.Fonts.TextBlock.EnumerateLineLayouts*) lays out one line at a time and accepts the wrapping length per call. Each produced line is positioned independently, as if it were the first and only line in the block — its geometry sits at [`TextOptions.Origin`](xref:SixLabors.Fonts.TextOptions.Origin) regardless of which line index the enumerator is on. The caller is responsible for placing the line into the final layout. + +```csharp +using SixLabors.Fonts; + +LineLayoutEnumerator enumerator = block.EnumerateLineLayouts(); + +while (enumerator.MoveNext(wrappingLength: 320)) +{ + LineLayout line = enumerator.Current; +} +``` + +Use this when each line goes into a different column, frame, or shape — flowed text, variable-width columns, virtualized lists, or curved baselines — and the block's natural top-to-bottom stacking does not match the surface you are painting on. The wrapping length can also vary per line. + +#### Picking between them + +- Use `GetLineLayouts(...)` when the whole block paints as one stacked unit and you want the returned line positions to be ready to draw against the block origin. +- Use `EnumerateLineLayouts()` when the caller controls where each line lands and the block's stacking is not the layout you want. + +### Render the prepared block + +[`RenderTo(...)`](xref:SixLabors.Fonts.TextBlock.RenderTo*) draws the block to any [`IGlyphRenderer`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer) using the same wrapping-length argument as the measurement methods. + +```csharp +using SixLabors.Fonts.Rendering; + +block.RenderTo(renderer, wrappingLength: 480); +``` + +Always render with the same `TextOptions` and wrapping length you measured with. Reusing the prepared block avoids re-shaping the text between the two passes. + +### When to choose TextBlock over TextMeasurer + +Use `TextMeasurer` for one-off measurements where you do not need to keep a measurement object around. + +Use `TextBlock` when: + +- The same text is laid out repeatedly with different wrapping lengths. +- You want to measure once and render later with the same prepared shaping. +- You need per-line interaction (hit testing, carets, selection) — see [Hit Testing and Caret Movement](texthittesting.md). +- You want to walk the laid-out text line by line without materializing every line up front. diff --git a/articles/fonts/texthittesting.md b/articles/fonts/texthittesting.md new file mode 100644 index 000000000..176094ac9 --- /dev/null +++ b/articles/fonts/texthittesting.md @@ -0,0 +1,161 @@ +# Hit Testing and Caret Movement + +Once text has been laid out, applications usually need to translate between pixels, character positions, and editor commands. Fonts exposes a small set of types that own the bidi, grapheme, and hard-break rules so callers do not need to reimplement them: [`TextHit`](xref:SixLabors.Fonts.TextHit), [`CaretPosition`](xref:SixLabors.Fonts.CaretPosition), [`CaretPlacement`](xref:SixLabors.Fonts.CaretPlacement), and [`CaretMovement`](xref:SixLabors.Fonts.CaretMovement). + +All positional values returned by these APIs are in pixel units. + +### Get a measurement object + +Hit testing, caret positioning, and caret movement all operate on a [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) (whole-block) or [`LineLayout`](xref:SixLabors.Fonts.LineLayout) (single line). Either come from [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) or from a prepared [`TextBlock`](xref:SixLabors.Fonts.TextBlock). See [Prepared Text with TextBlock](textblock.md) for when to prefer one over the other. + +```csharp +using System.Numerics; +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 320, + Origin = new Vector2(20, 30), + TextInteractionMode = TextInteractionMode.Editor +}; + +TextMetrics metrics = TextMeasurer.Measure("Hello, world!", options); +``` + +### Choose paragraph or editor mode + +[`TextOptions.TextInteractionMode`](xref:SixLabors.Fonts.TextOptions.TextInteractionMode) controls how trailing whitespace and terminal hard breaks behave for hit testing, caret movement, and selection. + +- `TextInteractionMode.Paragraph` (the default) is the right fit for laid-out paragraphs and rendered text labels. Trailing breaking whitespace at the end of a line is trimmed from the layout, and a hard break that ends the text does not produce a caret stop on a trailing blank line. This matches the way browsers measure and paint static text. +- `TextInteractionMode.Editor` is the right fit for editable text surfaces. Ordinary trailing whitespace stays addressable so typed spaces continue to advance the caret, and a terminal `Enter` produces a blank line whose geometry the caret can land on. + +Set this once on the `TextOptions` you measure with. Every interaction API on the resulting `TextMetrics` (and on each `LineLayout`) honors it automatically — there is no per-call switch. + +If your application has both rendered paragraph regions and editable regions, use a different `TextOptions` instance for each, with the matching `TextInteractionMode` set on it. + +### Hit-test a point + +[`HitTest(point)`](xref:SixLabors.Fonts.TextMetrics.HitTest*) maps a pointer position to the nearest grapheme and returns a [`TextHit`](xref:SixLabors.Fonts.TextHit). + +```csharp +using System.Numerics; +using SixLabors.Fonts; + +TextHit hit = metrics.HitTest(new Vector2(mouseX, mouseY)); + +int line = hit.LineIndex; +int grapheme = hit.GraphemeIndex; +int stringIndex = hit.StringIndex; +bool trailing = hit.IsTrailing; +``` + +[`TextHit`](xref:SixLabors.Fonts.TextHit) is meant to be passed straight back into the interaction APIs — [`GetCaretPosition(hit)`](xref:SixLabors.Fonts.TextMetrics.GetCaretPosition*), [`GetSelectionBounds(anchor, focus)`](xref:SixLabors.Fonts.TextMetrics.GetSelectionBounds*), [`GetWordMetrics(hit)`](xref:SixLabors.Fonts.TextMetrics.GetWordMetrics*). Those overloads consume the hit directly and apply the trailing-side and bidi rules internally, so callers do not need to compute the visual side themselves. + +The properties are exposed for diagnostics and for cases where you need to point back into your own text — for example, mapping the hit to a position in your source string. `GraphemeInsertionIndex` is the insertion position within the laid-out grapheme array; you rarely need to read it yourself. + +### Position a caret + +A [`CaretPosition`](xref:SixLabors.Fonts.CaretPosition) is both a drawable line and the navigation token used by the movement APIs. + +```csharp +using SixLabors.Fonts; + +CaretPosition caret = metrics.GetCaretPosition(hit); + +DrawCaret(caret.Start, caret.End); + +if (caret.HasSecondary) +{ + DrawSecondaryCaret(caret.SecondaryStart, caret.SecondaryEnd); +} +``` + +At bidi run boundaries, one logical insertion position has two visual edges. [`CaretPosition.HasSecondary`](xref:SixLabors.Fonts.CaretPosition.HasSecondary) indicates that case, and [`SecondaryStart`](xref:SixLabors.Fonts.CaretPosition.SecondaryStart) / [`SecondaryEnd`](xref:SixLabors.Fonts.CaretPosition.SecondaryEnd) give the second visual edge. Editor-style callers can choose how to present or navigate the boundary without recomputing bidi affinity. + +When initializing a caret without a pointer hit (for example, for a freshly opened editor), use the placement overload: + +```csharp +using SixLabors.Fonts; + +CaretPosition start = metrics.GetCaret(CaretPlacement.Start); +CaretPosition end = metrics.GetCaret(CaretPlacement.End); +``` + +### Move a caret + +[`MoveCaret(...)`](xref:SixLabors.Fonts.TextMetrics.MoveCaret*) applies an editor-style movement to a caret and returns the new caret. The library owns the grapheme, line, and hard-break rules; callers should not perform their own grapheme arithmetic. + +```csharp +using SixLabors.Fonts; + +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + +caret = metrics.MoveCaret(caret, CaretMovement.Next); +caret = metrics.MoveCaret(caret, CaretMovement.NextWord); +caret = metrics.MoveCaret(caret, CaretMovement.LineEnd); +caret = metrics.MoveCaret(caret, CaretMovement.TextStart); +``` + +[`CaretMovement`](xref:SixLabors.Fonts.CaretMovement) covers the standard editor commands: + +- `Previous` and `Next` move through grapheme insertion positions. +- `PreviousWord` and `NextWord` move through Unicode word boundaries. +- `LineStart` and `LineEnd` are the Home/End-style operations. +- `TextStart` and `TextEnd` are the whole-block equivalents. +- `LineUp` and `LineDown` move to the previous or next visual line. + +All movement operations work in logical order and the returned `CaretPosition` is placed at the correct visual edge for the resolved bidi layout. In a right-to-left run, `Next` advances the caret one grapheme forward in the source text — visually that lands on the *left* edge of the next glyph, matching how browsers and native text editors behave. `LineStart` and `LineEnd` resolve to the visual edges that match the line's text direction (logical start of an RTL paragraph is on the right). Callers should not adjust for direction themselves; pass the returned `CaretPosition` straight back into `MoveCaret(...)` and the library tracks the bidi state. + +At bidi run boundaries one logical insertion position has two visual edges. `MoveCaret(...)` returns a `CaretPosition` with `HasSecondary == true` in that case so editor-style callers can present both edges or pick whichever fits the surrounding caret state. + +`LineUp` and `LineDown` preserve the caret's original requested position on the line. Repeated vertical movement keeps that position even when an intermediate line is shorter and the visible caret has to clamp to that line's end. + +```csharp +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); +caret = metrics.MoveCaret(caret, CaretMovement.LineEnd); + +// Repeated LineDown remembers the original line position. +CaretPosition next = metrics.MoveCaret(caret, CaretMovement.LineDown); +CaretPosition after = metrics.MoveCaret(next, CaretMovement.LineDown); +``` + +This matches normal rich-text editor behavior: moving down through a short line does not permanently lose the user's original horizontal or vertical line position. + +### Look up a word + +For double-click or word-based selection, pass the hit (or caret) directly to [`GetWordMetrics(...)`](xref:SixLabors.Fonts.TextMetrics.GetWordMetrics*). This uses the grapheme that was hit, so clicking the trailing side of the final grapheme of a word still selects that word rather than the following separator segment. + +```csharp +using SixLabors.Fonts; + +TextHit hit = metrics.HitTest(doubleClickPosition); +WordMetrics word = metrics.GetWordMetrics(hit); +``` + +A Unicode word-boundary segment includes its separators. `can't stop` produces three segments: `can't`, the space, and `stop`. Higher-level editor commands can decide whether to stop on separator boundaries or skip over them. + +### Per-line interaction + +[`LineLayout`](xref:SixLabors.Fonts.LineLayout) mirrors the interaction surface for a single line. Use it when the caller already knows interaction is line-local; otherwise prefer [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) so cross-line behavior (such as `LineDown` or wrapping selection) works correctly. + +```csharp +using SixLabors.Fonts; + +ReadOnlyMemory layouts = block.GetLineLayouts(320); + +foreach (LineLayout line in layouts.Span) +{ + TextHit hit = line.HitTest(point); + CaretPosition caret = line.GetCaretPosition(hit); + WordMetrics word = line.GetWordMetrics(hit); +} +``` + +### Hard line breaks + +Hard line breaks at the end of non-empty lines are trimmed with other trailing breaking whitespace. Hard line breaks that own a blank line remain in the metrics so source ranges, hit testing, caret movement, and selection painting still cover that line. Consumers that inspect graphemes individually can use `GraphemeMetrics.IsLineBreak` to identify these cases. + +In `TextInteractionMode.Editor`, a terminal hard break also produces a blank line at the end of the text so the caret can land on it after the user types `Enter`. In `TextInteractionMode.Paragraph` that trailing blank line is omitted, matching paragraph-style layout. + +For more on the underlying measurement model and the `TextMetrics` shape, see [Measuring Text](measuringtext.md). For the full selection API, see [Selection and Bidi Drag](caretsandselection.md). diff --git a/articles/fonts/textlayout.md b/articles/fonts/textlayout.md index 6b6e16081..20246467d 100644 --- a/articles/fonts/textlayout.md +++ b/articles/fonts/textlayout.md @@ -1,14 +1,14 @@ # Text Layout and Options -Once you have a `Font`, `TextOptions` becomes the center of almost everything else. It is where you tell Fonts how text should flow, wrap, align, shape, and render, so getting comfortable with this type pays off quickly. +Once you have a [`Font`](xref:SixLabors.Fonts.Font), [`TextOptions`](xref:SixLabors.Fonts.TextOptions) becomes the center of almost everything else. It is where you tell Fonts how text should flow, wrap, align, shape, and render, so getting comfortable with this type pays off quickly. -The same options type is used by both `TextMeasurer` and `TextRenderer`, which makes it easy to keep measurement and rendering in sync. +The same options type is used by both [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) and [`TextRenderer`](xref:SixLabors.Fonts.Rendering.TextRenderer), which makes it easy to keep measurement and rendering in sync. ### Core units -`Font.Size` is expressed in points. `TextOptions.Dpi` controls how that size is converted into pixels for measurement and rendering. The default DPI is `72`. +[`Font.Size`](xref:SixLabors.Fonts.Font.Size) is expressed in points. [`TextOptions.Dpi`](xref:SixLabors.Fonts.TextOptions.Dpi) controls how that size is converted into pixels for measurement and rendering. The default DPI is `72`. -`WrappingLength` is expressed in pixels and defines when text wraps. `Origin` sets the rendering origin used by the layout engine. +[`WrappingLength`](xref:SixLabors.Fonts.TextOptions.WrappingLength) is expressed in pixels and defines when text wraps. [`Origin`](xref:SixLabors.Fonts.TextOptions.Origin) sets the rendering origin used by the layout engine. ```csharp using System.Numerics; @@ -31,10 +31,13 @@ These properties control how text is broken into lines and laid out: - `WrappingLength` - `WordBreaking` +- `MaxLines` +- `TextEllipsis` +- `TextHyphenation` - `TextDirection` - `LayoutMode` -`WordBreaking` supports `Standard`, `BreakAll`, `KeepAll`, and `BreakWord`. `TextDirection` supports left-to-right, right-to-left, and automatic detection. `LayoutMode` supports horizontal and vertical layouts, including mixed vertical modes that rotate horizontal glyphs. +[`WordBreaking`](xref:SixLabors.Fonts.TextOptions.WordBreaking) supports `Standard`, `BreakAll`, `KeepAll`, and `BreakWord`. [`MaxLines`](xref:SixLabors.Fonts.TextOptions.MaxLines) limits how many lines are laid out; use `-1` for unlimited lines. [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection) supports left-to-right, right-to-left, and automatic detection. [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) supports horizontal and vertical layouts, including mixed vertical modes that rotate horizontal glyphs. ```csharp using SixLabors.Fonts; @@ -49,11 +52,36 @@ TextOptions options = new(font) }; ``` +### Ellipsis and hyphenation markers + +[`TextEllipsis`](xref:SixLabors.Fonts.TextOptions.TextEllipsis) controls whether a marker is inserted when [`MaxLines`](xref:SixLabors.Fonts.TextOptions.MaxLines) hides remaining text. `TextEllipsis.Standard` inserts the standard ellipsis marker, `TextEllipsis.Custom` uses [`CustomEllipsis`](xref:SixLabors.Fonts.TextOptions.CustomEllipsis), and `TextEllipsis.None` clips to the line limit without adding a marker. + +[`TextHyphenation`](xref:SixLabors.Fonts.TextOptions.TextHyphenation) controls the marker used when wrapping selects a soft-hyphen break opportunity. `TextHyphenation.Standard` inserts the standard hyphenation marker, `TextHyphenation.Custom` uses [`CustomHyphen`](xref:SixLabors.Fonts.TextOptions.CustomHyphen), and `TextHyphenation.None` allows the soft-hyphen break without drawing a marker. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + WrappingLength = 220, + MaxLines = 2, + TextEllipsis = TextEllipsis.Standard, + TextHyphenation = TextHyphenation.Custom, + + // CustomHyphen is used only when wrapping chooses a soft-hyphen break. + CustomHyphen = new CodePoint('-') +}; +``` + +Set `CustomEllipsis` or `CustomHyphen` only when the matching option is `Custom`. Standard markers still depend on glyph coverage in the selected font or fallback families. + ### Alignment and justification -`TextAlignment` expresses logical alignment within the text box using `Start`, `End`, and `Center`, and it respects the active text direction. `TextJustification` controls whether additional spacing is distributed between words or between characters. +[`TextAlignment`](xref:SixLabors.Fonts.TextOptions.TextAlignment) expresses logical alignment within the text box using `Start`, `End`, and `Center`, and it respects the active text direction. [`TextJustification`](xref:SixLabors.Fonts.TextOptions.TextJustification) controls whether additional spacing is distributed between words or between characters. -`HorizontalAlignment` and `VerticalAlignment` give you physical alignment controls for the layout box itself. +[`HorizontalAlignment`](xref:SixLabors.Fonts.TextOptions.HorizontalAlignment) and [`VerticalAlignment`](xref:SixLabors.Fonts.TextOptions.VerticalAlignment) give you physical alignment controls for the layout box itself. ```csharp using SixLabors.Fonts; @@ -73,11 +101,11 @@ TextOptions options = new(font) Fonts exposes several knobs that directly affect glyph layout: -- `LineSpacing` multiplies the line height. -- `TabWidth` controls tab stops in space units. -- `KerningMode` enables, disables, or lets the engine decide about font-provided kerning during shaping. -- `Tracking` applies uniform letter-spacing and is measured in em. -- `HintingMode` is separate from shaping and controls TrueType grid fitting for the current size and DPI. +- [`LineSpacing`](xref:SixLabors.Fonts.TextOptions.LineSpacing) multiplies the line height. +- [`TabWidth`](xref:SixLabors.Fonts.TextOptions.TabWidth) controls tab stops in space units. +- [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode) enables, disables, or lets the engine decide about font-provided kerning during shaping. +- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) applies uniform letter-spacing and is measured in em. +- [`HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) is separate from shaping and controls TrueType grid fitting for the current size and DPI. ```csharp using SixLabors.Fonts; @@ -97,7 +125,7 @@ For a deeper explanation of how Fonts applies GSUB/GPOS shaping, bidi analysis, ### Fallback fonts and color fonts -Use `FallbackFontFamilies` when a single font cannot cover every glyph you need. +Use [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) when a single font cannot cover every glyph you need. ```csharp using SixLabors.Fonts; @@ -109,18 +137,18 @@ FontFamily emojiFamily = collection.Add("fonts/NotoColorEmoji-Regular.ttf"); TextOptions options = new(textFamily.CreateFont(16)) { - FallbackFontFamilies = new[] { arabicFamily, emojiFamily }, + FallbackFontFamilies = [arabicFamily, emojiFamily], ColorFontSupport = ColorFontSupport.ColrV1 | ColorFontSupport.Svg }; ``` -`ColorFontSupport` controls which color-font technologies are honored during layout and rendering: `ColrV0`, `ColrV1`, and `Svg`. +[`ColorFontSupport`](xref:SixLabors.Fonts.TextOptions.ColorFontSupport) controls which color-font technologies are honored during layout and rendering: `ColrV0`, `ColrV1`, and `Svg`. For a fuller discussion of multilingual text, fallback ordering, and script coverage, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). ### OpenType feature tags -`FeatureTags` lets you request additional OpenType features during shaping. The property type is `IReadOnlyList`, which means you can use either `KnownFeatureTags` enum values or parse raw four-character tags with `Tag.Parse(...)`. +[`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) lets you request additional OpenType features during shaping. The property type is `IReadOnlyList`, which means you can use either [`KnownFeatureTags`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.KnownFeatureTags) enum values or parse raw four-character tags with [`Tag.Parse(...)`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.Tag.Parse*). ```csharp using SixLabors.Fonts; @@ -129,12 +157,15 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] - { + FeatureTags = + [ KnownFeatureTags.Ligatures, KnownFeatureTags.TabularFigures, + + // 'ss01' is the first of OpenType's stylistic sets (ss01..ss20), + // which a font can use to expose an alternate glyph design. Tag.Parse("ss01") - } + ] }; ``` @@ -144,9 +175,9 @@ See [OpenType Features](opentypefeatures.md) for a fuller guide to common featur ### Text runs -`TextRuns` lets you override layout attributes for subranges of text. A `TextRun` can replace the font and apply `TextAttributes` or `TextDecorations`. +[`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) lets you override layout attributes for subranges of text. A [`TextRun`](xref:SixLabors.Fonts.TextRun) can replace the font and apply `TextAttributes` or `TextDecorations`. -`TextRun.Start` is inclusive and `TextRun.End` is exclusive. Both are grapheme indices, not UTF-16 code-unit indices. +[`TextRun.Start`](xref:SixLabors.Fonts.TextRun.Start) is inclusive and [`TextRun.End`](xref:SixLabors.Fonts.TextRun.End) is exclusive. Both are grapheme indices, not UTF-16 code-unit indices. ```csharp using SixLabors.Fonts; @@ -158,8 +189,8 @@ Font emphasisFont = SystemFonts.CreateFont("Segoe UI", 18, FontStyle.Bold); TextOptions options = new(baseFont) { - TextRuns = new[] - { + TextRuns = + [ new TextRun { Start = 7, @@ -167,10 +198,47 @@ TextOptions options = new(baseFont) Font = emphasisFont, TextDecorations = TextDecorations.Underline } - } + ] }; ``` For plain ASCII text, grapheme indices often line up with character positions. For emoji, combining marks, and complex scripts, calculate ranges in graphemes rather than assuming one UTF-16 code unit equals one visible character. See [Unicode, Code Points, and Graphemes](unicode.md) for a fuller explanation of `char`, `CodePoint`, and grapheme units. + +### Inline placeholders + +Use [`TextPlaceholder`](xref:SixLabors.Fonts.TextPlaceholder) when the text layout must reserve space for an inline object that your renderer will draw separately, such as an icon, emoji image, inline control, or attachment. Placeholders participate in measurement, wrapping, bidi ordering, and line-height calculation, but they do not consume text from the source string. + +Add a placeholder through a zero-length [`TextRun`](xref:SixLabors.Fonts.TextRun). The run's `Start` and `End` values must be the same grapheme index, because the placeholder is inserted at that point rather than replacing text. + +```csharp +using SixLabors.Fonts; + +const string text = "Pay now"; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + TextRuns = + [ + new TextRun + { + // Placeholder runs are zero-length insertion points: [Start, End). + Start = 4, + End = 4, + + // The placeholder reserves inline space; the caller draws the object itself. + Placeholder = new TextPlaceholder( + width: 28, + height: 20, + alignment: TextPlaceholderAlignment.Middle, + baselineOffset: 14) + } + ] +}; + +FontRectangle bounds = TextMeasurer.MeasureAdvance(text, options); +``` + +[`TextPlaceholderAlignment`](xref:SixLabors.Fonts.TextPlaceholderAlignment) controls how the placeholder box aligns with the surrounding line. `Baseline` uses the supplied baseline offset directly, while `AboveBaseline`, `BelowBaseline`, `Top`, `Bottom`, and `Middle` align the placeholder against the surrounding line box. diff --git a/articles/fonts/troubleshooting.md b/articles/fonts/troubleshooting.md index dde979771..fec9ad53f 100644 --- a/articles/fonts/troubleshooting.md +++ b/articles/fonts/troubleshooting.md @@ -4,7 +4,7 @@ When text does not measure or render the way you expect, the underlying cause is ### A font family cannot be found -If `Get(...)` or `SystemFonts.CreateFont(...)` fails, you may see `FontFamilyNotFoundException`. +If [`Get(...)`](xref:SixLabors.Fonts.FontCollection.Get*) or [`SystemFonts.CreateFont(...)`](xref:SixLabors.Fonts.SystemFonts.CreateFont*) fails, you may see [`FontFamilyNotFoundException`](xref:SixLabors.Fonts.FontFamilyNotFoundException). Typical causes: @@ -15,32 +15,32 @@ Typical causes: Safer patterns are: -- use `TryGet(...)` instead of `Get(...)` when probing -- inspect `FontDescription` after loading a file -- prefer application-owned font files over machine-specific `SystemFonts` when portability matters +- use [`TryGet(...)`](xref:SixLabors.Fonts.FontCollection.TryGet*) instead of `Get(...)` when probing +- inspect [`FontDescription`](xref:SixLabors.Fonts.FontDescription) after loading a file +- prefer application-owned font files over machine-specific [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts) when portability matters ### A font file loads poorly or throws Invalid or unsupported font data can surface as: -- `InvalidFontFileException` -- `InvalidFontTableException` -- `MissingFontTableException` +- [`InvalidFontFileException`](xref:SixLabors.Fonts.InvalidFontFileException) +- [`InvalidFontTableException`](xref:SixLabors.Fonts.InvalidFontTableException) +- [`MissingFontTableException`](xref:SixLabors.Fonts.MissingFontTableException) If you hit one of these: - verify the file is a real font and not an incomplete download - prefer loading from a stable local file or stream -- if the font is a collection, use `AddCollection(...)` or `LoadFontCollectionDescriptions(...)` +- if the font is a collection, use [`AddCollection(...)`](xref:SixLabors.Fonts.FontCollection.AddCollection*) or [`LoadFontCollectionDescriptions(...)`](xref:SixLabors.Fonts.FontDescription.LoadFontCollectionDescriptions*) ### Text renders with missing glyphs If some characters do not render as expected: - make sure the selected font actually contains the script you need -- add script-specific families to `FallbackFontFamilies` +- add script-specific families to [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) - enable color-font support if the missing content is emoji -- use `TryGetGlyphs(...)` when you need to probe a specific `CodePoint` value directly +- use [`TryGetGlyphs(...)`](xref:SixLabors.Fonts.Font.TryGetGlyphs*) when you need to probe a specific [`CodePoint`](xref:SixLabors.Fonts.Unicode.CodePoint) value directly Fallback can only help if one of the supplied families actually contains the required glyphs. @@ -48,7 +48,7 @@ Fallback can only help if one of the supplied families actually contains the req The most common reason is that the primary font already contains a glyph for that Unicode scalar value, so fallback never activates. -If you want a specific range to use a different font even when the primary font could render it, use `TextRuns` instead of relying on fallback. +If you want a specific range to use a different font even when the primary font could render it, use [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) instead of relying on fallback. Fallback order also matters. Fonts searches `FallbackFontFamilies` in order and uses the first suitable family it finds. @@ -57,7 +57,7 @@ Fallback order also matters. Fonts searches `FallbackFontFamilies` in order and Check these first: - use a font that actually supports the script -- set `TextDirection = TextDirection.Auto` or explicitly choose the correct direction +- set [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection) to [`TextDirection.Auto`](xref:SixLabors.Fonts.TextDirection.Auto) or explicitly choose the correct direction - avoid assuming simple one-character-per-glyph behavior - verify your fallback families cover the script, not just isolated characters @@ -67,15 +67,15 @@ Arabic, Indic, Thai, Hebrew, and similar scripts depend on shaping, not just raw This is usually a measurement-choice issue: -- `MeasureAdvance(...)` is the logical layout box -- `MeasureBounds(...)` is pure glyph ink bounds -- `MeasureRenderableBounds(...)` is the union of the two +- [`MeasureAdvance(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureAdvance*) is the logical layout box +- [`MeasureBounds(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureBounds*) is pure glyph ink bounds +- [`MeasureRenderableBounds(...)`](xref:SixLabors.Fonts.TextMeasurer.MeasureRenderableBounds*) is the union of the two It is normal for these values to differ. Italics, accents, and decorative forms often extend outside the advance box, while line height can add space that no glyph pixels occupy. ### Text run indices look wrong -`TextRun.Start` and `TextRun.End` are grapheme indices, not UTF-16 code-unit indices. +[`TextRun.Start`](xref:SixLabors.Fonts.TextRun.Start) and [`TextRun.End`](xref:SixLabors.Fonts.TextRun.End) are grapheme indices, not UTF-16 code-unit indices. That matters for: @@ -96,10 +96,10 @@ Usually one of these is true: - the axis tag is wrong - the value is outside the font's supported range -Use `font.FontMetrics.TryGetVariationAxes(...)` to inspect the actual axes and ranges exposed by the font. `FontVariation` tags must be exactly four characters. +Use [`font.FontMetrics.TryGetVariationAxes(...)`](xref:SixLabors.Fonts.FontMetrics.TryGetVariationAxes*) to inspect the actual axes and ranges exposed by the font. [`FontVariation`](xref:SixLabors.Fonts.FontVariation) tags must be exactly four characters. ### System font behavior differs by machine -`SystemFonts` is convenient, but it is not deterministic across environments. Different machines can have different installed families, versions, and script coverage. +[`SystemFonts`](xref:SixLabors.Fonts.SystemFonts) is convenient, but it is not deterministic across environments. Different machines can have different installed families, versions, and script coverage. If you need repeatable output across CI, servers, containers, and user machines, ship your own fonts and load them through `FontCollection`. diff --git a/articles/fonts/unicode.md b/articles/fonts/unicode.md index f3bdf3102..0c8bab356 100644 --- a/articles/fonts/unicode.md +++ b/articles/fonts/unicode.md @@ -5,8 +5,8 @@ Text handling gets easier once you stop treating every `char` as a whole charact ### The text-unit levels - `char`: a single UTF-16 code unit in a .NET `string` -- `CodePoint`: a Unicode scalar value, represented by -- grapheme: a user-perceived text element, represented by a `ReadOnlySpan` returned from `SpanGraphemeEnumerator` +- [`CodePoint`](xref:SixLabors.Fonts.Unicode.CodePoint): a Unicode scalar value +- grapheme: a user-perceived text element, represented by a `ReadOnlySpan` returned from [`SpanGraphemeEnumerator`](xref:SixLabors.Fonts.Unicode.SpanGraphemeEnumerator) In everyday text, those levels often line up for simple ASCII. Once you move beyond that, they diverge quickly. @@ -33,7 +33,7 @@ In strict Unicode terminology: That makes it the right unit when you want to talk about valid Unicode text values directly. -Useful `CodePoint` members include: +Useful [`CodePoint`](xref:SixLabors.Fonts.Unicode.CodePoint) members include: - `Value` - `Utf16SequenceLength` @@ -43,7 +43,7 @@ Useful `CodePoint` members include: - `Plane` - `ReplacementChar` -This is also the unit used by glyph-probing APIs such as `Font.TryGetGlyphs(...)`. +This is also the unit used by glyph-probing APIs such as [`Font.TryGetGlyphs(...)`](xref:SixLabors.Fonts.Font.TryGetGlyphs*). ### What is a grapheme? @@ -56,9 +56,9 @@ Examples: - many emoji sequences joined with zero-width joiners are one grapheme - a flag emoji made from two regional indicators is one grapheme -Fonts exposes grapheme enumeration through `SpanGraphemeEnumerator`, which implements the Unicode grapheme cluster algorithm from UAX #29. +Fonts exposes grapheme enumeration through [`SpanGraphemeEnumerator`](xref:SixLabors.Fonts.Unicode.SpanGraphemeEnumerator), which implements the Unicode grapheme cluster algorithm from UAX #29. -This is why `TextRun.Start` and `TextRun.End` are grapheme indices rather than raw `char` indices. +This is why [`TextRun.Start`](xref:SixLabors.Fonts.TextRun.Start) and [`TextRun.End`](xref:SixLabors.Fonts.TextRun.End) are grapheme indices rather than raw `char` indices. ### Enumerate `CodePoint` values @@ -68,7 +68,9 @@ The Unicode enumeration helpers live in `SixLabors.Fonts.Unicode`. using System; using SixLabors.Fonts.Unicode; -string text = "A\u0301 \U0001F600"; +// 'A' + combining acute accent (U+0301) renders as a single accented A grapheme, +// followed by a space and the grinning-face emoji (U+1F600). +string text = "Á 😀"; foreach (CodePoint codePoint in text.AsSpan().EnumerateCodePoints()) { @@ -77,14 +79,16 @@ foreach (CodePoint codePoint in text.AsSpan().EnumerateCodePoints()) } ``` -`EnumerateCodePoints()` returns a . It yields `CodePoint` values, which means the enumeration surface is Unicode scalar values. Invalid UTF-16 sequences are surfaced as `CodePoint.ReplacementChar`. +[`EnumerateCodePoints()`](xref:SixLabors.Fonts.Unicode.MemoryExtensions.EnumerateCodePoints*) returns a [`SpanCodePointEnumerator`](xref:SixLabors.Fonts.Unicode.SpanCodePointEnumerator). It yields [`CodePoint`](xref:SixLabors.Fonts.Unicode.CodePoint) values, which means the enumeration surface is Unicode scalar values. Invalid UTF-16 sequences are surfaced as [`CodePoint.ReplacementChar`](xref:SixLabors.Fonts.Unicode.CodePoint.ReplacementChar). Count helpers are also available: ```csharp using SixLabors.Fonts.Unicode; -int count = "A\u0301 \U0001F600".GetCodePointCount(); +// 'A' + combining acute (U+0301), space, grinning-face emoji (U+1F600). +// 4 code points: 'A', U+0301, ' ', U+1F600. +int count = "Á 😀".GetCodePointCount(); ``` ### Enumerate graphemes @@ -95,7 +99,8 @@ Use grapheme enumeration when you need units that better match what a reader see using System; using SixLabors.Fonts.Unicode; -string text = "A\u0301 \U0001F600"; +// Same text as before, but graphemes group the accented A into one cluster. +string text = "Á 😀"; int index = 0; foreach (ReadOnlySpan grapheme in text.AsSpan().EnumerateGraphemes()) @@ -104,16 +109,53 @@ foreach (ReadOnlySpan grapheme in text.AsSpan().EnumerateGraphemes()) } ``` -`EnumerateGraphemes()` returns a . +[`EnumerateGraphemes()`](xref:SixLabors.Fonts.Unicode.MemoryExtensions.EnumerateGraphemes*) returns a [`SpanGraphemeEnumerator`](xref:SixLabors.Fonts.Unicode.SpanGraphemeEnumerator). Count helpers are available here too: ```csharp using SixLabors.Fonts.Unicode; -int count = "A\u0301 \U0001F600".GetGraphemeCount(); +// 3 graphemes: the accented A, the space, and the emoji. +int count = "Á 😀".GetGraphemeCount(); ``` +### Enumerate word-boundary segments + +Use word enumeration when the surface needs to reason about whole words — caret movement that jumps a word at a time, double-click word selection, search-as-you-type tokenization. Word segmentation follows the Unicode Word Boundary Algorithm in UAX #29. + +```csharp +using System; +using SixLabors.Fonts.Unicode; + +string text = "Don't stop."; + +foreach (WordSegment word in text.AsSpan().EnumerateWordSegments()) +{ + Console.WriteLine( + $"[{word.Utf16Offset}..{word.Utf16Offset + word.Utf16Length}] '{word.Span.ToString()}'"); +} +``` + +The output for the example above is: + +```text +[0..5] 'Don't' +[5..6] ' ' +[6..10] 'stop' +[10..11] '.' +``` + +UAX #29 segments include separators — the space between `Don't` and `stop` is its own segment, and the trailing `.` is another. Higher-level editor commands can decide whether to stop on those segments or skip past them; the raw enumerator stays aligned with the standard. + +[`EnumerateWordSegments()`](xref:SixLabors.Fonts.Unicode.MemoryExtensions.EnumerateWordSegments*) returns a [`SpanWordEnumerator`](xref:SixLabors.Fonts.Unicode.SpanWordEnumerator). Each [`WordSegment`](xref:SixLabors.Fonts.Unicode.WordSegment) exposes: + +- `Span` — the UTF-16 slice of the segment. +- `Utf16Offset` and `Utf16Length` — UTF-16 indices into the original text. +- `CodePointOffset` and `CodePointCount` — code-point indices into the original text. + +This is the same Unicode word-boundary model used by [`TextMetrics.WordMetrics`](xref:SixLabors.Fonts.TextMetrics.WordMetrics), [`MoveCaret(CaretMovement.NextWord)`](xref:SixLabors.Fonts.TextMetrics.MoveCaret*), and [`GetWordMetrics(hit)`](xref:SixLabors.Fonts.TextMetrics.GetWordMetrics*). Use the enumerator when you need word boundaries against raw text without going through a full layout pass; use the metrics APIs when you need positioned word geometry as well. See [Hit Testing and Caret Movement](texthittesting.md) for the layout-aware side. + ### Which unit should you use? Use `char` when: @@ -124,13 +166,13 @@ Use `char` when: Use `CodePoint` when: - you are inspecting Unicode scalar values -- you are probing glyph availability with `TryGetGlyphs(...)` +- you are probing glyph availability with [`TryGetGlyphs(...)`](xref:SixLabors.Fonts.Font.TryGetGlyphs*) - you care about Unicode values, planes, or encoded sequence lengths Use graphemes when: - you are slicing visible text ranges -- you are working with `TextRun.Start` and `TextRun.End` +- you are working with [`TextRun.Start`](xref:SixLabors.Fonts.TextRun.Start) and [`TextRun.End`](xref:SixLabors.Fonts.TextRun.End) - you want indices that align better with user-visible text elements ### Relation to layout diff --git a/articles/fonts/useopentypefeatures.md b/articles/fonts/useopentypefeatures.md index 9ca21056a..5bd34d7a4 100644 --- a/articles/fonts/useopentypefeatures.md +++ b/articles/fonts/useopentypefeatures.md @@ -1,6 +1,6 @@ # Use OpenType Features for Numbers and Fractions -This recipe shows the most common way people first encounter discretionary OpenType features: asking fonts to align figures more neatly or substitute fraction glyphs for number-heavy text. +This recipe shows the most common way people first encounter discretionary OpenType features: asking fonts through [`TextOptions.FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) to align figures more neatly or substitute fraction glyphs for number-heavy text. ### Align numeric columns with tabular figures @@ -11,7 +11,7 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] { KnownFeatureTags.TabularFigures } + FeatureTags = [KnownFeatureTags.TabularFigures] }; ``` @@ -26,7 +26,7 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] { KnownFeatureTags.Fractions } + FeatureTags = [KnownFeatureTags.Fractions] }; ``` @@ -41,12 +41,15 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; Font font = SystemFonts.CreateFont("Segoe UI", 18); TextOptions options = new(font) { - FeatureTags = new Tag[] - { + FeatureTags = + [ KnownFeatureTags.TabularFigures, KnownFeatureTags.OldstyleFigures, + + // 'ss01' is the first of OpenType's stylistic sets (ss01..ss20), + // which a font can use to expose an alternate glyph design. Tag.Parse("ss01") - } + ] }; ``` diff --git a/articles/fonts/variablefonts.md b/articles/fonts/variablefonts.md index 17c15135a..4e435686e 100644 --- a/articles/fonts/variablefonts.md +++ b/articles/fonts/variablefonts.md @@ -1,10 +1,10 @@ # Variable Fonts -Variable fonts let one font file behave more like a design space than a single static face. Once that idea clicks, `FontVariation` becomes a practical way to ask for weight, width, slant, or optical-size variants without switching families. +Variable fonts let one font file behave more like a design space than a single static face. Once that idea clicks, [`FontVariation`](xref:SixLabors.Fonts.FontVariation) becomes a practical way to ask for weight, width, slant, or optical-size variants without switching families. ### Create a variable-font instance -Use `FontFamily.CreateFont(...)` with one or more `FontVariation` values. +Use [`FontFamily.CreateFont(...)`](xref:SixLabors.Fonts.FontFamily.CreateFont*) with one or more [`FontVariation`](xref:SixLabors.Fonts.FontVariation) values. ```csharp using SixLabors.Fonts; @@ -19,11 +19,11 @@ Font font = family.CreateFont( new FontVariation(KnownVariationAxes.OpticalSize, 16)); ``` -The tag must be exactly four characters. Common registered axis tags are available in `KnownVariationAxes`, but custom axes can also be addressed directly. +The tag must be exactly four characters. Common registered axis tags are available in [`KnownVariationAxes`](xref:SixLabors.Fonts.KnownVariationAxes), but custom axes can also be addressed directly. ### Use a prototype font -If you already have a base `Font`, you can derive a new instance from it. +If you already have a base [`Font`](xref:SixLabors.Fonts.Font), you can derive a new instance from it. ```csharp using SixLabors.Fonts; @@ -41,7 +41,7 @@ This is useful when you want to keep the same family, size, and requested style ### Inspect supported axes -You can query the variable axes exposed by the current font through `FontMetrics.TryGetVariationAxes(...)`. +You can query the variable axes exposed by the current font through [`FontMetrics.TryGetVariationAxes(...)`](xref:SixLabors.Fonts.FontMetrics.TryGetVariationAxes*). ```csharp using System; @@ -52,16 +52,16 @@ FontCollection collection = new(); FontFamily family = collection.Add("fonts/RobotoFlex.ttf"); Font font = family.CreateFont(16); -if (font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)) +if (font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)) { - foreach (VariationAxis axis in axes) + foreach (VariationAxis axis in axes.Span) { Console.WriteLine($"{axis.Tag}: {axis.Min}..{axis.Max} (default {axis.Default})"); } } ``` -Each `VariationAxis` exposes: +Each [`VariationAxis`](xref:SixLabors.Fonts.Tables.AdvancedTypographic.Variations.VariationAxis) exposes: - `Name` - `Tag` @@ -73,7 +73,7 @@ That makes it possible to build UI controls or configuration validation based on ### Registered and custom axes -`KnownVariationAxes` includes the registered tags most users expect: +[`KnownVariationAxes`](xref:SixLabors.Fonts.KnownVariationAxes) includes the registered tags most users expect: - `Weight` (`wght`) - `Width` (`wdth`) @@ -97,7 +97,7 @@ Font font = family.CreateFont( ### How values behave -`FontVariation` follows CSS `font-variation-settings` semantics. Variation values are clamped to the axis range defined by the font. +[`FontVariation`](xref:SixLabors.Fonts.FontVariation) follows CSS `font-variation-settings` semantics. Variation values are clamped to the axis range defined by the font. That means: @@ -107,7 +107,7 @@ That means: ### Non-variable fonts -Applying `FontVariation` values to a non-variable font is harmless but has no effect. If you need to know whether a font is actually variable, check `TryGetVariationAxes(...)` before building variation-driven UI or configuration. +Applying [`FontVariation`](xref:SixLabors.Fonts.FontVariation) values to a non-variable font is harmless but has no effect. If you need to know whether a font is actually variable, check [`TryGetVariationAxes(...)`](xref:SixLabors.Fonts.FontMetrics.TryGetVariationAxes*) before building variation-driven UI or configuration. ### When to use variable fonts diff --git a/articles/imagesharp.drawing/annotations.md b/articles/imagesharp.drawing/annotations.md new file mode 100644 index 000000000..031a75f0a --- /dev/null +++ b/articles/imagesharp.drawing/annotations.md @@ -0,0 +1,47 @@ +# Add Callouts and Annotations + +Annotations are just normal drawing commands layered over an existing image. Use pens for outlines and guides, transparent fills for highlights, and text layout options for labels. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("photo.jpg"); + +Rectangle highlight = new(92, 64, 220, 140); +PointF labelOrigin = new(highlight.Right + 28, highlight.Top + 12); +Font font = SystemFonts.CreateFont("Arial", 24, FontStyle.Bold); +RichTextOptions labelOptions = new(font) +{ + Origin = labelOrigin, + WrappingLength = 220 +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.22F)), highlight); + canvas.Draw(Pens.Dash(Color.Gold, 5), highlight); + + // The guide line connects the label to the highlighted region without changing the image pixels underneath. + canvas.DrawLine( + Pens.Solid(Color.Gold, 3), + new PointF(labelOrigin.X - 12, labelOrigin.Y + 12), + new PointF(highlight.Right, highlight.Top + (highlight.Height / 2F))); + + canvas.DrawText(labelOptions, "Region of interest", Brushes.Solid(Color.White), Pens.Solid(Color.Black, 1.5F)); +})); + +image.Save("annotated.jpg"); +``` + +Keep annotation geometry in image coordinates. If you need a local coordinate system for a panel or inset, use `CreateRegion(...)` or a saved transform. + +## Related Topics + +- [Canvas Drawing](canvas.md) +- [Brushes and Pens](brushesandpens.md) +- [Transforms and Composition](transformsandcomposition.md) diff --git a/articles/imagesharp.drawing/badge.md b/articles/imagesharp.drawing/badge.md new file mode 100644 index 000000000..e935f47be --- /dev/null +++ b/articles/imagesharp.drawing/badge.md @@ -0,0 +1,53 @@ +# Draw a Badge or Label + +Small generated badges usually combine a filled shape, an outline, and centered text. Build the shape once, then use the same path for fill and stroke so the border exactly follows the filled area. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 180, Color.Transparent.ToPixel()); + +RectangularPolygon badge = new(24, 36, 372, 108); +Font font = SystemFonts.CreateFont("Arial", 38, FontStyle.Bold); +PointF gradientStart = new(24, 36); +PointF gradientEnd = new(396, 144); +RichTextOptions textOptions = new(font) +{ + Origin = new(210, 90), + WrappingLength = 320, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center +}; + +LinearGradientBrush fill = new( + gradientStart, + gradientEnd, + GradientRepetitionMode.None, + new ColorStop(0F, Color.DeepSkyBlue), + new ColorStop(1F, Color.MediumBlue)); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(fill, badge); + canvas.Draw(Pens.Solid(Color.White.WithAlpha(0.9F), 4), badge); + + // The text anchor is the badge center, and wrapping keeps long labels inside the shape. + canvas.DrawText(textOptions, "ACTIVE", Brushes.Solid(Color.White), pen: null); +})); + +image.Save("badge.png"); +``` + +Use a path type that matches the badge geometry you want. `RectangularPolygon`, `EllipsePolygon`, `RegularPolygon`, `Star`, and custom `PathBuilder` paths can all be filled and stroked through the same canvas calls. + +## Related Topics + +- [Primitive Drawing Helpers](primitives.md) +- [Brushes and Pens](brushesandpens.md) +- [Drawing Text](text.md) diff --git a/articles/imagesharp.drawing/brushesandpens.md b/articles/imagesharp.drawing/brushesandpens.md new file mode 100644 index 000000000..655fc8ea7 --- /dev/null +++ b/articles/imagesharp.drawing/brushesandpens.md @@ -0,0 +1,272 @@ +# Brushes and Pens + +Brushes fill covered pixels. Pens define the outline generated when you stroke a path, line, or shape. + +## Solid Brushes and Pens + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(320, 200, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + Rectangle panel = new(30, 28, 140, 92); + canvas.Fill(Brushes.Solid(Color.LightSkyBlue), panel); + canvas.Draw(Pens.Solid(Color.Navy, 4), panel); + + EllipsePolygon ellipse = new(new PointF(230, 118), new SizeF(118, 72)); + canvas.Fill(Brushes.Solid(Color.Gold), ellipse); + canvas.Draw(Pens.Solid(Color.DarkOrange, 5), ellipse); +})); +``` + +## Pattern Brushes and Pattern Pens + +The `Brushes` and `Pens` factories include common hatch and dash styles. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +Brush hatchBrush = Brushes.ForwardDiagonal(Color.DarkSlateGray.WithAlpha(0.72F), Color.Transparent); +Pen dashPen = Pens.Dash(Color.MidnightBlue, 5); +Pen dotPen = Pens.Dot(Color.Crimson, 4); +Pen dashDotPen = Pens.DashDot(Color.Black, 3); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + Rectangle hatchArea = new(28, 28, 160, 138); + canvas.Fill(hatchBrush, hatchArea); + canvas.Draw(dashPen, hatchArea); + + canvas.DrawEllipse(dotPen, new(292, 96), new(170, 92)); + canvas.DrawLine(dashDotPen, new(38, 206), new(150, 178), new(264, 210), new(382, 172)); +})); +``` + +Pattern pens can also use a brush as their stroke fill, which is useful for gradient or hatch-pattern outlines. + +Dash patterns are expressed as multiples of the pen width. The pattern `[3F, 1F]` means draw for three stroke widths, skip for one stroke width, then repeat. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 180, Color.White.ToPixel()); + +PenOptions customDashOptions = new(Brushes.Solid(Color.DarkSlateBlue), 8, [4F, 1F, 1F, 1F]) +{ + StrokeOptions = new() + { + LineCap = LineCap.Round, + LineJoin = LineJoin.Round + } +}; + +PatternPen customDash = new(customDashOptions); + +PathBuilder builder = new(); +builder.AddCubicBezier(new(32, 120), new(118, 18), new(286, 24), new(388, 132)); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + + // The dash array is measured relative to the pen width. + canvas.Draw(customDash, builder.Build()); +})); +``` + +## Gradient Brushes + +Gradient brushes shade fills across space. Use color stops to describe the gradient ramp. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +LinearGradientBrush linear = new( + new(24, 24), + new(220, 150), + GradientRepetitionMode.None, + new(0F, Color.LightYellow), + new(0.5F, Color.DeepSkyBlue), + new(1F, Color.MediumBlue)); + +RadialGradientBrush radial = new( + new(306, 116), + 82F, + GradientRepetitionMode.Reflect, + new(0F, Color.Orange), + new(1F, Color.MediumVioletRed.WithAlpha(0.25F))); + +EllipsePolygon radialShape = new(new PointF(306, 116), new SizeF(156, 112)); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(linear, new Rectangle(24, 24, 190, 132)); + canvas.Fill(radial, radialShape); +})); +``` + +## Image and Matrix Pattern Brushes + +`PatternBrush` repeats a matrix of foreground/background values across the target. Use the `Brushes` helpers for common hatch styles, or construct a `PatternBrush` when you need a custom repeating matrix. + +`ImageBrush` uses an image as the brush source. The source image is not disposed by the brush, so keep it alive for as long as the canvas might replay commands that reference it. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image tile = new(24, 24, Color.Transparent.ToPixel()); +tile.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.LightYellow)); + canvas.DrawLine(Pens.Solid(Color.DarkGoldenrod, 3), new PointF(0, 24), new PointF(24, 0)); +})); + +using Image image = new(420, 220, Color.White.ToPixel()); + +ImageBrush imageBrush = new(tile, new RectangleF(0, 0, 24, 24), new Point(0, 0)); +PatternBrush matrixBrush = new( + Color.DarkSlateGray.WithAlpha(0.75F), + Color.Transparent, + new bool[,] + { + { true, false, false, false }, + { false, true, false, false }, + { false, false, true, false }, + { false, false, false, true } + }); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(imageBrush, new Rectangle(28, 28, 160, 132)); + canvas.Fill(matrixBrush, new Rectangle(232, 28, 160, 132)); +})); +``` + +## Stroke Shape Options + +`StrokeOptions` controls how outlines are generated before rasterization. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 220, Color.White.ToPixel()); + +StrokeOptions strokeOptions = new() +{ + LineJoin = LineJoin.Round, + LineCap = LineCap.Round, + MiterLimit = 4, + ArcDetailScale = 1 +}; + +PenOptions penOptions = new(Brushes.Solid(Color.MidnightBlue), 12, strokePattern: null) +{ + StrokeOptions = strokeOptions +}; + +SolidPen pen = new(penOptions); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.DrawLine(pen, new(40, 170), new(130, 42), new(250, 166), new(374, 54)); +})); +``` + +Pens do not paint centered pixels directly. A pen describes an outline generated from the source path, line, or shape. The generated outline is then filled with the pen's brush. This is why caps, joins, miter limits, dashes, and stroke width belong to the pen. + +Use `Pen.GeneratePath(...)` when you need to inspect or reuse the stroked outline as a shape. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 220, Color.White.ToPixel()); + +PathBuilder builder = new(); +builder.AddLine(new(52, 164), new(178, 44)); +builder.AddLine(new(178, 44), new(328, 166)); + +IPath centerLine = builder.Build(); +Pen outlinePen = Pens.Solid(Color.MediumVioletRed, 18); +IPath outline = outlinePen.GeneratePath(centerLine); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Pink.WithAlpha(0.45F)), outline); + canvas.Draw(Pens.Solid(Color.DarkRed, 2), outline); + canvas.Draw(Pens.Dash(Color.Gray, 1.5F), centerLine); +})); +``` + +## Clipping Brushes and Pens + +Clipping is canvas state, not a brush or pen property. Use `Save(DrawingOptions, params IPath[])` to apply one or more clip paths to later brush and pen commands, then `Restore()` when the clipped work is complete. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +EllipsePolygon clip = new(new PointF(210, 120), new SizeF(300, 150)); +LinearGradientBrush brush = new( + new PointF(40, 40), + new PointF(380, 200), + GradientRepetitionMode.None, + new ColorStop(0F, Color.Gold), + new ColorStop(1F, Color.MediumPurple)); +DrawingOptions clipInside = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + _ = canvas.Save(clipInside, clip); + + // Both the fill and the dashed outline are clipped by the saved canvas state. + canvas.Fill(brush, new Rectangle(32, 34, 356, 172)); + canvas.Draw(Pens.Dash(Color.Black, 5), new Rectangle(32, 34, 356, 172)); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2), clip); +})); +``` diff --git a/articles/imagesharp.drawing/canvas.md b/articles/imagesharp.drawing/canvas.md new file mode 100644 index 000000000..ca91af10f --- /dev/null +++ b/articles/imagesharp.drawing/canvas.md @@ -0,0 +1,386 @@ +# Canvas Drawing + +`DrawingCanvas` is the central drawing surface in ImageSharp.Drawing. You normally use it through `Paint(...)` inside an ImageSharp processing pipeline: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(400, 240, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Clear(Brushes.Solid(Color.White)); + Rectangle panel = new(24, 24, 160, 96); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), panel); + canvas.Draw(Pens.Solid(Color.Black, 3), panel); +})); +``` + +The callback receives a canvas for the current frame. Use the canvas for all drawing work that should happen together. + +## Deferred Drawing and Replay + +`DrawingCanvas` looks immediate, but most drawing commands are recorded first and replayed later. Calls such as `Fill(...)`, `Draw(...)`, `DrawText(...)`, and `SaveLayer(...)` append drawing intent to a command buffer. Calls that must happen at a specific point, such as `Apply(...)` and `RenderScene(...)`, are stored as entries in the canvas replay timeline. + +The root canvas replays the timeline when it is disposed. During replay, command ranges are prepared into backend command batches, and the backend creates and renders scenes for those ranges. This is why a manually-created canvas must be disposed: disposal is the point where recorded work is actually rendered into the target. + +The replay timeline can contain three kinds of entry: + +- command ranges for normal drawing commands +- apply barriers for `Apply(...)` operations +- retained scene references inserted by `RenderScene(...)` + +This deferred model lets ImageSharp.Drawing use one public canvas API for CPU images, WebGPU surfaces, and retained backend scenes. The canvas records drawing intent once, performs shared preparation once, and then hands a stable command batch to the active backend. + +`Flush()` seals the commands recorded so far into a command-range timeline entry. It does not render immediately by itself. Most code does not need it; use it when a later operation must appear after the current commands in replay order. + +```csharp +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.LightGray)); + + // Seal the fill before the blur barrier so the blur sees the filled pixels. + canvas.Flush(); + canvas.Apply(new Rectangle(40, 40, 180, 120), region => region.GaussianBlur(6)); + + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(40, 40, 180, 120)); +})); +``` + +Inside `Paint(...)`, ImageSharp.Drawing owns the canvas lifetime. When you call `CreateCanvas(...)` yourself, your `using` statement is what triggers replay. + +## Paint Versus CreateCanvas + +Use `Paint(...)` for normal `Mutate(...)` and `Clone(...)` pipelines. It follows ImageSharp's processor model and handles each frame for you. + +Use `CreateCanvas(...)` when you already have an image frame and want to manage the canvas lifetime yourself. Disposing the canvas replays the recorded work into the target frame. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = new(320, 180, Color.White.ToPixel()); +using DrawingCanvas canvas = image.Frames.RootFrame.CreateCanvas(image.Configuration, new()); + +canvas.Fill(Brushes.Solid(Color.LightSteelBlue)); +canvas.Draw(Pens.Dash(Color.Navy, 3), new Rectangle(18, 18, 284, 144)); +``` + +## Clear and Fill + +Use `Fill(...)` when you want normal brush compositing. Use `Clear(...)` when you want to replace pixels in the covered area, including replacing them with transparent pixels. + +`Clear(...)` can target the full canvas, a rectangle, or any `IPath`. It also honors the active clip state created by `Save(...)`, so clears can be scoped by both the supplied clear shape and the current canvas state. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(320, 200, Color.Transparent.ToPixel()); +DrawingOptions clipToEllipse = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); + canvas.Fill(Brushes.Solid(Color.Crimson.WithAlpha(0.8F)), new Rectangle(26, 18, 268, 164)); + + EllipsePolygon clip = new(new PointF(160, 100), new SizeF(214, 126)); + _ = canvas.Save(clipToEllipse, clip); + + canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.85F))); + + // Transparent clear removes content inside the supplied path and active clip. + EllipsePolygon cutout = new(new PointF(164, 98), new SizeF(74, 48)); + canvas.Clear(Brushes.Solid(Color.Transparent), cutout); + canvas.Restore(); + + canvas.Draw(Pens.DashDot(Color.Black, 3), clip); +})); +``` + +## State and Storage + +`Save()` stores the current drawing state on a stack and `Restore()` returns to the previous state. The state includes drawing options, clip paths, target bounds, and layer information for later commands. + +The overload `Save(DrawingOptions, params IPath[])` stores the supplied `DrawingOptions` instance by reference. Treat options passed to `Save(...)` as owned by the active canvas state until that state has been restored. + +The active state reference is captured when each command is recorded. Later `Save(...)` or `Restore()` calls do not replace the state for commands already in the command buffer, but mutating a referenced `DrawingOptions` instance can still affect commands that captured that same instance. + +The state captured for drawing includes: + +- `DrawingOptions`, including graphics options, shape options, and transform +- clip paths supplied to `Save(DrawingOptions, params IPath[])` +- target bounds for the active canvas or region +- destination offset for region canvases +- whether the command is being recorded inside a layer + +`Save()` pushes a normal state frame. `SaveLayer(...)` pushes a layer state frame. Only layer state frames create layer boundary commands when restored. + +## Save and Restore State + +`Save(...)` pushes the current drawing state. The overload that accepts `DrawingOptions` and clip paths replaces the active state until you call `Restore()`. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); +DrawingOptions clipInside = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + EllipsePolygon clipPath = new(new PointF(180, 110), new SizeF(260, 140)); + + _ = canvas.Save(clipInside, clipPath); + canvas.Fill(Brushes.Solid(Color.MidnightBlue), new Rectangle(0, 0, 360, 220)); + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.72F)), new Rectangle(56, 38, 248, 144)); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 3), clipPath); +})); +``` + +Use `SaveLayer(...)` when you need an isolated compositing layer that is later blended back onto the parent canvas. + +## Region Canvases + +`CreateRegion(...)` creates a child canvas over a clipped subregion of the parent target. The child canvas has a local origin at `(0, 0)` for drawing commands, but it shares the parent replay timeline. The root canvas still owns final replay. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.LightGray)); + + using DrawingCanvas region = canvas.CreateRegion(new Rectangle(80, 48, 180, 112)); + region.Fill(Brushes.Solid(Color.CornflowerBlue)); + + // Region-local coordinates start at the region origin. + region.Draw(Pens.Solid(Color.White, 5), new Rectangle(12, 12, 156, 88)); + + canvas.Draw(Pens.Solid(Color.Black, 2), new Rectangle(80, 48, 180, 112)); +})); +``` + +Use a region when you want a smaller local coordinate system. Use `Save(...)` with clip paths when you want to keep the parent coordinate system but clip later commands. + +## Layers + +`SaveLayer(...)` starts an isolated composition scope. Commands drawn inside the layer are recorded into that scope, and `Restore()` closes the layer. The closed layer is composited back into the parent using the `GraphicsOptions` supplied to `SaveLayer(...)`. + +Layer bounds limit the isolated target and final composition area. They do not move the canvas origin, so commands inside a bounded layer still use the same local coordinates as the parent canvas. + +A layer is useful when a group of commands must be blended as one result. Without a layer, each command is blended into the parent independently. With a layer, commands first render into an isolated target, then that whole target is composited back once. + +The layer lifecycle is: + +1. `SaveLayer(...)` records a begin-layer command and pushes a layer state. +2. Drawing commands inside the layer are recorded with the layer state. +3. `Restore()` or `RestoreTo(...)` records an end-layer command. +4. Disposal replay asks the backend to lower that layer scope for the target. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.SteelBlue), new Rectangle(24, 24, 312, 172)); + + GraphicsOptions layerOptions = new() + { + BlendPercentage = 0.5F + }; + + _ = canvas.SaveLayer(layerOptions, new Rectangle(70, 46, 220, 128)); + + // The layer bounds isolate composition; these coordinates are still parent-canvas coordinates. + canvas.Fill(Brushes.Solid(Color.OrangeRed), new EllipsePolygon(new PointF(180, 110), new SizeF(170, 96))); + canvas.Draw(Pens.Solid(Color.White, 8), new Rectangle(96, 74, 168, 72)); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 2), new Rectangle(70, 46, 220, 128)); +})); +``` + +If a canvas is disposed while a layer is still active, disposal unwinds the layer using the same path as `Restore()`. + +Use bounded layers deliberately. A smaller layer bounds can reduce the isolated composition area, but anything outside those bounds is not part of that layer's final composition. + +## Draw Images + +`DrawImage(...)` records image drawing through the same canvas timeline as shape and text commands. Pass the source image, a source rectangle, a destination rectangle, and an optional resampler. + +The source rectangle is sampled from the source image and scaled into the destination rectangle. The current transform and clip state apply to the destination drawing. Source rectangles that extend outside the source image are clipped to the available pixels. + +Because canvas drawing is deferred, the source image must remain alive until the canvas has replayed the command. With `Paint(...)`, that means keeping the source image alive for the duration of the `Mutate(...)` call. With a manually-created canvas, keep it alive until the canvas is disposed. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image image = new(420, 260, Color.White.ToPixel()); +DrawingOptions clipInside = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +EllipsePolygon clip = new(new PointF(210, 130), new SizeF(300, 170)); +Rectangle sourceRect = new(20, 12, 240, 180); +RectangleF destination = new(60, 45, 300, 170); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + _ = canvas.Save(clipInside, clip); + + // Bicubic resampling is a good default for scaled photographic content. + canvas.DrawImage(source, sourceRect, destination, KnownResamplers.Bicubic); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 3), clip); +})); +``` + +## Strokes and Command Preparation + +Stroke drawing is prepared during replay. A `Draw(...)` command records the original path, pen, stroke width, dash pattern, caps, joins, and active state. When the canvas prepares the command batch, it normalizes strokes for backend execution. + +Simple solid line segments can stay as line commands. Dashed strokes, paths, joins, caps, and other complex strokes are prepared as stroke path commands or expanded into fillable geometry before backend handoff. Clip paths are applied during preparation so backends receive commands with consistent clipping semantics. + +That means `Draw(...)` and `Fill(...)` share the same backend handoff model even though the public calls describe different drawing intent. Backends receive prepared commands and can focus on rendering them for their target. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 220, Color.White.ToPixel()); + +PathBuilder builder = new(); +builder.AddCubicBezier(new(36, 150), new(116, 32), new(292, 44), new(384, 158)); + +IPath path = builder.Build(); +Pen pen = Pens.DashDot(Color.DarkSlateBlue, 10); +pen.StrokeOptions.LineCap = LineCap.Round; +pen.StrokeOptions.LineJoin = LineJoin.Round; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + + // Dash, cap, and join settings are part of the recorded stroke intent. + canvas.Draw(pen, path); +})); +``` + +Use the pen's `StrokeOptions` for stroke shape: + +- `LineCap` controls open path ends. +- `LineJoin` controls corners. +- `MiterLimit` controls how far miter joins can extend. +- dash pens such as `Pens.Dash(...)` and `Pens.DashDot(...)` record a stroke pattern. + +## Retained Scene Replay + +Use `CreateScene()` when the same recorded drawing should be replayed into more than one canvas target. It seals and prepares the recorded drawing commands into a retained backend scene. `RenderScene(...)` inserts that retained scene into the receiving canvas timeline at the point where it is called. + +The scene is backend-owned state, so keep it alive until every canvas that records it has been disposed. A canvas that receives `RenderScene(...)` still replays on disposal like any other canvas. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; + +using Image source = new(160, 120, Color.Transparent.ToPixel()); +using DrawingCanvas sourceCanvas = source.Frames.RootFrame.CreateCanvas(source.Configuration, new()); + +sourceCanvas.Fill(Brushes.Solid(Color.Gold), new EllipsePolygon(new PointF(80, 60), new SizeF(116, 72))); +sourceCanvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(12, 12, 136, 96)); + +using DrawingBackendScene scene = sourceCanvas.CreateScene(); + +using Image first = new(160, 120, Color.White.ToPixel()); +using DrawingCanvas firstCanvas = first.Frames.RootFrame.CreateCanvas(first.Configuration, new()); +firstCanvas.RenderScene(scene); +firstCanvas.Dispose(); + +using Image second = new(160, 120, Color.LightGray.ToPixel()); +using DrawingCanvas secondCanvas = second.Frames.RootFrame.CreateCanvas(second.Configuration, new()); +secondCanvas.RenderScene(scene); +secondCanvas.Dispose(); +``` + +`RenderScene(...)` preserves timeline order. Commands recorded before it replay before the retained scene; commands recorded after it replay after the retained scene. + +## Apply Image Processing to a Region + +`Apply(...)` runs ImageSharp processors inside a rectangle, path, or path builder region. It is a replay barrier because the processor needs real pixels, not just recorded drawing commands. + +During replay, ImageSharp.Drawing reads the covered target pixels into a temporary image, runs the processor operation on that temporary image, then writes the processed result back through the canvas pipeline using the recorded path and state. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.LightGray)); + canvas.Draw(Pens.Solid(Color.Black, 4), new Rectangle(24, 24, 312, 172)); + + EllipsePolygon blurPath = new(new PointF(180, 110), new SizeF(220, 120)); + + // The blur is clipped to the supplied path region. + canvas.Apply(blurPath, region => region.GaussianBlur(8)); +})); +``` + +Because `Apply(...)` reads pixels at its replay point, commands before the barrier affect the processed image, and commands after the barrier do not. diff --git a/articles/imagesharp.drawing/clipimagetoshape.md b/articles/imagesharp.drawing/clipimagetoshape.md new file mode 100644 index 000000000..47198ba89 --- /dev/null +++ b/articles/imagesharp.drawing/clipimagetoshape.md @@ -0,0 +1,49 @@ +# Clip an Image to a Shape + +Use `Save(DrawingOptions, params IPath[])` with `BooleanOperation.Intersection` when later drawing should be limited to a shape. This is useful for avatars, shaped thumbnails, masked hero images, and photo badges. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("portrait.jpg"); +using Image image = new(360, 360, Color.Transparent.ToPixel()); + +PointF avatarCenter = new(180, 180); +SizeF avatarSize = new(300, 300); +EllipsePolygon avatar = new(avatarCenter, avatarSize); +RectangleF destination = new(30, 30, 300, 300); +DrawingOptions clipInside = new() +{ + ShapeOptions = new() + { + // Intersection keeps the image draw inside the avatar path instead of subtracting it. + BooleanOperation = BooleanOperation.Intersection + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Save(clipInside, avatar); + + // The active clip limits the photo to the ellipse while DrawImage handles resizing. + canvas.DrawImage(source, source.Bounds, destination, KnownResamplers.Bicubic); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.White, 8), avatar); + canvas.Draw(Pens.Solid(Color.DarkSlateGray.WithAlpha(0.4F), 2), avatar); +})); + +image.Save("avatar.png"); +``` + +Keep the source image alive until the drawing operation has replayed. The `Paint(...)` pipeline handles the canvas lifetime for this example. + +## Related Topics + +- [Clipping, Regions, and Layers](clippingregionslayers.md) +- [Images, Masks, and Processing](imagesandprocessing.md) +- [Troubleshooting](troubleshooting.md) diff --git a/articles/imagesharp.drawing/clippingregionslayers.md b/articles/imagesharp.drawing/clippingregionslayers.md new file mode 100644 index 000000000..0dfc39a7b --- /dev/null +++ b/articles/imagesharp.drawing/clippingregionslayers.md @@ -0,0 +1,101 @@ +# Clipping, Regions, and Layers + +Canvas state controls where later commands can draw and how grouped commands are composed. The three main tools are `Save(...)` with clip paths, `CreateRegion(...)`, and `SaveLayer(...)`. + +## Clip Later Commands + +`Save(DrawingOptions, params IPath[])` pushes a new state with the supplied options and clip paths. The clip paths are combined with each command by `ShapeOptions.BooleanOperation`. + +The default boolean operation is `Difference`, which subtracts the clip path. For ordinary "draw inside this shape" clipping, set `BooleanOperation.Intersection`. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 260, Color.White.ToPixel()); + +EllipsePolygon spotlight = new(new PointF(210, 130), new SizeF(300, 160)); +DrawingOptions clipInside = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.MidnightBlue)); + + _ = canvas.Save(clipInside, spotlight); + + // The rectangle is larger than the ellipse; the saved state keeps only the intersection. + canvas.Fill(Brushes.Horizontal(Color.Gold, Color.OrangeRed), new Rectangle(20, 40, 380, 180)); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.White, 3), spotlight); +})); +``` + +Use `Restore()` to pop the latest state, or `RestoreTo(saveCount)` when nested states must be unwound together. + +## Region Canvases + +`CreateRegion(...)` creates a child canvas with local coordinates inside a rectangular area. It is useful for controls, panels, tiles, thumbnails, and other sub-layouts where `(0, 0)` should mean the region origin. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + using DrawingCanvas region = canvas.CreateRegion(new Rectangle(70, 48, 220, 124)); + + region.Fill(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(10, 10, 120, 68)); + + // Region-local coordinates are relative to the region, not the parent canvas. + region.Draw(Pens.Solid(Color.DarkBlue, 5), new Rectangle(0, 0, 220, 124)); + region.DrawLine(Pens.Solid(Color.OrangeRed, 4), new PointF(0, 123), new PointF(219, 0)); +})); +``` + +Nested regions can also have their own saved state. The root canvas still owns final replay, so disposing a child region does not render the whole image immediately. + +## Layers + +`SaveLayer(...)` starts an isolated compositing scope. Commands drawn inside the layer render into that layer, then `Restore()` composites the layer back to the parent with the supplied `GraphicsOptions`. + +Layer bounds limit the isolated target and final composition area. They do not shift the coordinate system. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.SteelBlue), new Rectangle(24, 24, 312, 172)); + + _ = canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.55F }, new Rectangle(70, 46, 220, 128)); + + // Layer bounds constrain compositing; these coordinates are still parent coordinates. + canvas.Fill(Brushes.Solid(Color.OrangeRed), new EllipsePolygon(new PointF(180, 110), new SizeF(170, 96))); + canvas.Draw(Pens.Solid(Color.White, 8), new Rectangle(96, 74, 168, 72)); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 2), new Rectangle(70, 46, 220, 128)); +})); +``` + +Use layers when a group of commands should blend back as one result. Without a layer, each command blends into the parent independently. diff --git a/articles/imagesharp.drawing/gettingstarted.md b/articles/imagesharp.drawing/gettingstarted.md index 0e98544b3..7bd412fbb 100644 --- a/articles/imagesharp.drawing/gettingstarted.md +++ b/articles/imagesharp.drawing/gettingstarted.md @@ -1,114 +1,161 @@ # Getting Started >[!NOTE] ->The official guide assumes intermediate level knowledge of C# and .NET. If you are totally new to .NET development, it might not be the best idea to jump right into a framework as your first step - grasp the basics then come back. Prior experience with other languages and frameworks helps, but is not required. +>This guide assumes intermediate C# and .NET knowledge. If you are new to .NET, start with the language and runtime basics first, then come back to the image and drawing APIs. -### ImageSharp.Drawing - Paths and Polygons +ImageSharp.Drawing adds vector drawing, brush and pen styling, and text rendering to ImageSharp. The main workflow is: -ImageSharp.Drawing provides several classes for building and manipulating various shapes and paths. +1. Create or load an `Image`. +2. Call `Mutate(...)`. +3. Use `Paint(...)` to receive a `DrawingCanvas`. +4. Draw onto the canvas with brushes, pens, paths, shapes, images, or text. -- @"SixLabors.ImageSharp.Drawing.IPath" Root interface defining a path/polygon and the type that the rasterizer uses to generate pixel output. -- This `SixLabors.ImageSharp.Drawing` namespace contains a variety of available polygons to speed up your drawing process. +The same canvas can mix all of those operations. This model scales from small badges to poster-style artwork, route maps, typography sheets, image masking, and WebGPU scenes. -In addition to the vector manipulation APIs the library also contains rasterization APIs that can convert your @"SixLabors.ImageSharp.Drawing.IPath"s to pixels. +## Draw a Shape -### Drawing Polygons - -ImageSharp provides several options for drawing polygons whether you want to draw outlines or fill shapes. - -#### Minimal Example - -```c# +```csharp using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; -Image image = ...; // create any way you like. +using Image image = new(320, 200, Color.White.ToPixel()); -Star star = new(x: 100.0f, y: 100.0f, prongs: 5, innerRadii: 20.0f, outerRadii:30.0f); +Star star = new(x: 160, y: 100, prongs: 5, innerRadii: 42, outerRadii: 86); +Pen outline = Pens.DashDot(Color.MidnightBlue, 4); -image.Mutate( x=> x.Fill(Color.Red, star)); // fill the star with red +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Gold), star); + canvas.Draw(outline, star); +})); +image.Save("star.png"); ``` -#### Expanded Example +`Paint(...)` creates a canvas for each frame being processed. Drawing is recorded through that canvas and applied when the paint operation runs. + +## Combine Drawing Operations + +Most real compositions combine background fills, path drawing, text, image drawing, clipping, and image processors. Keep the source images and brushes alive until the `Paint(...)` call has completed because the canvas records commands first and replays them later. -```c# +```csharp +using SixLabors.Fonts; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; -Image image = ...; // Create any way you like. +using Image source = Image.Load("photo.jpg"); +using Image image = new(640, 360, Color.White.ToPixel()); -// The options are optional -DrawingOptions options = new() +Font font = SystemFonts.CreateFont("Arial", 34); +RichTextOptions titleOptions = new(font) { - GraphicsOptions = new() + Origin = new(40, 42), + WrappingLength = 560, + HorizontalAlignment = HorizontalAlignment.Center +}; + +EllipsePolygon focus = new(new PointF(320, 195), new SizeF(360, 190)); +RectangleF photoArea = new(80, 92, 480, 230); +DrawingOptions clipToFocus = new() +{ + ShapeOptions = new() { - ColorBlendingMode = PixelColorBlendingMode.Multiply + BooleanOperation = BooleanOperation.Intersection } }; -PatternBrush brush = Brushes.Horizontal(Color.Red, Color.Blue); -PatternPen pen = Pens.DashDot(Color.Green, 5); -Star star = new(x: 100.0f, y: 100.0f, prongs: 5, innerRadii: 20.0f, outerRadii:30.0f); - -// Draws a star with horizontal red and blue hatching with a dash-dot pattern outline. -image.Mutate(x=> x.Fill(options, brush, star) - .Draw(option, pen, star)); -``` +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.AliceBlue)); + canvas.DrawText(titleOptions, "Clipped photo with local processing", Brushes.Solid(Color.MidnightBlue), pen: null); -### API Cornerstones for Polygon Rasterization -Our `Fill` APIs always work off a `Brush` (some helpers create the brush for you) and will take your provided set of paths and polygons filling in all the pixels inside the vector with the color the brush provides. + _ = canvas.Save(clipToFocus, focus); -Our `Draw` APIs always work off the `Pen` where we processes your vector to create an outline with a certain pattern and fill in the outline with an internal brush inside the pen. + // DrawImage scales the selected source rectangle into the destination rectangle. + canvas.DrawImage(source, source.Bounds, photoArea, KnownResamplers.Bicubic); + canvas.Apply(focus, region => region.GaussianBlur(3)); + canvas.Restore(); + canvas.Draw(Pens.Solid(Color.DarkSlateBlue, 3), focus); +})); -### Drawing Text +image.Save("composition.png"); +``` -ImageSharp.Drawing provides several options for drawing text all overloads of a single `DrawText` API. Our text drawing infrastructure is build on top of our [Fonts](../fonts/index.md) library. (See [SixLabors.Fonts](../fonts/index.md) for details on handling fonts.) +## Use Drawing Options -#### Minimal Example +`DrawingOptions` controls the shared drawing state used by the canvas. The most common settings are graphics options for blending and antialiasing, shape options for fill behavior, and transforms for vector output. -```c# -using SixLabors.Fonts; +```csharp +using System.Numerics; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(320, 200, Color.White.ToPixel()); -Image image = ...; // Create any way you like. -Font font = ...; // See our Fonts library for best practices on retrieving one of these. -string yourText = "this is some sample text"; +DrawingOptions options = new() +{ + GraphicsOptions = new() + { + Antialias = true, + BlendPercentage = 0.85F + }, + + // Transform is applied to vector output before rasterization. + Transform = new(Matrix3x2.CreateRotation(-0.18F, new(160, 100))) +}; -image.Mutate(x=> x.DrawText(yourText, font, Color.Black, new PointF(10, 10))); +EllipsePolygon shape = new(new PointF(160, 100), new SizeF(210, 96)); +Brush brush = Brushes.Horizontal(Color.DeepSkyBlue, Color.Navy); + +image.Mutate(ctx => ctx.Paint(options, canvas => +{ + canvas.Fill(brush, shape); + canvas.Draw(Pens.Solid(Color.Black, 3), shape); +})); ``` -#### Expanded Example +## Draw Text + +Text drawing uses SixLabors.Fonts for font discovery, shaping, measurement, and layout. Use `RichTextOptions` when you draw directly to a canvas. -```c# +```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; -Image image = ...; // Create any way you like. -Font font = ...; // See our Fonts library for best practices on retrieving one of these. +using Image image = new(640, 240, Color.White.ToPixel()); -// The options are optional -RichTextOptions options = new(font) +Font font = SystemFonts.CreateFont("Arial", 42); +RichTextOptions textOptions = new(font) { - Origin = new PointF(100, 100), // Set the rendering origin. - TabWidth = 8, // A tab renders as 8 spaces wide - WrappingLength = 100, // Greater than zero so we will word wrap at 100 pixels wide - HorizontalAlignment = HorizontalAlignment.Right // Right align + Origin = new(48, 70), + WrappingLength = 540, + HorizontalAlignment = HorizontalAlignment.Center }; -PatternBrush brush = Brushes.Horizontal(Color.Red, Color.Blue); -PatternPen pen = Pens.DashDot(Color.Green, 5); -string text = "sample text"; - -// Draws the text with horizontal red and blue hatching with a dash-dot pattern outline. -image.Mutate(x=> x.DrawText(options, text, brush, pen)); +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.DrawText(textOptions, "Drawing text with ImageSharp", Brushes.Solid(Color.Black), pen: null); +})); ``` + +For deeper text guidance, see the [Fonts](../fonts/index.md) docs. + +## Next Steps + +- [Canvas Drawing](canvas.md) +- [Paths and Shapes](pathsandshapes.md) +- [Brushes and Pens](brushesandpens.md) +- [Drawing Text](text.md) diff --git a/articles/imagesharp.drawing/imagesandprocessing.md b/articles/imagesharp.drawing/imagesandprocessing.md new file mode 100644 index 000000000..937dc1081 --- /dev/null +++ b/articles/imagesharp.drawing/imagesandprocessing.md @@ -0,0 +1,97 @@ +# Images, Masks, and Processing + +ImageSharp.Drawing can draw images through the canvas, use images as brushes, and run ImageSharp processors inside drawing regions. + +## Draw an Image + +`DrawImage(...)` copies a source rectangle from an image into a destination rectangle on the canvas. The destination is affected by the current transform and clip state. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image image = new(480, 300, Color.White.ToPixel()); + +DrawingOptions clipInside = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +EllipsePolygon clip = new(new PointF(240, 150), new SizeF(340, 190)); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + _ = canvas.Save(clipInside, clip); + + // The selected source pixels are scaled into the destination rectangle. + canvas.DrawImage(source, new Rectangle(20, 10, 280, 180), new RectangleF(70, 54, 340, 190), KnownResamplers.Bicubic); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 3), clip); +})); +``` + +Keep the source image alive until the canvas has replayed the command. With `Paint(...)`, that means the source must remain alive until `Mutate(...)` completes. + +## Use an Image as a Brush + +Use `ImageBrush` when an image should fill any path as a texture. This is different from `DrawImage(...)`: the brush samples image pixels while the supplied path controls coverage. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image image = new(420, 260, Color.White.ToPixel()); + +Star star = new(x: 210, y: 130, prongs: 5, innerRadii: 62, outerRadii: 118); +RectangleF sourceRegion = new(0, 0, source.Width, source.Height); +ImageBrush brush = new(source, sourceRegion, new Point(-120, -70)); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + + // The star path controls coverage; the brush supplies the sampled image pixels. + canvas.Fill(brush, star); + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 3), star); +})); +``` + +## Apply Processors Inside a Shape + +`Apply(...)` runs normal ImageSharp processors inside a rectangle, path, or path builder. It is a replay barrier: commands before it affect the pixels being processed, and commands after it do not. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image image = new(520, 320, Color.White.ToPixel()); + +RectangleF destination = new(40, 38, 440, 244); +EllipsePolygon redaction = new(new PointF(300, 168), new SizeF(150, 96)); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.DrawImage(source, source.Bounds, destination, KnownResamplers.Bicubic); + + // Apply scopes the processor to the path; pixels outside the ellipse stay unchanged. + canvas.Apply(redaction, region => region.Pixelate(10)); + canvas.Draw(Pens.Solid(Color.OrangeRed, 3), redaction); +})); +``` + +On GPU-backed canvases, `Apply(...)` requires the affected pixels to be read back, processed by the CPU pipeline, and written back before presentation. Keep regions as small as the effect allows. diff --git a/articles/imagesharp.drawing/index.md b/articles/imagesharp.drawing/index.md index 491c9025a..d65308cb2 100644 --- a/articles/imagesharp.drawing/index.md +++ b/articles/imagesharp.drawing/index.md @@ -1,20 +1,35 @@ # Introduction ### What is ImageSharp.Drawing? -ImageSharp.Drawing is a library built on top of ImageSharp to providing 2D Drawing extensions. +ImageSharp.Drawing is a library built on top of ImageSharp to provide 2D drawing extensions. -ImageSharp.Drawing is designed from the ground up to be flexible and extensible. The library provides API endpoints for common vector and text processing operations adding the building blocks for building custom images. +ImageSharp.Drawing is designed from the ground up to be high-performance, flexible, and extensible. It provides vector geometry, brush and pen styling, canvas drawing, image compositing, and text rendering building blocks for custom images. -Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios. - -### License +### Start Here + +- [Getting Started](gettingstarted.md) introduces the `Paint(...)` and `DrawingCanvas` workflow. +- [Canvas Drawing](canvas.md) covers canvas state, clipping, regions, and applying ImageSharp processors to drawn regions. +- [Primitive Drawing Helpers](primitives.md) covers rectangles, ellipses, arcs, pies, lines, and Bezier helpers. +- [Paths and Shapes](pathsandshapes.md) covers built-in shapes, custom paths, and fill rules. +- [Brushes and Pens](brushesandpens.md) covers solid, pattern, and gradient fills plus stroke options. +- [Clipping, Regions, and Layers](clippingregionslayers.md) covers clip paths, region canvases, save/restore state, and isolated layer composition. +- [Images, Masks, and Processing](imagesandprocessing.md) covers `DrawImage(...)`, image brushes, clipping masks, and `Apply(...)`. +- [Transforms and Composition](transformsandcomposition.md) covers transforms, blending, alpha composition, and antialiasing. +- [Drawing Text](text.md) covers `RichTextOptions`, measuring, and text along paths. +- [WebGPU](webgpu.md) covers GPU-backed windows, external surfaces, and offscreen render targets. +- [Recipes](recipes.md) provides copy-pasteable solutions for common drawing tasks. +- [Troubleshooting](troubleshooting.md) covers common canvas, clipping, text, image, and WebGPU issues. + +Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios. + +### License ImageSharp.Drawing is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] >Starting with ImageSharp.Drawing 3.0.0, projects that directly depend on ImageSharp.Drawing require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. - + ### Installation - + ImageSharp.Drawing is installed via [NuGet](https://www.nuget.org/packages/SixLabors.ImageSharp.Drawing) with nightly builds available on [Feedz](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json). # [Package Manager](#tab/tabid-1) diff --git a/articles/imagesharp.drawing/pathsandshapes.md b/articles/imagesharp.drawing/pathsandshapes.md new file mode 100644 index 000000000..cd4a9566a --- /dev/null +++ b/articles/imagesharp.drawing/pathsandshapes.md @@ -0,0 +1,320 @@ +# Paths and Shapes + +ImageSharp.Drawing separates geometry from painting. Shapes and paths describe where drawing happens; brushes and pens describe how pixels are shaded. + +The core geometry types are: + +- `IPath` for any path-like shape that can be filled or stroked. +- `Path` for an open path made from line segments, arcs, and curves. +- `Polygon` for a closed path. +- `ComplexPolygon` for a shape made from multiple paths, such as an outer contour with holes. +- `Polygon`, `RectangularPolygon`, `EllipsePolygon`, `RegularPolygon`, `Star`, and `Pie` for common shapes. +- `PathBuilder` when you want to construct a custom path from line and curve commands. +- `PathCollection` when one operation should cover several paths. + +`IPath.PathType` tells you whether a path is open, closed, or mixed. A mixed path is a composite path containing both open and closed figures. + +## Built-In Shapes + +Built-in shape types are closed paths. They can be filled directly and stroked with a pen. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 260, Color.White.ToPixel()); + +EllipsePolygon ellipse = new(new PointF(120, 110), new SizeF(160, 96)); +Star star = new(x: 292, y: 128, prongs: 7, innerRadii: 34, outerRadii: 72); +Pie pie = new(new PointF(120, 202), new SizeF(120, 86), startAngle: -30, sweepAngle: 245); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.SkyBlue), ellipse); + canvas.Draw(Pens.Solid(Color.Navy, 3), ellipse); + + canvas.Fill(Brushes.Solid(Color.Orange), star); + canvas.Draw(Pens.Solid(Color.DarkRed, 3), star); + + canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), pie); + canvas.Draw(Pens.Solid(Color.DarkGreen, 3), pie); +})); +``` + +## Open and Closed Paths + +Open paths are useful for strokes, polylines, and curved baselines. Closed paths enclose an area and are the normal input for fills. + +`Path` is open by default. `Polygon` is closed. `PathBuilder.CloseFigure()` closes the current figure before starting the next one. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(440, 220, Color.White.ToPixel()); + +PathBuilder openBuilder = new(); +openBuilder.AddCubicBezier( + new(36, 152), + new(116, 34), + new(252, 38), + new(396, 154)); + +IPath openPath = openBuilder.Build(); + +PathBuilder closedBuilder = new(); +closedBuilder.AddLines(new(64, 174), new(154, 54), new(244, 174)); +closedBuilder.CloseFigure(); + +IPath closedPath = closedBuilder.Build(); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Draw(Pens.Solid(Color.MidnightBlue, 8), openPath); + + // Closed figures can be filled because they define an inside area. + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.6F)), closedPath); + canvas.Draw(Pens.Solid(Color.DarkGoldenrod, 4), closedPath); +})); +``` + +When you fill an open path, ImageSharp.Drawing closes it for fill processing. Prefer building the figure as closed when the intended geometry is a filled area; that keeps the model clear and also gives stroke joins closed-contour behavior. + +## Custom Paths and Figures + +Use `PathBuilder` for custom geometry. Build the path once, then reuse it for fill and stroke operations. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 220, Color.White.ToPixel()); + +PathBuilder builder = new(); +builder.AddLines(new(42, 176), new(112, 36), new(210, 154)); +builder.AddCubicBezier( + new(210, 154), + new(268, 46), + new(336, 50), + new(376, 164)); + +IPath path = builder.Build(); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Draw(Pens.Solid(Color.MidnightBlue, 8), path); + canvas.Draw(Pens.Dot(Color.White, 3), path); +})); +``` + +`PathBuilder` supports multiple figures. If the builder contains more than one figure, `Build()` returns a `ComplexPolygon`. Each figure keeps its own open or closed state. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +PathBuilder builder = new(); +builder.AddLines(new(52, 190), new(122, 54), new(196, 190)); +builder.CloseFigure(); + +builder.AddCubicBezier( + new(236, 178), + new(268, 38), + new(336, 48), + new(374, 178)); + +IPath mixedPath = builder.Build(); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.LightSkyBlue), mixedPath); + canvas.Draw(Pens.Solid(Color.Navy, 5), mixedPath); +})); +``` + +Use `PathBuilder.StartFigure()` when you want to begin a new figure without closing the previous one. Use `CloseAllFigures()` when every current figure should be closed. + +## Complex Polygons and Holes + +`ComplexPolygon` represents multiple paths as one path. It is useful when a shape has multiple contours, or when you want to model an outer contour and one or more holes. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +Polygon outer = new( +[ + new PointF(60, 36), + new PointF(360, 36), + new PointF(360, 204), + new PointF(60, 204) +]); + +EllipsePolygon hole = new(new PointF(210, 120), new SizeF(178, 96)); +ComplexPolygon complex = new(outer, hole); + +DrawingOptions options = new() +{ + ShapeOptions = new() + { + IntersectionRule = IntersectionRule.EvenOdd + } +}; + +image.Mutate(ctx => ctx.Paint(options, canvas => +{ + canvas.Fill(Brushes.Solid(Color.MediumPurple), complex); + canvas.Draw(Pens.Solid(Color.Black, 3), complex); +})); +``` + +The fill rule decides how overlapping contours inside a complex polygon are interpreted. `NonZero` is the default and matches the usual SVG and web canvas behavior: contour winding is meaningful, so holes are normally expressed by winding the inner contour in the opposite direction to its parent. Use `EvenOdd` when you want parity-based holes where contour direction is not significant. + +## Path Collections + +`PathCollection` groups paths so one draw or fill call can apply the same brush, pen, and drawing state to all of them. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 220, Color.White.ToPixel()); + +PathCollection bubbles = new( + new EllipsePolygon(new PointF(104, 112), new SizeF(96, 72)), + new EllipsePolygon(new PointF(210, 92), new SizeF(126, 86)), + new EllipsePolygon(new PointF(316, 126), new SizeF(104, 78))); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.LightCyan), bubbles); + canvas.Draw(Pens.Solid(Color.DarkSlateBlue, 3), bubbles); +})); +``` + +Use a `PathCollection` when paths remain independent. Use `ComplexPolygon` or `Clip(...)` when the contours need to be interpreted together as one shape. + +## Clipping and Boolean Operations + +`Clip(...)` creates a new path from a subject path and one or more clipping paths. The operation comes from `ShapeOptions.BooleanOperation`. The default boolean operation is `Difference`, which subtracts the clipping paths from the subject. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +EllipsePolygon subject = new(new PointF(190, 120), new SizeF(260, 154)); +Star cutout = new(x: 226, y: 120, prongs: 6, innerRadii: 38, outerRadii: 82); + +ShapeOptions clipOptions = new() +{ + BooleanOperation = BooleanOperation.Difference +}; + +IPath clipped = subject.Clip(clipOptions, cutout); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Orange), clipped); + canvas.Draw(Pens.Solid(Color.DarkRed, 3), clipped); +})); +``` + +Use `BooleanOperation.Intersection` when you want only the overlap, `Union` when you want to merge shapes, and `Xor` when you want areas covered by exactly one side. + +## Inspecting and Reusing Geometry + +Paths are reusable geometry objects. You can measure them, transform them, convert open paths to closed paths, and use the same geometry for fills, strokes, clipping, and text paths. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +PathBuilder builder = new(); +builder.AddLines(new(70, 178), new(132, 54), new(226, 172), new(318, 66), new(368, 178)); + +IPath openPath = builder.Build(); +IPath closedPath = openPath.AsClosedPath(); +IPath shiftedPath = closedPath.Translate(0, 24); + +float length = openPath.ComputeLength(); +float area = closedPath.ComputeArea(); +RectangleF bounds = shiftedPath.Bounds; +RectangularPolygon boundsPath = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + + // Use measurements to draw simple diagnostics around the transformed geometry. + canvas.Fill(Brushes.Solid(Color.LightGoldenrodYellow), shiftedPath); + canvas.Draw(Pens.Solid(Color.DarkGoldenrod, 4), shiftedPath); + canvas.Draw(Pens.Dash(Color.Gray, 2), boundsPath); +})); +``` + +`ComputeLength()` follows open and closed contours. `ComputeArea()` is meaningful for closed shapes. `Transform(...)` applies an arbitrary matrix, while helpers such as `Translate(...)`, `Scale(...)`, and `RotateDegree(...)` cover common transforms. + +## Fill Rules + +`ShapeOptions.IntersectionRule` controls how overlapping contours are interpreted during fill operations. `NonZero` is the default, matching the normal SVG and web canvas fill-rule default. Use `EvenOdd` when you explicitly want alternating inside/outside behavior for nested or overlapping contours. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 220, Color.White.ToPixel()); + +EllipsePolygon outer = new(new PointF(180, 110), new SizeF(260, 150)); +EllipsePolygon inner = new(new PointF(180, 110), new SizeF(126, 76)); +PathCollection shape = new(outer, inner); + +DrawingOptions options = new() +{ + ShapeOptions = new() + { + IntersectionRule = IntersectionRule.EvenOdd + } +}; + +image.Mutate(ctx => ctx.Paint(options, canvas => +{ + canvas.Fill(Brushes.Solid(Color.HotPink), shape); + canvas.Draw(Pens.Solid(Color.Black, 3), shape); +})); +``` + +For lower-level polygon boolean operations, see [PolygonClipper](../polygonclipper/index.md). diff --git a/articles/imagesharp.drawing/primitives.md b/articles/imagesharp.drawing/primitives.md new file mode 100644 index 000000000..4765d5860 --- /dev/null +++ b/articles/imagesharp.drawing/primitives.md @@ -0,0 +1,86 @@ +# Primitive Drawing Helpers + +Primitive helpers are convenience methods on `DrawingCanvas` for common geometry. Use them when the shape is simple and you do not need to keep an `IPath` instance around. + +The helpers still follow the same rules as path drawing: fills use brushes, strokes use pens, `DrawingOptions` controls antialiasing and transforms, and active canvas state applies to the recorded command. + +## Rectangles, Ellipses, Lines, and Beziers + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 240, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(16, 16, 328, 208)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(180, 120), new SizeF(170, 100)); + + // DrawLine accepts a polyline, so each point after the first extends the same stroke. + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(28, 206), + new PointF(110, 46), + new PointF(248, 188), + new PointF(332, 34)); + + // DrawBezier is useful for one cubic curve; use PathBuilder for longer paths. + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(32, 126), + new PointF(88, 30), + new PointF(258, 210), + new PointF(326, 118)); +})); +``` + +## Arcs and Pies + +Arc and pie helpers take a center point, a size, a rotation angle, a start angle, and a sweep angle. Positive and negative sweeps are both valid, which makes clockwise and counter-clockwise segments easy to express. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 240, Color.White.ToPixel()); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.FillArc( + Brushes.Solid(Color.CornflowerBlue), + new PointF(112, 92), + new SizeF(84, 58), + rotation: 15, + startAngle: -30, + sweepAngle: 240); + + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(224, 92), + new SizeF(116, 62), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + + // Pie helpers connect the arc back to the center, creating a wedge. + canvas.FillPie(Brushes.Solid(Color.Goldenrod), new PointF(118, 172), new SizeF(58, 58), startAngle: 20, sweepAngle: 240); + canvas.DrawPie(Pens.Solid(Color.DarkSlateBlue, 6), new PointF(236, 170), new SizeF(62, 48), startAngle: 35, sweepAngle: -210); +})); +``` + +## When to Use Paths Instead + +Use `PathBuilder`, `Polygon`, `ComplexPolygon`, or a built-in shape type when you need to: + +- reuse or transform the same geometry; +- combine multiple figures into one shape; +- choose fill rules for overlapping contours; +- stroke the generated outline with `Pen.GeneratePath(...)`; +- measure bounds, length, or area before drawing. + +The primitive helpers are best for direct one-off drawing. Paths are better when the geometry is part of the model. diff --git a/articles/imagesharp.drawing/recipes.md b/articles/imagesharp.drawing/recipes.md new file mode 100644 index 000000000..591afd7e8 --- /dev/null +++ b/articles/imagesharp.drawing/recipes.md @@ -0,0 +1,19 @@ +# Recipes + +These pages are the quick-start side of the ImageSharp.Drawing docs. They focus on practical drawing tasks that combine canvas commands, brushes, pens, images, text, clipping, and processors. + +## Common Tasks + +- [Add a Text Watermark](watermark.md) for anchored, semi-transparent text over an image. +- [Clip an Image to a Shape](clipimagetoshape.md) for avatar crops, badges, and shaped image fills. +- [Draw a Badge or Label](badge.md) for small generated graphics with shapes, strokes, and text. +- [Add Callouts and Annotations](annotations.md) for overlays, markers, outlines, and dashed guides. +- [Create a Soft Shadow](softshadow.md) for shadowed panels and grouped drawing effects. + +## Related Topics + +- [Canvas Drawing](canvas.md) +- [Brushes and Pens](brushesandpens.md) +- [Clipping, Regions, and Layers](clippingregionslayers.md) +- [Images, Masks, and Processing](imagesandprocessing.md) +- [Drawing Text](text.md) diff --git a/articles/imagesharp.drawing/softshadow.md b/articles/imagesharp.drawing/softshadow.md new file mode 100644 index 000000000..21f4229c2 --- /dev/null +++ b/articles/imagesharp.drawing/softshadow.md @@ -0,0 +1,38 @@ +# Create a Soft Shadow + +Draw the shadow shape first, flush it, then apply a blur to the shadow region before drawing the foreground object. `Apply(...)` is a replay barrier, so only commands recorded before the barrier are processed. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +Rectangle shadowBounds = new(70, 72, 280, 110); +Rectangle panelBounds = new(62, 58, 280, 110); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.35F)), shadowBounds); + + // Flush makes the shadow pixels available to the blur barrier before the panel is drawn. + canvas.Flush(); + canvas.Apply(shadowBounds, region => region.GaussianBlur(10)); + + canvas.Fill(Brushes.Solid(Color.White), panelBounds); + canvas.Draw(Pens.Solid(Color.LightGray, 1), panelBounds); +})); + +image.Save("shadow.png"); +``` + +Keep the blur region tight. On CPU canvases this reduces the amount of image data processed, and on GPU-backed canvases it reduces readback and upload work. + +## Related Topics + +- [Canvas Drawing](canvas.md) +- [Images, Masks, and Processing](imagesandprocessing.md) +- [Transforms and Composition](transformsandcomposition.md) diff --git a/articles/imagesharp.drawing/text.md b/articles/imagesharp.drawing/text.md new file mode 100644 index 000000000..5b565e976 --- /dev/null +++ b/articles/imagesharp.drawing/text.md @@ -0,0 +1,300 @@ +# Drawing Text + +ImageSharp.Drawing exposes a high-performance text drawing API that is unusually rich for a 2D image library. It combines the text engine from SixLabors.Fonts with the canvas drawing model, so shaped text, fallback fonts, color fonts, bidirectional layout, wrapping, alignment, rich runs, filled glyphs, stroked glyphs, decorations, path text, and glyph geometry all flow through `DrawingCanvas.DrawText(...)`. + +Use the [Fonts](../fonts/index.md) docs for font loading and text-layout details. This page focuses on placing that text onto an image. + +At the simple end, text is one call. At the advanced end, the same model can draw a multilingual paragraph with per-run fonts, brushes, pens, decorations, and layout options, or turn glyphs into paths for clipping and compositing. + +## Draw Simple Text + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(640, 240, Color.White.ToPixel()); + +Font font = SystemFonts.CreateFont("Arial", 46); +RichTextOptions options = new(font) +{ + Origin = new(48, 72) +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.DrawText(options, "Hello from ImageSharp.Drawing", Brushes.Solid(Color.Black), pen: null); +})); +``` + +Pass a brush to fill glyphs, a pen to outline glyphs, or both. + +## Draw Rich Text + +`RichTextOptions.TextRuns` lets one string carry multiple visual styles without manually splitting and positioning each span. Runs can change font, brush, pen, decorations, and other text features while the layout engine still wraps and aligns the text as one paragraph. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(760, 260, Color.White.ToPixel()); + +Font body = SystemFonts.CreateFont("Arial", 34); +Font emphasis = SystemFonts.CreateFont("Arial", 40, FontStyle.Bold); +const string text = "Rich text can mix fill, outline, and decoration in one layout."; + +RichTextOptions options = new(body) +{ + Origin = new(48, 48), + WrappingLength = 664, + LineSpacing = 1.15F, + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = 9, + Font = emphasis, + Brush = Brushes.Solid(Color.MidnightBlue), + Pen = Pens.Solid(Color.Gold, 1.5F) + }, + + new RichTextRun + { + Start = 18, + End = 22, + Brush = Brushes.Solid(Color.DarkRed), + TextDecorations = TextDecorations.Underline, + UnderlinePen = Pens.Solid(Color.DarkRed, 2) + }, + + new RichTextRun + { + Start = 24, + End = 31, + Brush = Brushes.Solid(Color.DarkGreen), + Pen = Pens.Solid(Color.LightGreen, 1) + }, + + new RichTextRun + { + Start = 37, + End = 47, + Brush = Brushes.Solid(Color.DarkGoldenrod), + TextDecorations = TextDecorations.Overline, + OverlinePen = Pens.Solid(Color.DarkGoldenrod, 2) + } + ] +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + + // Runs style spans; DrawText still shapes, wraps, and aligns the paragraph as one layout. + canvas.DrawText(options, text, Brushes.Solid(Color.Black), pen: null); +})); +``` + +Run indices are counted in grapheme clusters, not UTF-16 code units. `Start` is inclusive and `End` is exclusive, so each run covers the `[Start, End)` grapheme range. For plain ASCII those values match character positions; for emoji, combining marks, and complex scripts, count grapheme clusters as shown in the [Fonts Unicode docs](../fonts/unicode.md). + +## Draw Prepared Text + +Use [TextBlock](../fonts/textblock.md) when the same text will be measured, wrapped, inspected, or drawn more than once. `TextBlock` keeps the prepared text layout work in the Fonts layer, and `DrawingCanvas.DrawText(...)` places that prepared block onto the canvas. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(640, 260, Color.White.ToPixel()); + +Font font = SystemFonts.CreateFont("Arial", 32); +RichTextOptions options = new(font) +{ + Origin = new(0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + LineSpacing = 1.15F +}; + +TextBlock block = new("Prepared text can be measured and drawn with the same shaping.", options); +TextMetrics metrics = block.Measure(wrappingLength: 520); +RectangularPolygon layoutBox = new(60, 48, 520, metrics.Advance.Height + 24); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + + // TextBlock owns shaping and text options; DrawText supplies canvas placement and wrapping. + canvas.Draw(Pens.Solid(Color.LightGray, 1), layoutBox); + canvas.DrawText(block, new PointF(60, 60), 520, Brushes.Solid(Color.DarkSlateBlue), pen: null); +})); +``` + +For manual line flow, choose the `TextBlock` API based on the coordinate space you want to draw from: + +- Use `TextBlock.GetLineLayouts(...)` when the text still behaves as one stacked block. Each returned `LineLayout` is positioned in block coordinates, including the cumulative advance of the lines before it, so it is ready to draw relative to the block origin. +- Use `TextBlock.EnumerateLineLayouts()` when each line is placed independently. Each `LineLayout` is line-local, as if it were the first line in the block, and the caller supplies the final canvas position or path when calling `DrawingCanvas.DrawText(...)`. + +The line-local enumerator is the right fit for text that flows through different columns, separate frames, or different paths. See [Prepared Text with TextBlock](../fonts/textblock.md) for the Fonts-side coordinate model. + +## Wrap and Align Text + +`RichTextOptions` inherits the core Fonts text options and adds ImageSharp.Drawing-specific rich text behavior. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(640, 260, Color.White.ToPixel()); + +Font font = SystemFonts.CreateFont("Arial", 34); +RichTextOptions options = new(font) +{ + Origin = new(48, 42), + WrappingLength = 544, + HorizontalAlignment = HorizontalAlignment.Center, + LineSpacing = 1.15F +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.DrawText( + options, + "Wrapped text can be measured and rendered with the same options.", + Brushes.Solid(Color.MidnightBlue), + pen: null); +})); +``` + +## Center Text in a Region + +Use `WrappingLength`, `HorizontalAlignment`, and `VerticalAlignment` when text should align within a known layout region. For centered alignment, `Origin` is the center anchor for the laid-out text, and `WrappingLength` sets the width used for line breaking. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(520, 220, Color.White.ToPixel()); + +Font font = SystemFonts.CreateFont("Arial", 36); +Rectangle layoutBounds = new(40, 56, 440, 108); +PointF layoutCenter = new( + layoutBounds.Left + (layoutBounds.Width / 2F), + layoutBounds.Top + (layoutBounds.Height / 2F)); + +RichTextOptions options = new(font) +{ + Origin = layoutCenter, + WrappingLength = layoutBounds.Width, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Draw(Pens.Solid(Color.LightGray, 1), layoutBounds); + + // The origin is the center anchor because both horizontal and vertical alignment are centered. + canvas.DrawLine(Pens.Dash(Color.Gray, 1), new PointF(layoutCenter.X, layoutBounds.Top), new PointF(layoutCenter.X, layoutBounds.Bottom)); + canvas.DrawLine(Pens.Dash(Color.Gray, 1), new PointF(layoutBounds.Left, layoutCenter.Y), new PointF(layoutBounds.Right, layoutCenter.Y)); + canvas.DrawText(options, "Centered by layout options", Brushes.Solid(Color.Black), pen: null); +})); +``` + +## Draw Text Along a Path + +Text can also follow an `IPath`. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(640, 260, Color.White.ToPixel()); + +Font font = SystemFonts.CreateFont("Arial", 30); +RichTextOptions options = new(font) +{ + Origin = new(0, 0) +}; + +PathBuilder builder = new(); +builder.AddCubicBezier( + new(52, 168), + new(186, 42), + new(420, 44), + new(588, 172)); + +IPath path = builder.Build(); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Draw(Pens.Dot(Color.LightGray, 2), path); + canvas.DrawText(options, "Text can follow path geometry", path, Brushes.Solid(Color.DarkSlateBlue), pen: null); +})); +``` + +## Use Text as Geometry + +Use `TextBuilder.GeneratePaths(...)` when the glyph outlines themselves should become drawing geometry. The returned paths can be filled, stroked, used as clips, or combined with image drawing. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image image = new(640, 240, Color.White.ToPixel()); + +RectangleF imageArea = new(0, 0, image.Width, image.Height); +Font font = SystemFonts.CreateFont("Arial", 104, FontStyle.Bold); +TextOptions glyphOptions = new(font) +{ + Origin = new(42, 150) +}; + +IPathCollection letters = TextBuilder.GeneratePaths("MASK", glyphOptions); +IPath[] glyphClips = [.. letters]; +DrawingOptions clipOptions = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.DarkSlateBlue)); + canvas.Save(clipOptions, glyphClips); + + // The generated glyph paths clip the photo to the visible letter shapes. + canvas.DrawImage(source, source.Bounds, imageArea, KnownResamplers.Bicubic); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.White, 2), letters); +})); +``` diff --git a/articles/imagesharp.drawing/transformsandcomposition.md b/articles/imagesharp.drawing/transformsandcomposition.md new file mode 100644 index 000000000..2e22b8b4c --- /dev/null +++ b/articles/imagesharp.drawing/transformsandcomposition.md @@ -0,0 +1,116 @@ +# Transforms and Composition + +`DrawingOptions` carries the transform, graphics options, and shape options used by canvas commands. Use it when drawing state should change for a group of operations. + +## Transform Drawing + +`DrawingOptions.Transform` is applied to vector output before rasterization. For strokes, the path is stroked in local geometry space and the generated outline is transformed for drawing. + +## Why Matrix4x4? + +ImageSharp.Drawing is a 2D drawing library, but it uses `Matrix4x4` for transforms so the same drawing state can represent both ordinary 2D affine transforms and projective transforms. + +For normal drawing, construct the value from `Matrix3x2`. That keeps rotation, scale, skew, and translation code familiar: + +```csharp +Matrix4x4 transform = new(Matrix3x2.CreateRotation(angle, center)); +``` + +Use the full `Matrix4x4` form when you need transforms that cannot be expressed by `Matrix3x2`, such as perspective-style projection. The canvas, path, text, brush, image, and WebGPU paths all carry the same transform type, so code can move between CPU drawing, retained scenes, and GPU rendering without changing the public drawing model. + +```csharp +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 260, Color.White.ToPixel()); + +DrawingOptions rotated = new() +{ + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.32F, new Vector2(210, 130))) +}; + +RectangularPolygon panel = new(92, 70, 236, 120); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + _ = canvas.Save(rotated); + + // Both the fill and stroke use the saved transform. + canvas.Fill(Brushes.Solid(Color.LightSkyBlue), panel); + canvas.Draw(Pens.Solid(Color.MidnightBlue, 5), panel); + canvas.Restore(); + + canvas.Draw(Pens.Dot(Color.Gray, 2), panel); +})); +``` + +Transforms also apply to clipped drawing. When you save transformed options with clip paths, the command geometry and clip geometry are prepared so the backend receives consistent clipped output. + +## Blend and Composite + +`GraphicsOptions` controls antialiasing, color blending, alpha composition, and blend percentage. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(360, 240, Color.White.ToPixel()); + +DrawingOptions multiply = new() +{ + GraphicsOptions = new() + { + ColorBlendingMode = PixelColorBlendingMode.Multiply, + AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver, + BlendPercentage = 0.85F + } +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(24, 88, 312, 60)); + + _ = canvas.Save(multiply); + + // The saved GraphicsOptions affect commands recorded until Restore. + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(100, 32, 110, 176)); + canvas.Fill(Brushes.Solid(Color.Red.WithAlpha(0.5F)), new EllipsePolygon(194, 120, 124, 92)); + canvas.Restore(); +})); +``` + +Use `SaveLayer(...)` when the blend should apply to a group as a single composited result. Use plain `Save(...)` when each command should blend independently. + +## Antialiasing + +Turn antialiasing off when exact integer coverage matters, such as low-resolution masks or pixel-art-style output. Leave it on for normal vector graphics. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(80, 80, Color.Black.ToPixel()); +DrawingOptions aliased = new() +{ + GraphicsOptions = new() + { + Antialias = false + } +}; + +image.Mutate(ctx => ctx.Paint(aliased, canvas => +{ + + // With antialiasing disabled, integer rectangle corners render as full covered pixels. + canvas.Fill(Brushes.Solid(Color.White), new Rectangle(10, 10, 44, 28)); +})); +``` diff --git a/articles/imagesharp.drawing/troubleshooting.md b/articles/imagesharp.drawing/troubleshooting.md new file mode 100644 index 000000000..fc615d451 --- /dev/null +++ b/articles/imagesharp.drawing/troubleshooting.md @@ -0,0 +1,144 @@ +# Troubleshooting + +This page collects common issues you can hit when moving from simple drawing samples to full ImageSharp.Drawing pipelines. Most problems come from three areas: deferred canvas replay, clipping and fill-rule choices, or text layout state. + +If the issue is WebGPU-specific, start with the WebGPU section below and then check the dedicated [WebGPU](webgpu.md) page. + +## Nothing Appears on the Image + +If you are drawing through `image.Mutate(ctx => ctx.Paint(...))`, the processing pipeline owns the canvas lifetime and replays the recorded drawing commands for you. + +If you create a canvas manually, make sure the canvas is disposed or flushed before you inspect the destination image. Canvas drawing is recorded and replayed in order, so pending commands are not visible until the canvas replays them. + +```csharp +using Image image = new(400, 240, Color.White.ToPixel()); + +using (DrawingCanvas canvas = image.CreateCanvas()) +{ + canvas.Fill(Color.CornflowerBlue, new Rectangle(40, 40, 180, 100)); + + // Disposing the canvas replays the recorded drawing commands onto the image. +} + +image.Save("output.png"); +``` + +When you use images as drawing sources, keep those source images alive until the canvas has replayed. `DrawImage` and `ImageBrush` record the drawing operation; they do not make the source image safe to dispose before replay. + +## Clipping Removes the Wrong Area + +`ShapeOptions.BooleanOperation` controls how the clip shape combines with the current drawing region. The default value is `Difference`, which subtracts the supplied shape from the current region. For the usual "draw only inside this shape" behavior, set it to `BooleanOperation.Intersection`. + +```csharp +DrawingOptions options = new() +{ + ShapeOptions = new() + { + // Intersect keeps the part of subsequent drawing inside the clip shape. + BooleanOperation = BooleanOperation.Intersection + } +}; + +PointF clipCenter = new(200, 120); +SizeF clipSize = new(260, 160); +EllipsePolygon clip = new(clipCenter, clipSize); + +canvas.Save(options, clip); +canvas.Fill(Color.HotPink, new Rectangle(0, 0, 400, 240)); +canvas.Restore(); +``` + +Use `Save(...)` for scoped clipping and state changes. Call `Restore()` when the scoped operation is complete so later drawing returns to the previous state. + +## Holes or Overlaps Fill Unexpectedly + +The fill rule controls how overlapping contours inside a complex polygon are interpreted. ImageSharp.Drawing defaults to `IntersectionRule.NonZero`, which matches the default used by SVG and web canvas APIs. With `NonZero`, contour winding order is meaningful, so holes are normally expressed by reversing the winding of the inner contour. + +Use `IntersectionRule.EvenOdd` when you want parity-based filling where each crossing toggles between inside and outside. This can be convenient for imported geometry that does not carry reliable winding direction. + +```csharp +DrawingOptions options = new() +{ + ShapeOptions = new() + { + // EvenOdd treats alternating contours as filled and unfilled regions. + IntersectionRule = IntersectionRule.EvenOdd + } +}; + +canvas.Fill(options, Color.MediumSeaGreen, complexPolygon); +``` + +## Text Is Not Centered Where Expected + +For region-based text layout, use the text alignment options instead of manually subtracting measured text sizes. The `Origin` is the layout anchor, `WrappingLength` defines the line width, and `HorizontalAlignment` / `VerticalAlignment` place the text block relative to that anchor. + +`TextAlignment` controls how wrapped lines are aligned inside the paragraph. `HorizontalAlignment` controls how the resulting paragraph bounds are positioned relative to `Origin`. + +## Styled Text Affects the Wrong Characters + +Rich text runs use grapheme indices, not UTF-16 code unit indices. `Start` is inclusive and `End` is exclusive, so the affected range is `[Start, End)`. + +This matters for emoji, combining marks, flags, and other user-perceived characters that can contain multiple Unicode scalar values. See the Fonts [Unicode](../fonts/unicode.md) page for the same indexing model. + +## Processors Run Before Earlier Drawing + +Canvas operations are ordered, but image processors operate at replay barriers. If you need a processor such as blur, opacity, or a mask operation to include drawing that has already been recorded, flush the canvas before applying the processor. + +```csharp +canvas.Fill(Color.Black, shadowShape); + +// Flush seals the shadow geometry before the blur processor is applied. +canvas.Flush(); +canvas.Apply(x => x.GaussianBlur(8)); +``` + +This is most useful when you mix vector drawing with ImageSharp processors in the same canvas sequence. + +## Images, Brushes, or Masks Stop Working After Disposal + +Drawing commands can be replayed later than the point where the command is recorded. Keep any source `Image` used by `DrawImage`, masks, or `ImageBrush` alive until the canvas has been disposed or flushed. + +The canvas does not own images passed into it. Dispose those images after the drawing scope that uses them has completed. + +## WebGPU Produces a Blank Frame + +Probe WebGPU support before creating GPU-backed drawing resources. WebGPU depends on the runtime environment, adapter, device, texture format, and browser or native surface. + +For window or surface rendering, acquire a frame, draw into its canvas, and dispose the frame. Disposing the frame completes the drawing scope and presents it to the surface. + +```csharp +if (!surface.TryAcquireFrame(out WebGPUSurfaceFrame frame)) +{ + return; +} + +using (frame) +{ + DrawingCanvas canvas = frame.CreateCanvas(); + + // Drawing commands are presented when the frame is disposed. + canvas.Clear(Color.White); + canvas.Fill(Color.SteelBlue, new Rectangle(40, 40, 180, 120)); +} +``` + +Resize the `WebGPUExternalSurface` when the framebuffer size changes. If you need to read pixels back to the CPU, use a pixel type that matches the target texture format, for example `Rgba32` with an `Rgba8Unorm` target. + +## A Good Debugging Order + +1. Confirm the canvas scope is disposed or flushed before checking the output. +2. Check source image lifetimes when using image brushes, masks, or `DrawImage`. +3. Check `ShapeOptions.BooleanOperation` when clipping. +4. Check `ShapeOptions.IntersectionRule` and contour winding for complex polygons. +5. Check text layout options before doing manual measurement math. +6. Check grapheme-based `[Start, End)` indices for rich text runs. +7. Probe WebGPU availability and surface frame acquisition before drawing GPU content. + +## Related Topics + +- [Canvas Drawing](canvas.md) +- [Clipping, Regions, and Layers](clippingregionslayers.md) +- [Paths and Shapes](pathsandshapes.md) +- [Drawing Text](text.md) +- [WebGPU](webgpu.md) diff --git a/articles/imagesharp.drawing/watermark.md b/articles/imagesharp.drawing/watermark.md new file mode 100644 index 000000000..931b5befe --- /dev/null +++ b/articles/imagesharp.drawing/watermark.md @@ -0,0 +1,41 @@ +# Add a Text Watermark + +Use `DrawText(...)` with alignment options when a watermark should stay anchored to an image edge. The text layout options keep the placement declarative, so you do not need to measure the string manually. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("photo.jpg"); + +Font font = SystemFonts.CreateFont("Arial", 36, FontStyle.Bold); +RichTextOptions options = new(font) +{ + Origin = new(image.Width - 64, image.Height - 64), + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Bottom +}; + +image.Mutate(ctx => ctx.Paint(canvas => +{ + // Alignment anchors the watermark to the bottom-right corner without measuring the text first. + canvas.DrawText( + options, + "© Six Labors", + Brushes.Solid(Color.White.WithAlpha(0.72F)), + Pens.Solid(Color.Black.WithAlpha(0.45F), 2)); +})); + +image.Save("watermarked.jpg"); +``` + +Use a subtle fill alpha and a darker outline when the watermark must remain readable over mixed image content. + +## Related Topics + +- [Drawing Text](text.md) +- [Images, Masks, and Processing](imagesandprocessing.md) diff --git a/articles/imagesharp.drawing/webgpu.md b/articles/imagesharp.drawing/webgpu.md new file mode 100644 index 000000000..a7f0ca720 --- /dev/null +++ b/articles/imagesharp.drawing/webgpu.md @@ -0,0 +1,393 @@ +# WebGPU + +ImageSharp.Drawing.WebGPU provides a GPU-backed drawing target for the same `DrawingCanvas` API used by the CPU image pipeline. + +Use the WebGPU package when you want ImageSharp.Drawing to render into a native WebGPU surface or an offscreen GPU texture. Use the regular ImageSharp.Drawing package when you want to draw directly into an `Image` on the CPU. + +## What WebGPU Is + +WebGPU is a modern, explicit GPU API. It gives an application access to a graphics adapter, a device, command queues, textures, buffers, shaders, and presentation surfaces. It is conceptually similar to modern native graphics APIs such as Vulkan, Metal, and Direct3D 12, but it exposes a portable WebGPU programming model. + +In ImageSharp.Drawing, WebGPU is not a browser feature. It is a native rendering backend used by .NET applications through the `SixLabors.ImageSharp.Drawing.WebGPU` package. The package creates or attaches to native WebGPU surfaces, records `DrawingCanvas` commands, lowers those commands into GPU work, and renders them into a WebGPU texture. + +The most important difference from normal ImageSharp drawing is the destination: + +- normal ImageSharp.Drawing draws into CPU image memory +- ImageSharp.Drawing.WebGPU draws into GPU textures and surfaces + +Use WebGPU when the destination is interactive, GPU-owned, or repeatedly redrawn. Use the CPU path when you need simple image generation, server-side processing, format encoding, or direct pixel access after every operation. + +## How ImageSharp.Drawing Uses WebGPU + +The WebGPU backend keeps the public drawing model the same. You still draw with `DrawingCanvas`, `Brush`, `Pen`, `IPath`, `RichTextOptions`, layers, clips, and retained scenes. + +The difference is what happens when the canvas flushes or is disposed: + +1. The canvas prepares the recorded drawing commands. +2. The WebGPU backend creates a retained GPU scene from those commands. +3. The backend creates render-scoped WebGPU resources. +4. GPU compute/render work rasterizes the scene into the target texture. +5. Window and external-surface frames are presented when the frame is disposed. + +That means WebGPU drawing is still deferred like the rest of the canvas API. The canvas callback is where you record work. Canvas or frame disposal is where the recorded work is submitted to the target. + +## Public WebGPU Types + +The public WebGPU API is target-first. + +- `WebGPUEnvironment` probes support and configures the library-managed WebGPU environment before first use. +- `WebGPUWindow` owns a native window, WebGPU surface, device resources, and render loop. +- `WebGPUExternalSurface` attaches to a native drawable owned by another toolkit or host application. +- `WebGPURenderTarget` owns an offscreen GPU texture and can read it back into an ImageSharp image. +- `WebGPUSurfaceFrame` represents one acquired presentable frame. Dispose it to render and present the frame. + +Most application code should start by choosing the target type. You do not normally create devices, queues, or command encoders yourself. + +## Installation + +Install the WebGPU package alongside ImageSharp.Drawing. + +The package restores the managed WebGPU interop and native WebGPU runtime dependencies it needs. Applications still need a machine and driver stack capable of creating a WebGPU adapter, device, queue, and compute pipeline. + +# [Package Manager](#tab/tabid-1) + +```bash +PM > Install-Package SixLabors.ImageSharp.Drawing.WebGPU -Version VERSION_NUMBER +``` + +# [.NET CLI](#tab/tabid-2) + +```bash +dotnet add package SixLabors.ImageSharp.Drawing.WebGPU --version VERSION_NUMBER +``` + +# [PackageReference](#tab/tabid-3) + +```xml + +``` + +# [Paket CLI](#tab/tabid-4) + +```bash +paket add SixLabors.ImageSharp.Drawing.WebGPU --version VERSION_NUMBER +``` + +*** + +## Check WebGPU Support + +Use `WebGPUEnvironment` when an application needs to check support before constructing a WebGPU window, external surface, or render target. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUEnvironment.Options = new() +{ + PowerPreference = WebGPUPowerPreference.HighPerformance +}; + +WebGPUEnvironmentError availability = WebGPUEnvironment.ProbeAvailability(); +if (availability != WebGPUEnvironmentError.Success) +{ + return; +} + +WebGPUEnvironmentError compute = WebGPUEnvironment.ProbeComputePipelineSupport(); +if (compute != WebGPUEnvironmentError.Success) +{ + return; +} +``` + +Assign `WebGPUEnvironment.Options` before any other WebGPU object is created. The library-managed WebGPU environment is initialized on first use. + +`ProbeAvailability()` checks whether the package can initialize the WebGPU API, create an instance, acquire an adapter, acquire a device, and get the default queue. `ProbeComputePipelineSupport()` checks whether the acquired device can create a trivial compute pipeline. The compute-pipeline probe is useful because the drawing backend depends on compute work for the staged raster pipeline. + +The result is a `WebGPUEnvironmentError`. `Success` is the only successful value. Other values tell you which step failed, such as API initialization, adapter acquisition, device acquisition, queue acquisition, or compute-pipeline creation. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +static bool TryUseWebGPU() +{ + WebGPUEnvironmentError availability = WebGPUEnvironment.ProbeAvailability(); + if (availability != WebGPUEnvironmentError.Success) + { + Console.WriteLine($"WebGPU unavailable: {availability}"); + return false; + } + + WebGPUEnvironmentError compute = WebGPUEnvironment.ProbeComputePipelineSupport(); + if (compute != WebGPUEnvironmentError.Success) + { + Console.WriteLine($"WebGPU compute unavailable: {compute}"); + return false; + } + + return true; +} +``` + +Configure `WebGPUEnvironment.UncapturedError` if you want to log native WebGPU validation or device errors. The callback may be invoked from a native WebGPU callback thread, so keep it short and non-blocking. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUEnvironment.UncapturedError = (errorType, message) => +{ + Console.Error.WriteLine($"{errorType}: {message}"); +}; +``` + +## Texture Formats + +WebGPU targets have a concrete texture format. The supported formats are: + +- `Rgba8Unorm`, mapped to `Rgba32` +- `Bgra8Unorm`, mapped to `Bgra32` +- `Rgba8Snorm`, mapped to `NormalizedByte4` +- `Rgba16Float`, mapped to `HalfVector4` + +Use the default `Rgba8Unorm` unless you have a reason to match another host surface format or readback pixel type. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUWindowOptions options = new() +{ + Format = WebGPUTextureFormat.Rgba8Unorm +}; +``` + +For readback, the ImageSharp pixel type must match the render target format. For example, `Rgba8Unorm` reads back naturally as `Image`, and `Bgra8Unorm` reads back naturally as `Image`. + +## Present Modes + +Window and external-surface targets present completed frames to a display. `WebGPUPresentMode` controls how frames wait for that display. + +- `Fifo` is the safest default. It is v-synced and avoids tearing. +- `Immediate` presents as soon as possible and can tear. +- `Mailbox` keeps newer frames over older queued frames when supported by the backend and platform. + +Use `Fifo` for most applications. Use `Immediate` or `Mailbox` only when latency matters more than presentation stability and you have tested the target platform. + +## Draw to a Window + +`WebGPUWindow` owns the platform window, WebGPU device resources, and frame acquisition. The render callback receives a `WebGPUSurfaceFrame`, and the frame exposes the `DrawingCanvas` for that render. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUWindowOptions options = new() +{ + Title = "ImageSharp.Drawing WebGPU", + Size = new(960, 540), + PresentMode = WebGPUPresentMode.Fifo +}; + +using WebGPUWindow window = new(options); + +window.Run((WebGPUSurfaceFrame frame) => +{ + DrawingCanvas canvas = frame.Canvas; + RectangularPolygon panel = new(64, 72, 320, 180); + EllipsePolygon marker = new(new PointF(224, 162), new SizeF(120, 82)); + + // Run supplies a frame canvas and presents it after the callback completes. + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), panel); + canvas.Fill(Brushes.Solid(Color.Gold), marker); + canvas.Draw(Pens.Solid(Color.Black, 3), panel); +}); +``` + +`Run(Action)` is the simplest model. The window acquires a frame, gives you the frame and its canvas, disposes the frame after the callback, and presents the result. + +Use the `WebGPUSurfaceFrame` overload when you need frame lifetime control or the elapsed render time. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPUWindow window = new(); + +window.Run((frame, elapsed) => +{ + DrawingCanvas canvas = frame.Canvas; + float radius = 40 + (MathF.Sin((float)elapsed.TotalSeconds) * 12); + EllipsePolygon pulse = new(new PointF(120, 120), radius); + + // Disposing the frame after this callback presents the rendered canvas. + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), pulse); +}); +``` + +`WebGPUWindow` also exposes window events and properties such as title, size, framebuffer size, render scale, position, visibility, focus, state, border, frame rate limits, and present mode. `FramebufferSize` is the size that matters for the WebGPU surface. `ClientSize` is the window coordinate size. + +## Manual Frame Acquisition + +Use `TryAcquireFrame(...)` when you own the loop and want to decide when events, updates, and rendering happen. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPUWindow window = new(); + +while (!window.IsClosing) +{ + window.DoEvents(); + + if (!window.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) + { + continue; + } + + using (frame) + { + DrawingCanvas canvas = frame.Canvas; + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new RectangularPolygon(40, 40, 180, 120)); + } +} +``` + +`TryAcquireFrame(...)` can return `false` when the surface cannot provide a drawable frame right now. That can happen for transient surface states such as timeout, outdated surface, lost surface, zero-sized framebuffer, or device recovery. Treat `false` as "skip this render attempt and try again later." + +## Draw to an Existing Surface + +Use `WebGPUExternalSurface` when another toolkit owns the window or native drawable. Create a `WebGPUSurfaceHost` for the platform handle, notify the surface when the drawable framebuffer changes size, and acquire one `WebGPUSurfaceFrame` for each render. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +void RunWin32Surface(nint hwnd, nint hinstance) +{ + WebGPUSurfaceHost host = WebGPUSurfaceHost.Win32(hwnd, hinstance); + using WebGPUExternalSurface surface = new(host, new(1280, 720)); + + void Resize(Size framebufferSize) => surface.Resize(framebufferSize); + + void Render() + { + if (!surface.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) + { + return; + } + + using (frame) + { + RectangularPolygon content = new(48, 48, 320, 160); + + // The external UI loop owns when Render is called; the frame owns presentation. + frame.Canvas.Clear(Brushes.Solid(Color.White)); + frame.Canvas.Fill(Brushes.Solid(Color.Orange), content); + } + } +} +``` + +`WebGPUSurfaceHost` includes factory methods for GLFW, SDL, Win32, X11, Cocoa, UIKit, Wayland, WinRT, Android, Vivante, and EGL hosts. + +The host application remains responsible for: + +- creating and owning the native window or drawable +- providing the correct native handles to `WebGPUSurfaceHost` +- calling `Resize(...)` when the drawable framebuffer size changes +- calling `TryAcquireFrame(...)` from its render loop +- disposing each acquired `WebGPUSurfaceFrame` +- keeping native handles valid for the lifetime of the external surface + +Use `WebGPUExternalSurface` when ImageSharp.Drawing should render into an existing UI framework or native application instead of creating its own window. + +## Draw Offscreen + +`WebGPURenderTarget` renders into an offscreen GPU texture. Create a canvas, draw into it, dispose the canvas to flush the drawing work, then read the result back when CPU image access is needed. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; + +using WebGPURenderTarget target = new(640, 360); + +using (DrawingCanvas canvas = target.CreateCanvas()) +{ + RectangularPolygon background = new(0, 0, target.Width, target.Height); + EllipsePolygon highlight = new(new PointF(320, 180), new SizeF(260, 140)); + + // Disposing the canvas flushes the recorded drawing commands to the GPU target. + canvas.Fill(Brushes.Solid(Color.White), background); + canvas.Fill(Brushes.Solid(Color.LightSkyBlue), highlight); + canvas.Draw(Pens.Solid(Color.DarkSlateBlue, 4), highlight); +} + +using Image image = target.ReadbackImage(); +image.Save("webgpu-output.png"); +``` + +Offscreen render targets are useful for GPU-generated images, render-to-texture workflows, tests, benchmarks, and any workflow that wants GPU drawing without a visible window. + +Readback copies GPU texture data into CPU memory. It is useful when you need an `Image`, but it is also a synchronization point. Avoid reading back every frame in an interactive render loop unless you actually need CPU pixels. + +## Choosing a Target + +Use `WebGPUWindow` when ImageSharp.Drawing should own the application window and render loop. + +Use `WebGPUExternalSurface` when an existing application, UI framework, or native toolkit owns the window and event loop. + +Use `WebGPURenderTarget` when you want GPU rendering without a visible window, or when the output needs to be read back into an ImageSharp image. + +## When Not to Use WebGPU + +WebGPU is not automatically the best target for every drawing workload. Prefer normal ImageSharp.Drawing when: + +- you are generating static images on the server +- you need direct CPU pixel access after most operations +- you are encoding the result immediately to PNG, JPEG, WebP, or another image format +- your deployment environment has no reliable GPU, native WebGPU runtime, or compute-pipeline support +- the drawing workload is small enough that GPU setup and readback costs dominate + +Prefer WebGPU when: + +- the target is already a GPU surface +- the scene is interactive or redrawn repeatedly +- you can keep the result on the GPU +- you want a native window or external host surface +- the drawing workload benefits from GPU-side batching and rasterization + +## Frame Lifetime Rules + +The important lifetime rules are: + +- Dispose a `DrawingCanvas` created from `WebGPURenderTarget.CreateCanvas()` to submit its recorded work. +- Dispose a `WebGPUSurfaceFrame` to submit and present the frame. +- Keep retained scenes alive until every canvas or frame that recorded them has been disposed. +- Keep source images used by image brushes alive until the WebGPU canvas has replayed. +- Call `Resize(...)` on external surfaces before acquiring the next frame after a framebuffer resize. + +The window `Run(...)` helpers handle frame disposal for you. Manual loops and external surfaces require you to dispose the frame yourself. + +## Troubleshooting + +If WebGPU cannot start, call `ProbeAvailability()` and log the returned `WebGPUEnvironmentError`. + +If support probing succeeds but drawing fails, also call `ProbeComputePipelineSupport()` and configure `WebGPUEnvironment.UncapturedError` before creating WebGPU targets. + +If a window or external surface stops rendering after resize or display changes, make sure the framebuffer size is positive and, for external surfaces, call `Resize(...)` with the new framebuffer size before acquiring frames. + +If readback fails or produces an unexpected pixel type, check that the render target format and requested `Image` type match. diff --git a/articles/imagesharp.web/configuration.md b/articles/imagesharp.web/configuration.md index eadf96343..d09936e3a 100644 --- a/articles/imagesharp.web/configuration.md +++ b/articles/imagesharp.web/configuration.md @@ -123,13 +123,13 @@ Use this pattern when you want to keep ImageSharp.Web's ICC-conversion behavior The builder methods let you replace only the layer you actually need to change: -- `SetRequestParser()` replaces the request parser. -- `SetCache()` replaces the backend cache. -- `SetCacheKey()` and `SetCacheHash()` change cache naming. -- `AddProvider()`, `InsertProvider()`, `RemoveProvider()`, and `ClearProviders()` manage source providers. -- `AddProcessor()`, `RemoveProcessor()`, and `ClearProcessors()` manage the processing command set. -- `AddConverter()`, `RemoveConverter()`, and `ClearConverters()` manage typed command parsing. -- `Configure(...)` binds or mutates option objects for any registered provider, cache, or parser. +- [`SetRequestParser()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetRequestParser*) replaces the request parser. +- [`SetCache()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCache*) replaces the backend cache. +- [`SetCacheKey()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCacheKey*) and [`SetCacheHash()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCacheHash*) change cache naming. +- [`AddProvider()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.AddProvider*), [`InsertProvider()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.InsertProvider*), [`RemoveProvider()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.RemoveProvider*), and [`ClearProviders()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.ClearProviders*) manage source providers. +- [`AddProcessor()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.AddProcessor*), [`RemoveProcessor()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.RemoveProcessor*), and [`ClearProcessors()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.ClearProcessors*) manage the processing command set. +- [`AddConverter()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.AddConverter*), [`RemoveConverter()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.RemoveConverter*), and [`ClearConverters()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.ClearConverters*) manage typed command parsing. +- [`Configure(...)`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.Configure*) binds or mutates option objects for any registered provider, cache, or parser. For example, if you want to keep the default middleware but remove format conversion: diff --git a/articles/imagesharp.web/extensibility.md b/articles/imagesharp.web/extensibility.md index 9b9e3cac5..abde17691 100644 --- a/articles/imagesharp.web/extensibility.md +++ b/articles/imagesharp.web/extensibility.md @@ -50,7 +50,7 @@ public sealed class SepiaWebProcessor : IImageWebProcessor } ``` -Register it with the builder: +Register it with [`AddProcessor()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.AddProcessor*): ```csharp builder.Services.AddImageSharp() @@ -61,7 +61,7 @@ Processor order is driven by the order of the recognized command keys in the req ## Custom Command Converters -The built-in converters already cover integral types, floating-point values, booleans, strings, arrays, lists, colors, and enums. If your processor wants a custom command type, implement `ICommandConverter`, register it with `AddConverter()`, then parse it inside the processor with `CommandParser.ParseValue()`. +The built-in converters already cover integral types, floating-point values, booleans, strings, arrays, lists, colors, and enums. If your processor wants a custom command type, implement [`ICommandConverter`](xref:SixLabors.ImageSharp.Web.Commands.Converters.ICommandConverter`1), register it with [`AddConverter()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.AddConverter*), then parse it inside the processor with [`CommandParser.ParseValue()`](xref:SixLabors.ImageSharp.Web.Commands.CommandParser.ParseValue*). This is the right place to centralize parsing rules for custom value syntaxes instead of repeating string parsing inside each processor. @@ -71,9 +71,9 @@ Implement a custom provider when your source image is not on disk, in Azure Blob - open the source stream; - report source last-write and cache metadata; -- decide whether requests are `CommandOnly` or always handled. +- decide whether requests use [`ProcessingBehavior.CommandOnly`](xref:SixLabors.ImageSharp.Web.Providers.ProcessingBehavior.CommandOnly) or are always handled. -When the source maps naturally to an `IFileProvider`, [`FileProviderImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.FileProviderImageProvider) is the easiest base class. +When the source maps naturally to an [`IFileProvider`](xref:Microsoft.Extensions.FileProviders.IFileProvider), [`FileProviderImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.FileProviderImageProvider) is the easiest base class. Implement a custom cache when processed images should live somewhere other than the built-in physical filesystem cache or the cloud caches. A cache receives the hashed key, encoded stream, and [`ImageCacheMetadata`](xref:SixLabors.ImageSharp.Web.ImageCacheMetadata), then later returns an [`IImageCacheResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageCacheResolver) that can reopen the cached entry. @@ -92,7 +92,7 @@ Your parser returns an ordered [`CommandCollection`](xref:SixLabors.ImageSharp.W ## Extend Razor Integration -If you add custom processors and want equally natural Razor markup, derive from [`ImageTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper) and override `AddProcessingCommands(...)` to write your custom command keys into the outgoing URL. +If you add custom processors and want equally natural Razor markup, derive from [`ImageTagHelper`](xref:SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper) and override [`AddProcessingCommands(...)`](xref:SixLabors.ImageSharp.Web.TagHelpers.ImageTagHelper.AddProcessingCommands*) to write your custom command keys into the outgoing URL. That lets your Razor layer stay strongly typed instead of falling back to raw query-string fragments. diff --git a/articles/imagesharp.web/imagecaches.md b/articles/imagesharp.web/imagecaches.md index feeb02e04..76979780c 100644 --- a/articles/imagesharp.web/imagecaches.md +++ b/articles/imagesharp.web/imagecaches.md @@ -13,7 +13,7 @@ For each processed request, the middleware: ## Default Physical Cache -[`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) is the default backend registered by `AddImageSharp()`. +[`PhysicalFileSystemCache`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCache) is the default backend registered by [`AddImageSharp()`](xref:SixLabors.ImageSharp.Web.ServiceCollectionExtensions.AddImageSharp*). - It stores cached files under the web root by default. - [`PhysicalFileSystemCacheOptions.CacheFolder`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCacheOptions.CacheFolder) defaults to `is-cache`. @@ -32,7 +32,7 @@ builder.Services.AddImageSharp() }); ``` -If your app does not define a web root, set `CacheRootPath` explicitly. Relative paths are resolved against the application content root. +If your app does not define a web root, set [`CacheRootPath`](xref:SixLabors.ImageSharp.Web.Caching.PhysicalFileSystemCacheOptions.CacheRootPath) explicitly. Relative paths are resolved against the application content root. ## Browser Lifetime Versus Backend Lifetime @@ -51,7 +51,7 @@ By default, ImageSharp.Web uses: - [`SHA256CacheHash`](xref:SixLabors.ImageSharp.Web.Caching.SHA256CacheHash) to hash that key into the stored filename. - [`ImageSharpMiddlewareOptions.CacheHashLength`](xref:SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions.CacheHashLength) to control how many hash characters are kept. -If you need cache entries to vary by host or some other request detail, swap the key implementation: +If you need cache entries to vary by host or some other request detail, swap the key implementation with [`SetCacheKey()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCacheKey*) or [`SetCacheHash()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCacheHash*): ```csharp using SixLabors.ImageSharp.Web; @@ -92,7 +92,7 @@ Install the Azure provider package: dotnet add package SixLabors.ImageSharp.Web.Providers.Azure ``` -Then replace the default cache backend: +Then replace the default cache backend with [`SetCache()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCache*): ```csharp using Azure.Storage.Blobs.Models; @@ -121,7 +121,7 @@ Install the AWS provider package: dotnet add package SixLabors.ImageSharp.Web.Providers.AWS ``` -Then replace the default cache backend: +Then replace the default cache backend with [`SetCache()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.SetCache*): ```csharp using Amazon.S3; diff --git a/articles/imagesharp.web/imageproviders.md b/articles/imagesharp.web/imageproviders.md index f8ad15f93..13c66ec27 100644 --- a/articles/imagesharp.web/imageproviders.md +++ b/articles/imagesharp.web/imageproviders.md @@ -1,12 +1,12 @@ # Image Providers -Image providers answer one question: where does the source image come from? Every incoming request is offered to the registered providers in order, and the first provider whose `Match` function returns `true` owns the request. +Image providers answer one question: where does the source image come from? Every incoming request is offered to the registered providers in order, and the first provider whose [`Match`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider.Match) function returns `true` owns the request. -That means provider order matters. If two providers can both match the same path, put the more specific one first or narrow its `Match` predicate so the wrong provider does not claim the request. +That means provider order matters. If two providers can both match the same path, put the more specific one first or narrow its [`Match`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider.Match) predicate so the wrong provider does not claim the request. ## Default Physical Filesystem Provider -[`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) is the default source provider registered by `AddImageSharp()`. +[`PhysicalFileSystemProvider`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProvider) is the default source provider registered by [`AddImageSharp()`](xref:SixLabors.ImageSharp.Web.ServiceCollectionExtensions.AddImageSharp*). - It resolves images from the web root by default. - [`PhysicalFileSystemProviderOptions.ProviderRootPath`](xref:SixLabors.ImageSharp.Web.Providers.PhysicalFileSystemProviderOptions.ProviderRootPath) can be `null`, absolute, or relative to the application content root. @@ -30,11 +30,11 @@ If you want truly command-only processing for local files, changing `ProcessingB ## Provider Matching and Ordering -ImageSharp.Web stops at the first provider whose `Match` function returns `true`. It does not continue searching if that provider later decides the request is invalid, so keep these rules in mind: +ImageSharp.Web stops at the first provider whose [`Match`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider.Match) function returns `true`. It does not continue searching if that provider later decides the request is invalid, so keep these rules in mind: - Register more specific providers before more general ones. -- Keep `Match` predicates mutually exclusive whenever possible. -- Use `InsertProvider(...)` when provider precedence matters more than registration order. +- Keep [`Match`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider.Match) predicates mutually exclusive whenever possible. +- Use [`InsertProvider(...)`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.InsertProvider*) when provider precedence matters more than registration order. Cloud providers in particular usually want a path prefix such as a container or bucket name so they can distinguish their requests cheaply. @@ -114,8 +114,8 @@ If your public URL shape does not naturally include the bucket name, use URL rew Implement [`IImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider) when you need a new source backend. Your provider is responsible for three things: -- deciding whether it owns the request via `Match`; -- deciding whether the request is valid via `IsValidRequest(...)`; +- deciding whether it owns the request via [`Match`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider.Match); +- deciding whether the request is valid via [`IsValidRequest(...)`](xref:SixLabors.ImageSharp.Web.Providers.IImageProvider.IsValidRequest*); - returning an [`IImageResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageResolver) that can open the source stream and report source metadata. If your source already fits an `IFileProvider`-style model, [`FileProviderImageProvider`](xref:SixLabors.ImageSharp.Web.Providers.FileProviderImageProvider) is the easiest base class to start from. diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 93d94b200..aaef8f30f 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -1,6 +1,6 @@ # ImageSharp.Web -ImageSharp.Web is Six Labors' ASP.NET Core image middleware for on-the-fly processing and caching. It sits in front of one or more image providers, parses URL commands, runs the matching ImageSharp processors, and stores the result so repeated requests are inexpensive after the first hit. +ImageSharp.Web is Six Labors' high-performance ASP.NET Core image middleware for on-the-fly processing and caching. It sits in front of one or more image providers, parses URL commands, runs the matching ImageSharp processors, and stores the result so repeated requests are inexpensive after the first hit. The current package targets .NET 8 and is built on top of [ImageSharp](../imagesharp/index.md). The middleware is intentionally modular: you can change how commands are parsed, where source images come from, how cache keys are built, where processed images are stored, and whether image requests must be signed. diff --git a/articles/imagesharp/gettingstarted.md b/articles/imagesharp/gettingstarted.md index 9cbb4a5cb..126cffaa1 100644 --- a/articles/imagesharp/gettingstarted.md +++ b/articles/imagesharp/gettingstarted.md @@ -5,8 +5,8 @@ ImageSharp is easiest to learn if you think in terms of a simple flow: load or i The main types you will run into first are: - [`Image`](xref:SixLabors.ImageSharp.Image) is the format-agnostic image container used by the main loading, processing, and saving APIs. -- `Image` is the generic image container to use when you know the pixel format and want direct pixel access. See [Pixel Formats](pixelformats.md) for more detail. -- [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame) and `ImageFrame` represent individual frames in multi-frame images such as GIF and WebP. +- [`Image`](xref:SixLabors.ImageSharp.Image`1) is the generic image container to use when you know the pixel format and want direct pixel access. See [Pixel Formats](pixelformats.md) for more detail. +- [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame) and [`ImageFrame`](xref:SixLabors.ImageSharp.ImageFrame`1) represent individual frames in multi-frame images such as GIF and WebP. - [`ImageInfo`](xref:SixLabors.ImageSharp.ImageInfo) gives you dimensions, pixel information, and metadata without fully decoding the image. ## Load, Process, and Save an Image @@ -28,15 +28,15 @@ image.Save("output.jpg"); This example shows the core workflow: -- `Image.Load()` detects the input format from the image data. -- `Mutate()` applies processors to the current image in order. -- `Save()` picks an encoder from the output path unless you pass one explicitly. +- [`Image.Load()`](xref:SixLabors.ImageSharp.Image.Load*) detects the input format from the image data. +- [`Mutate()`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*) applies processors to the current image in order. +- [`Save()`](xref:SixLabors.ImageSharp.ImageExtensions.Save*) picks an encoder from the output path unless you pass one explicitly. For more detail, see [Loading, Identifying, and Saving](loadingandsaving.md), [Processing Images](processing.md), and [Image Formats](imageformats.md). ## Read Image Information Without Decoding Pixels -If you only need image dimensions, pixel information, or metadata, use `Image.Identify()` instead of `Image.Load()`: +If you only need image dimensions, pixel information, or metadata, use [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify*) instead of [`Image.Load()`](xref:SixLabors.ImageSharp.Image.Load*): ```csharp using SixLabors.ImageSharp; @@ -58,19 +58,19 @@ You can also create images directly in memory: using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using Image image = new(640, 480, Color.White); +using Image image = new(640, 480, Color.White.ToPixel()); ``` -Use `Image` when the pixel format matters to your workflow, for example when you need direct access to pixel rows or want to interoperate with a known buffer format. +Use [`Image`](xref:SixLabors.ImageSharp.Image`1) when the pixel format matters to your workflow, for example when you need direct access to pixel rows or want to interoperate with a known buffer format. ## Mutate or Clone? ImageSharp exposes two primary processing entry points: -- `Mutate()` changes the current image in place. -- `Clone()` creates a deep copy and applies the processors to that copy. +- [`Mutate()`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*) changes the current image in place. +- [`Clone()`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Clone*) creates a deep copy and applies the processors to that copy. -Use `Mutate()` when you want to transform the current image, and `Clone()` when you need to keep the original unchanged. +Use [`Mutate()`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate*) when you want to transform the current image, and [`Clone()`](xref:SixLabors.ImageSharp.Processing.ProcessingExtensions.Clone*) when you need to keep the original unchanged. ```csharp using SixLabors.ImageSharp; @@ -82,7 +82,7 @@ using Image thumbnail = image.Clone(x => x.Resize(160, 160)); ## Dispose Images Promptly -ImageSharp images own pooled memory buffers and should be disposed as soon as you are done with them. Prefer `using` declarations or `using` blocks around `Image` and `Image` instances. +ImageSharp images own pooled memory buffers and should be disposed as soon as you are done with them. Prefer `using` declarations or `using` blocks around [`Image`](xref:SixLabors.ImageSharp.Image) and [`Image`](xref:SixLabors.ImageSharp.Image`1) instances. See [Memory Management](memorymanagement.md) for production guidance around pooling, contiguous buffers, and diagnostics. diff --git a/articles/imagesharp/gif.md b/articles/imagesharp/gif.md index 5c388559b..6c22e59b5 100644 --- a/articles/imagesharp/gif.md +++ b/articles/imagesharp/gif.md @@ -25,12 +25,12 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.PixelFormats; -using Image gif = new(120, 120, Color.Black); +using Image gif = new(120, 120, Color.Black.ToPixel()); gif.Metadata.GetGifMetadata().RepeatCount = 0; gif.Frames.RootFrame.Metadata.GetGifMetadata().FrameDelay = 10; -using Image frame = new(120, 120, Color.Orange); +using Image frame = new(120, 120, Color.Orange.ToPixel()); frame.Frames.RootFrame.Metadata.GetGifMetadata().FrameDelay = 10; gif.Frames.AddFrame(frame.Frames.RootFrame); diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index b60882825..8638d9020 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -50,7 +50,7 @@ No single format is best everywhere. The right choice depends on whether your pr [`Image`](xref:SixLabors.ImageSharp.Image`1) represents decoded pixel data. Once an image is loaded into memory, it is no longer tied to a specific file format unless you explicitly inspect or preserve that information. -ImageSharp can detect the encoded format of a source before loading it: +ImageSharp can detect the encoded format of a source before loading it with [`Image.DetectFormat()`](xref:SixLabors.ImageSharp.Image.DetectFormat*): ```csharp using SixLabors.ImageSharp; @@ -80,7 +80,7 @@ if (image.Metadata.DecodedImageFormat is not null) } ``` -When you save by path, `image.Save("output.jpg")` or `image.Save("output.png")` selects the encoder from the destination file extension. +When you save by path, [`image.Save("output.jpg")`](xref:SixLabors.ImageSharp.ImageExtensions.Save*) or `image.Save("output.png")` selects the encoder from the destination file extension. You can also choose a format explicitly by passing an encoder or by using the `SaveAs...()` helpers. @@ -101,22 +101,22 @@ image.Save("output.png", new PngEncoder()); ImageSharp also provides format-specific helpers: -- `image.SaveAsBmp()` (shortcut for `image.Save(new BmpEncoder())`) -- `image.SaveAsCur()` (shortcut for `image.Save(new CurEncoder())`) -- `image.SaveAsExr()` (shortcut for `image.Save(new ExrEncoder())`) -- `image.SaveAsGif()` (shortcut for `image.Save(new GifEncoder())`) -- `image.SaveAsIco()` (shortcut for `image.Save(new IcoEncoder())`) -- `image.SaveAsJpeg()` (shortcut for `image.Save(new JpegEncoder())`) -- `image.SaveAsPbm()` (shortcut for `image.Save(new PbmEncoder())`) -- `image.SaveAsPng()` (shortcut for `image.Save(new PngEncoder())`) -- `image.SaveAsQoi()` (shortcut for `image.Save(new QoiEncoder())`) -- `image.SaveAsTga()` (shortcut for `image.Save(new TgaEncoder())`) -- `image.SaveAsTiff()` (shortcut for `image.Save(new TiffEncoder())`) -- `image.SaveAsWebp()` (shortcut for `image.Save(new WebpEncoder())`) +- `image.SaveAsBmp()` uses [`BmpEncoder`](xref:SixLabors.ImageSharp.Formats.Bmp.BmpEncoder). +- `image.SaveAsCur()` uses [`CurEncoder`](xref:SixLabors.ImageSharp.Formats.Cur.CurEncoder). +- `image.SaveAsExr()` uses [`ExrEncoder`](xref:SixLabors.ImageSharp.Formats.Exr.ExrEncoder). +- `image.SaveAsGif()` uses [`GifEncoder`](xref:SixLabors.ImageSharp.Formats.Gif.GifEncoder). +- `image.SaveAsIco()` uses [`IcoEncoder`](xref:SixLabors.ImageSharp.Formats.Ico.IcoEncoder). +- `image.SaveAsJpeg()` uses [`JpegEncoder`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder). +- `image.SaveAsPbm()` uses [`PbmEncoder`](xref:SixLabors.ImageSharp.Formats.Pbm.PbmEncoder). +- `image.SaveAsPng()` uses [`PngEncoder`](xref:SixLabors.ImageSharp.Formats.Png.PngEncoder). +- `image.SaveAsQoi()` uses [`QoiEncoder`](xref:SixLabors.ImageSharp.Formats.Qoi.QoiEncoder). +- `image.SaveAsTga()` uses [`TgaEncoder`](xref:SixLabors.ImageSharp.Formats.Tga.TgaEncoder). +- `image.SaveAsTiff()` uses [`TiffEncoder`](xref:SixLabors.ImageSharp.Formats.Tiff.TiffEncoder). +- `image.SaveAsWebp()` uses [`WebpEncoder`](xref:SixLabors.ImageSharp.Formats.Webp.WebpEncoder). ## General Decoder Options -Use [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) with the general `Load()` APIs when you want to control metadata handling, frame limits, or decode-to-size behavior: +Use [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) with the general [`Load()`](xref:SixLabors.ImageSharp.Image.Load*) APIs when you want to control metadata handling, frame limits, or decode-to-size behavior: ```csharp using SixLabors.ImageSharp; @@ -138,10 +138,10 @@ Format-specific decoder option types also exist for specialized scenarios such a Several formats share useful option sets through common encoder base types: -- [`ImageEncoder`](xref:SixLabors.ImageSharp.Formats.ImageEncoder) exposes `SkipMetadata`. -- [`AlphaAwareImageEncoder`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder) adds `TransparentColorMode`. -- [`QuantizingImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder) adds `Quantizer` and `PixelSamplingStrategy`. -- [`AnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder) adds `RepeatCount`, `BackgroundColor`, and `AnimateRootFrame`. +- [`ImageEncoder`](xref:SixLabors.ImageSharp.Formats.ImageEncoder) exposes [`SkipMetadata`](xref:SixLabors.ImageSharp.Formats.ImageEncoder.SkipMetadata). +- [`AlphaAwareImageEncoder`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder) adds [`TransparentColorMode`](xref:SixLabors.ImageSharp.Formats.AlphaAwareImageEncoder.TransparentColorMode). +- [`QuantizingImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder) adds [`Quantizer`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.Quantizer) and [`PixelSamplingStrategy`](xref:SixLabors.ImageSharp.Formats.QuantizingImageEncoder.PixelSamplingStrategy). +- [`AnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder) adds [`RepeatCount`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder.RepeatCount), [`BackgroundColor`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder.BackgroundColor), and [`AnimateRootFrame`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder.AnimateRootFrame). Those inherited options are especially useful when working with GIF, APNG, and animated WebP. For a format-agnostic guide to palettes and dithered output, see [Quantization, Palettes, and Dithering](quantization.md). diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index 1a5a45e35..fd8b127b1 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -1,6 +1,6 @@ # ImageSharp -ImageSharp is the part of the Six Labors stack you reach for when you need to load, inspect, process, and save images entirely in managed .NET code. It gives you one consistent image model whether you are building a thumbnail service, a photo workflow, a web upload pipeline, or a lower-level imaging tool. +ImageSharp is the high-performance part of the Six Labors stack you reach for when you need to load, inspect, process, and save images entirely in managed .NET code. It gives you one consistent image model whether you are building a thumbnail service, a photo workflow, a web upload pipeline, or a lower-level imaging tool. This section is written as a guided set of articles rather than a flat feature list. Start with [Getting Started](gettingstarted.md) if you are new to the library, then branch into loading, processing, formats, or lower-level pixel work as your needs get more specific. diff --git a/articles/imagesharp/memorymanagement.md b/articles/imagesharp/memorymanagement.md index 40db76b83..cf29325d4 100644 --- a/articles/imagesharp/memorymanagement.md +++ b/articles/imagesharp/memorymanagement.md @@ -22,14 +22,11 @@ Configuration config = Configuration.Default.Clone(); config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { MaximumPoolSizeMegabytes = 128, - AllocationLimitMegabytes = 1024, - AccumulativeAllocationLimitMegabytes = 2048 + AllocationLimitMegabytes = 1024 }); ``` -[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow. [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) caps the allocator's total live allocations across all active owners and memory groups. - -Use [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) when you want multiple concurrent images, frames, or intermediate buffers to share one fixed allocator budget. Disposing the image or memory owner releases that reservation so later allocations can succeed. When both limits are set, keep the accumulative limit at or above the single-allocation limit. +[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow for one live allocation group. ## Prefer Contiguous Buffers Only When You Need Them diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index c4c48d1a7..41786db47 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -39,7 +39,7 @@ DecoderOptions options = new() MaxFrames = 1, SkipMetadata = true, TargetSize = new Size(1600, 1600), - SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreNone + SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; using Image image = Image.Load(options, stream); @@ -51,9 +51,9 @@ For public upload endpoints, `MaxFrames = 1` is often appropriate when you only [`SegmentIntegrityHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SegmentIntegrityHandling) is a tradeoff between strictness and recovery: -- [`IgnoreNone`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreNone) rejects files on any segment validation error. -- [`IgnoreNonCritical`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreNonCritical) is the library default. -- [`IgnoreData`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreData) and [`IgnoreAll`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreAll) are better suited to recovery tools than to public-facing ingest paths. +- [`Strict`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.Strict) rejects files on any recoverable segment validation error. +- [`IgnoreAncillary`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreAncillary) is the library default and ignores recoverable errors in optional metadata or other ancillary segments. +- [`IgnoreImageData`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreImageData) also ignores recoverable image data segment errors and is better suited to recovery tools than to public-facing ingest paths. That recommendation is an inference from the enum semantics: the more errors you ignore, the more "best effort" your decode path becomes. @@ -89,13 +89,13 @@ using SixLabors.ImageSharp.Memory; Configuration config = Configuration.Default.Clone(); config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { + // Roughly limits the workload to about 64 megapixels of Rgba32 data. - AllocationLimitMegabytes = 256, - AccumulativeAllocationLimitMegabytes = 512 + AllocationLimitMegabytes = 256 }); ``` -[`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) limits the size of any one live allocation group. [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) limits the total live memory reserved through that allocator, which is useful when several requests or frames may overlap. +[`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) limits the size of any one live allocation group. This is one of the most important safeguards for services that handle arbitrary uploads. For broader guidance on allocator behavior and tradeoffs, see [Memory Management](memorymanagement.md). @@ -118,6 +118,6 @@ For ImageSharp.Web command signing, see [Securing Requests in ImageSharp.Web](.. - Use `Identify()` first whenever a full decode is not necessary. - Use `GetPixelMemorySize()` during identify-based preflight when you need a decoded memory budget check. - Use `TargetSize`, `MaxFrames`, and `SkipMetadata` to shrink decode cost up front. -- Prefer `IgnoreNone` or the default `IgnoreNonCritical` over broader error ignoring on untrusted inputs. +- Prefer [`Strict`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.Strict) or the default [`IgnoreAncillary`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreAncillary) over broader error ignoring on untrusted inputs. - Restrict the enabled format modules when your workload only needs a few codecs. - Use allocator limits and host-level request limits together rather than relying on only one layer. diff --git a/articles/imagesharp/troubleshooting.md b/articles/imagesharp/troubleshooting.md index 15765f1f2..ec52a4723 100644 --- a/articles/imagesharp/troubleshooting.md +++ b/articles/imagesharp/troubleshooting.md @@ -64,7 +64,7 @@ Ways to reduce decode cost: - use [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) when a smaller decode is acceptable; - use [`DecoderOptions.MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) to cap animated formats; - use [`DecoderOptions.SkipMetadata`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.SkipMetadata) when metadata is not needed; -- adjust [`MemoryAllocatorOptions.AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) for a larger per-allocation budget, or [`MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) for a larger total live allocator budget. +- adjust [`MemoryAllocatorOptions.AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) for a larger per-allocation budget. Also avoid turning on [`PreferContiguousImageBuffers`](xref:SixLabors.ImageSharp.Configuration.PreferContiguousImageBuffers) unless you explicitly need contiguous memory for interop. diff --git a/articles/polygonclipper/booleanoperations.md b/articles/polygonclipper/booleanoperations.md index 22f995b2f..734251954 100644 --- a/articles/polygonclipper/booleanoperations.md +++ b/articles/polygonclipper/booleanoperations.md @@ -4,10 +4,10 @@ Boolean operations are the center of PolygonClipper. They let you combine or sub The public entry points are the static methods on [`PolygonClipper`](xref:SixLabors.PolygonClipper.PolygonClipper): -- `Intersection(subject, clip)` -- `Union(subject, clip)` -- `Difference(subject, clip)` -- `Xor(subject, clip)` +- [`Intersection(subject, clip)`](xref:SixLabors.PolygonClipper.PolygonClipper.Intersection*) +- [`Union(subject, clip)`](xref:SixLabors.PolygonClipper.PolygonClipper.Union*) +- [`Difference(subject, clip)`](xref:SixLabors.PolygonClipper.PolygonClipper.Difference*) +- [`Xor(subject, clip)`](xref:SixLabors.PolygonClipper.PolygonClipper.Xor*) These are also the recommended entry points in the source, because they route work through internal reusable instances. @@ -28,7 +28,7 @@ A few quick cases make the behavior easier to picture: - if the two polygons are identical, `Xor` returns an empty result; - if one polygon sits inside the other, `Xor` keeps the outer region and removes the shared inner region. -`Difference` is the one where argument order matters most. `Difference(a, b)` is not the same as `Difference(b, a)`. +[`Difference`](xref:SixLabors.PolygonClipper.PolygonClipper.Difference*) is the one where argument order matters most. `Difference(a, b)` is not the same as `Difference(b, a)`. ## Run a Boolean Operation diff --git a/articles/polygonclipper/gettingstarted.md b/articles/polygonclipper/gettingstarted.md index e5e28d8c5..1e313326c 100644 --- a/articles/polygonclipper/gettingstarted.md +++ b/articles/polygonclipper/gettingstarted.md @@ -44,12 +44,12 @@ You do not need to repeat the first vertex at the end of a contour for normal po Most applications should call the static methods: -- `PolygonClipper.Intersection(...)` -- `PolygonClipper.Union(...)` -- `PolygonClipper.Difference(...)` -- `PolygonClipper.Xor(...)` -- `PolygonClipper.Normalize(...)` -- `PolygonStroker.Stroke(...)` +- [`PolygonClipper.Intersection(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Intersection*) +- [`PolygonClipper.Union(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Union*) +- [`PolygonClipper.Difference(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Difference*) +- [`PolygonClipper.Xor(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Xor*) +- [`PolygonClipper.Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) +- [`PolygonStroker.Stroke(...)`](xref:SixLabors.PolygonClipper.PolygonStroker.Stroke*) Those are the recommended entry points in the source and route work through internal reusable instances. The instance constructors are there for advanced manual flows, but they are not the usual starting point. diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index addade1e1..7162e9b43 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -1,6 +1,6 @@ # PolygonClipper -PolygonClipper is Six Labors' focused geometry library for polygon boolean operations, contour normalization, and stroke-outline generation in managed .NET. It is designed for real 2D geometry workloads: non-convex shapes, holes, multiple contours, overlapping edges, and inputs that need canonicalized output. +PolygonClipper is Six Labors' high-performance focused geometry library for polygon boolean operations, contour normalization, and stroke-outline generation in managed .NET. It is designed for real 2D geometry workloads: non-convex shapes, holes, multiple contours, overlapping edges, and inputs that need canonicalized output. The current package targets [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/overview). If you already use [ImageSharp.Drawing](../imagesharp.drawing/index.md), you may already be relying on PolygonClipper indirectly: ImageSharp.Drawing uses it internally for boolean operations against paths and for stroke geometry generation. @@ -49,6 +49,6 @@ paket add SixLabors.PolygonClipper --version VERSION_NUMBER - [Getting Started](gettingstarted.md) walks through building a polygon from contours and vertices, then running a first boolean operation. - [Polygons, Contours, and Holes](polygonsandcontours.md) explains the library's core data model and how hierarchy is represented. -- [Boolean Operations](booleanoperations.md) covers `Intersection`, `Union`, `Difference`, and `Xor`, including subject-versus-clip semantics. -- [Normalization and Winding](normalization.md) explains when to use `Normalize(...)` to resolve self-intersections and overlaps into positive-winding output. -- [Stroking](stroking.md) covers `PolygonStroker`, `StrokeOptions`, joins, caps, and open-versus-closed path behavior. +- [Boolean Operations](booleanoperations.md) covers [`Intersection`](xref:SixLabors.PolygonClipper.PolygonClipper.Intersection*), [`Union`](xref:SixLabors.PolygonClipper.PolygonClipper.Union*), [`Difference`](xref:SixLabors.PolygonClipper.PolygonClipper.Difference*), and [`Xor`](xref:SixLabors.PolygonClipper.PolygonClipper.Xor*), including subject-versus-clip semantics. +- [Normalization and Winding](normalization.md) explains when to use [`Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) to resolve self-intersections and overlaps into positive-winding output. +- [Stroking](stroking.md) covers [`PolygonStroker`](xref:SixLabors.PolygonClipper.PolygonStroker), [`StrokeOptions`](xref:SixLabors.PolygonClipper.StrokeOptions), joins, caps, and open-versus-closed path behavior. diff --git a/articles/polygonclipper/normalization.md b/articles/polygonclipper/normalization.md index 01d1c6714..1d585c09a 100644 --- a/articles/polygonclipper/normalization.md +++ b/articles/polygonclipper/normalization.md @@ -4,7 +4,7 @@ Boolean operations combine two polygons. Normalization is different: it cleans u That makes [`PolygonClipper.Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) the right tool when your input geometry is already yours, but its contours are messy enough that you want a cleaner region description before export, rendering, or further processing. -## When to Use `Normalize(...)` +## When to Use [`Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) Normalization is useful when: @@ -44,7 +44,7 @@ Normalization is a separate pipeline from the two-input boolean operations. In P ## Normalization Is Not Required for Every Workflow -You do not need to call `Normalize(...)` before every boolean operation. The boolean APIs already process complex polygon inputs. +You do not need to call [`Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) before every boolean operation. The boolean APIs already process complex polygon inputs. Reach for normalization when your goal is specifically: diff --git a/articles/polygonclipper/polygonsandcontours.md b/articles/polygonclipper/polygonsandcontours.md index 08c9c8542..f0c3b2abc 100644 --- a/articles/polygonclipper/polygonsandcontours.md +++ b/articles/polygonclipper/polygonsandcontours.md @@ -43,10 +43,10 @@ That is enough for a single simple region. As soon as you need holes or multiple Contours can participate in a parent-child hierarchy: -- `ParentIndex` points to the owning contour when a contour is a hole or nested child; -- `HoleCount` and `GetHoleIndex(...)` let an outer contour enumerate its direct holes; -- `Depth` records how deeply nested the contour is; -- `IsExternal` is `true` when `ParentIndex` is `null`. +- [`ParentIndex`](xref:SixLabors.PolygonClipper.Contour.ParentIndex) points to the owning contour when a contour is a hole or nested child; +- [`HoleCount`](xref:SixLabors.PolygonClipper.Contour.HoleCount) and [`GetHoleIndex(...)`](xref:SixLabors.PolygonClipper.Contour.GetHoleIndex*) let an outer contour enumerate its direct holes; +- [`Depth`](xref:SixLabors.PolygonClipper.Contour.Depth) records how deeply nested the contour is; +- [`IsExternal`](xref:SixLabors.PolygonClipper.Contour.IsExternal) is `true` when `ParentIndex` is `null`. If you already know the hierarchy of your input data, you can represent it directly: @@ -81,11 +81,11 @@ When you do not already know the hierarchy, boolean operations and normalization [`Contour`](xref:SixLabors.PolygonClipper.Contour) also exposes orientation helpers: -- `IsCounterClockwise()` -- `IsClockwise()` -- `Reverse()` -- `SetClockwise()` -- `SetCounterClockwise()` +- [`IsCounterClockwise()`](xref:SixLabors.PolygonClipper.Contour.IsCounterClockwise*) +- [`IsClockwise()`](xref:SixLabors.PolygonClipper.Contour.IsClockwise*) +- [`Reverse()`](xref:SixLabors.PolygonClipper.Contour.Reverse*) +- [`SetClockwise()`](xref:SixLabors.PolygonClipper.Contour.SetClockwise*) +- [`SetCounterClockwise()`](xref:SixLabors.PolygonClipper.Contour.SetCounterClockwise*) Those are useful when you are inspecting or preparing contours, but you do not need to normalize orientation by hand for every workflow. If your real goal is to resolve messy self-overlapping input into canonical positive-winding output, use [`PolygonClipper.Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*). @@ -93,8 +93,8 @@ Those are useful when you are inspecting or preparing contours, but you do not n Both polygons and contours can answer a few practical geometry questions without running a boolean operation: -- `GetBoundingBox()` returns a [`Box2`](xref:SixLabors.PolygonClipper.Box2) -- `Translate(x, y)` offsets the geometry in place +- [`GetBoundingBox()`](xref:SixLabors.PolygonClipper.Polygon.GetBoundingBox*) returns a [`Box2`](xref:SixLabors.PolygonClipper.Box2) +- [`Translate(x, y)`](xref:SixLabors.PolygonClipper.Polygon.Translate*) offsets the geometry in place ```csharp using SixLabors.PolygonClipper; diff --git a/articles/polygonclipper/stroking.md b/articles/polygonclipper/stroking.md index 0dea51876..4b5c63d1d 100644 --- a/articles/polygonclipper/stroking.md +++ b/articles/polygonclipper/stroking.md @@ -6,7 +6,7 @@ That makes it useful both for standalone geometry workflows and for renderers th ## Use the Static Entry Point -Most callers should use the static method: +Most callers should use the static [`PolygonStroker.Stroke(...)`](xref:SixLabors.PolygonClipper.PolygonStroker.Stroke*) method: ```csharp using SixLabors.PolygonClipper; @@ -45,9 +45,7 @@ StrokeOptions options = new() { LineJoin = LineJoin.Round, LineCap = LineCap.Round, - InnerJoin = InnerJoin.Round, MiterLimit = 4, - InnerMiterLimit = 1.01, ArcDetailScale = 1, NormalizeOutput = true }; @@ -57,12 +55,11 @@ Polygon outline = PolygonStroker.Stroke(source, 12, options); The main knobs are: -- `LineJoin` for outer corners; -- `LineCap` for open-path ends; -- `InnerJoin` for sharp interior turns; -- `MiterLimit` and `InnerMiterLimit` for clamping long miters; -- `ArcDetailScale` for the smoothness-versus-vertex-count tradeoff on round joins and caps; -- `NormalizeOutput` when you want overlaps and self-intersections in the emitted stroke geometry resolved before returning. +- [`LineJoin`](xref:SixLabors.PolygonClipper.StrokeOptions.LineJoin) for outer corners; +- [`LineCap`](xref:SixLabors.PolygonClipper.StrokeOptions.LineCap) for open-path ends; +- [`MiterLimit`](xref:SixLabors.PolygonClipper.StrokeOptions.MiterLimit) for clamping long outer miters; +- [`ArcDetailScale`](xref:SixLabors.PolygonClipper.StrokeOptions.ArcDetailScale) for the smoothness-versus-vertex-count tradeoff on round joins and caps; +- [`NormalizeOutput`](xref:SixLabors.PolygonClipper.StrokeOptions.NormalizeOutput) when you want overlaps and self-intersections in the emitted stroke geometry resolved before returning. `NormalizeOutput` defaults to `false` for throughput. When you leave it off, render the returned geometry with a non-zero winding fill rule. @@ -89,4 +86,4 @@ Negative widths are supported for advanced scenarios. They flip the emitted side ## Used by ImageSharp.Drawing -ImageSharp.Drawing also uses PolygonClipper for stroke geometry generation. Its higher-level stroke options are mapped down to PolygonClipper's `LineJoin`, `LineCap`, `InnerJoin`, miter, and normalization settings before outline polygons are generated. +ImageSharp.Drawing also uses PolygonClipper for stroke geometry generation. Its higher-level stroke options are mapped down to PolygonClipper's `LineJoin`, `LineCap`, miter, and normalization settings before outline polygons are generated. diff --git a/articles/toc.md b/articles/toc.md index 8a45843cf..d0328ada4 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -39,6 +39,22 @@ # [ImageSharp.Drawing](imagesharp.drawing/index.md) ## [Getting Started](imagesharp.drawing/gettingstarted.md) +## [Canvas Drawing](imagesharp.drawing/canvas.md) +## [Primitive Drawing Helpers](imagesharp.drawing/primitives.md) +## [Paths and Shapes](imagesharp.drawing/pathsandshapes.md) +## [Brushes and Pens](imagesharp.drawing/brushesandpens.md) +## [Clipping, Regions, and Layers](imagesharp.drawing/clippingregionslayers.md) +## [Images, Masks, and Processing](imagesharp.drawing/imagesandprocessing.md) +## [Transforms and Composition](imagesharp.drawing/transformsandcomposition.md) +## [Drawing Text](imagesharp.drawing/text.md) +## [WebGPU](imagesharp.drawing/webgpu.md) +## [Recipes](imagesharp.drawing/recipes.md) +### [Add a Text Watermark](imagesharp.drawing/watermark.md) +### [Clip an Image to a Shape](imagesharp.drawing/clipimagetoshape.md) +### [Draw a Badge or Label](imagesharp.drawing/badge.md) +### [Add Callouts and Annotations](imagesharp.drawing/annotations.md) +### [Create a Soft Shadow](imagesharp.drawing/softshadow.md) +## [Troubleshooting](imagesharp.drawing/troubleshooting.md) # [ImageSharp.Web](imagesharp.web/index.md) ## [Getting Started](imagesharp.web/gettingstarted.md) @@ -57,6 +73,9 @@ ## [Font Metadata and Inspection](fonts/fontmetadata.md) ## [Font Metrics](fonts/fontmetrics.md) ## [Measuring Text](fonts/measuringtext.md) +## [Prepared Text with TextBlock](fonts/textblock.md) +## [Hit Testing and Caret Movement](fonts/texthittesting.md) +## [Selection and Bidi Drag](fonts/caretsandselection.md) ## [Text Layout and Options](fonts/textlayout.md) ## [OpenType Features](fonts/opentypefeatures.md) ## [Hinting and Shaping](fonts/hintingandshaping.md) diff --git a/ext/Fonts b/ext/Fonts index 4b403a735..4bbe910a5 160000 --- a/ext/Fonts +++ b/ext/Fonts @@ -1 +1 @@ -Subproject commit 4b403a7357c00f5e17856be57f4de8469d31a49a +Subproject commit 4bbe910a5905eba99425455678c1bdd86c157541 diff --git a/ext/ImageSharp b/ext/ImageSharp index c5624b534..936a65bdb 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit c5624b534e6e51b5bdeed1aae5b1d7c3a9c330ae +Subproject commit 936a65bdbf2209d5e4d549c5719d7c02869c4ea2 diff --git a/ext/PolygonClipper b/ext/PolygonClipper index f52404c2e..56d393bca 160000 --- a/ext/PolygonClipper +++ b/ext/PolygonClipper @@ -1 +1 @@ -Subproject commit f52404c2ee42f2daf996c67012eab3d1223aee55 +Subproject commit 56d393bca8d2349aa8828c7357e14379e66c056a From cc40dfef63564d45f306b0334b902fd0eccc6454 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 May 2026 21:24:32 +1000 Subject: [PATCH 16/21] Fix code blocks add drawing links --- articles/fonts/index.md | 40 +- articles/imagesharp.drawing/badge.md | 2 +- articles/imagesharp.drawing/brushesandpens.md | 4 +- articles/imagesharp.drawing/canvas.md | 33 +- articles/imagesharp.drawing/gettingstarted.md | 8 +- articles/imagesharp.drawing/index.md | 48 +- .../migratingfromskiasharp.md | 432 ++++++++++++++++++ .../migratingfromsystemdrawing.md | 355 ++++++++++++++ articles/imagesharp.drawing/pathsandshapes.md | 32 +- articles/imagesharp.drawing/primitives.md | 6 +- articles/imagesharp.drawing/softshadow.md | 5 +- articles/imagesharp.drawing/text.md | 8 +- .../transformsandcomposition.md | 4 +- .../imagesharp.drawing/troubleshooting.md | 7 +- articles/imagesharp.drawing/webgpu.md | 50 +- articles/imagesharp.web/index.md | 40 +- articles/imagesharp/index.md | 41 +- articles/imagesharp/migratingfromskiasharp.md | 185 ++++++++ .../imagesharp/migratingfromsystemdrawing.md | 91 +++- articles/polygonclipper/index.md | 40 +- articles/toc.md | 3 + docfx.json | 5 +- ext/Fonts | 2 +- ext/ImageSharp | 2 +- ext/ImageSharp.Drawing | 2 +- ext/PolygonClipper | 2 +- index.md | 41 +- templates/modern/public/main.css | 107 +++++ templates/modern/public/main.js | 39 +- 29 files changed, 1510 insertions(+), 124 deletions(-) create mode 100644 articles/imagesharp.drawing/migratingfromskiasharp.md create mode 100644 articles/imagesharp.drawing/migratingfromsystemdrawing.md create mode 100644 articles/imagesharp/migratingfromskiasharp.md diff --git a/articles/fonts/index.md b/articles/fonts/index.md index 3970d5732..3e0cad97a 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -14,7 +14,7 @@ Fonts is often used underneath [ImageSharp.Drawing](../imagesharp.drawing/index. Fonts is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/Fonts/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with Fonts 3.0.0, projects that directly depend on SixLabors.Fonts require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with Fonts 3.0.0, projects that directly depend on SixLabors.Fonts require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation @@ -49,6 +49,44 @@ paket add SixLabors.Fonts --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. +### How to use the license file + +Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. + +If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: + +```xml + + path/to/sixlabors.lic + +``` + +If you do not want to store the license on disk, pass the license string directly from an environment variable or secret store. When extracting the value from `sixlabors.lic`, use the full file contents, not only the `Key` field: + +```xml + + $(SIXLABORS_LICENSE_KEY) + +``` + +You can also pass the key to common .NET CLI commands. + +PowerShell: + +```powershell +dotnet build -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +``` + +Bash and other shells that expand environment variables with `$NAME`: + +```bash +dotnet build -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +``` + +Build as normal after the file or property is configured. If the license is missing or invalid, the build fails with a clear error. You do not need to reference the licensing package directly; it is carried by Six Labors libraries. + ### Start Here If you are new to Fonts, start with [Loading Fonts and Collections](gettingstarted.md) and then use the pages below to branch into the topics your application needs. diff --git a/articles/imagesharp.drawing/badge.md b/articles/imagesharp.drawing/badge.md index e935f47be..221ed9eb8 100644 --- a/articles/imagesharp.drawing/badge.md +++ b/articles/imagesharp.drawing/badge.md @@ -44,7 +44,7 @@ image.Mutate(ctx => ctx.Paint(canvas => image.Save("badge.png"); ``` -Use a path type that matches the badge geometry you want. `RectangularPolygon`, `EllipsePolygon`, `RegularPolygon`, `Star`, and custom `PathBuilder` paths can all be filled and stroked through the same canvas calls. +Use a path type that matches the badge geometry you want. [`RectangularPolygon`](xref:SixLabors.ImageSharp.Drawing.RectangularPolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`Star`](xref:SixLabors.ImageSharp.Drawing.Star), and custom [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) paths can all be filled and stroked through the same canvas calls. ## Related Topics diff --git a/articles/imagesharp.drawing/brushesandpens.md b/articles/imagesharp.drawing/brushesandpens.md index 655fc8ea7..dd7875d82 100644 --- a/articles/imagesharp.drawing/brushesandpens.md +++ b/articles/imagesharp.drawing/brushesandpens.md @@ -27,7 +27,7 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Pattern Brushes and Pattern Pens -The `Brushes` and `Pens` factories include common hatch and dash styles. +The [`Brushes`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes) and [`Pens`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens) factories include common hatch and dash styles. ```csharp using SixLabors.ImageSharp; @@ -128,7 +128,7 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Image and Matrix Pattern Brushes -`PatternBrush` repeats a matrix of foreground/background values across the target. Use the `Brushes` helpers for common hatch styles, or construct a `PatternBrush` when you need a custom repeating matrix. +[`PatternBrush`](xref:SixLabors.ImageSharp.Drawing.Processing.PatternBrush) repeats a matrix of foreground/background values across the target. Use the [`Brushes`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes) helpers for common hatch styles, or construct a [`PatternBrush`](xref:SixLabors.ImageSharp.Drawing.Processing.PatternBrush) when you need a custom repeating matrix. `ImageBrush` uses an image as the brush source. The source image is not disposed by the brush, so keep it alive for as long as the canvas might replay commands that reference it. diff --git a/articles/imagesharp.drawing/canvas.md b/articles/imagesharp.drawing/canvas.md index ca91af10f..e58d52b71 100644 --- a/articles/imagesharp.drawing/canvas.md +++ b/articles/imagesharp.drawing/canvas.md @@ -1,6 +1,6 @@ # Canvas Drawing -`DrawingCanvas` is the central drawing surface in ImageSharp.Drawing. You normally use it through `Paint(...)` inside an ImageSharp processing pipeline: +[`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) is the central drawing surface in ImageSharp.Drawing. You normally use it through [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) inside an ImageSharp processing pipeline: ```csharp using SixLabors.ImageSharp; @@ -24,7 +24,7 @@ The callback receives a canvas for the current frame. Use the canvas for all dra ## Deferred Drawing and Replay -`DrawingCanvas` looks immediate, but most drawing commands are recorded first and replayed later. Calls such as `Fill(...)`, `Draw(...)`, `DrawText(...)`, and `SaveLayer(...)` append drawing intent to a command buffer. Calls that must happen at a specific point, such as `Apply(...)` and `RenderScene(...)`, are stored as entries in the canvas replay timeline. +[`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) looks immediate, but most drawing commands are recorded first and replayed later. Calls such as [`Fill(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Fill*), [`Draw(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Draw*), [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*), and [`SaveLayer(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.SaveLayer*) append drawing intent to a command buffer. Calls that must happen at a specific point, such as [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) and [`RenderScene(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.RenderScene*) are stored as entries in the canvas replay timeline. The root canvas replays the timeline when it is disposed. During replay, command ranges are prepared into backend command batches, and the backend creates and renders scenes for those ranges. This is why a manually-created canvas must be disposed: disposal is the point where recorded work is actually rendered into the target. @@ -36,28 +36,27 @@ The replay timeline can contain three kinds of entry: This deferred model lets ImageSharp.Drawing use one public canvas API for CPU images, WebGPU surfaces, and retained backend scenes. The canvas records drawing intent once, performs shared preparation once, and then hands a stable command batch to the active backend. -`Flush()` seals the commands recorded so far into a command-range timeline entry. It does not render immediately by itself. Most code does not need it; use it when a later operation must appear after the current commands in replay order. +[`Flush()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Flush) seals the commands recorded so far into a command-range timeline entry. It does not render immediately by itself. Most code does not need it; replay barriers such as [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) already seal earlier commands before they run. ```csharp image.Mutate(ctx => ctx.Paint(canvas => { canvas.Fill(Brushes.Solid(Color.LightGray)); - // Seal the fill before the blur barrier so the blur sees the filled pixels. - canvas.Flush(); + // Apply is a replay barrier, so the blur sees the earlier fill. canvas.Apply(new Rectangle(40, 40, 180, 120), region => region.GaussianBlur(6)); canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(40, 40, 180, 120)); })); ``` -Inside `Paint(...)`, ImageSharp.Drawing owns the canvas lifetime. When you call `CreateCanvas(...)` yourself, your `using` statement is what triggers replay. +Inside [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions), ImageSharp.Drawing owns the canvas lifetime. When you call [`CreateCanvas(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvasFactoryExtensions.CreateCanvas*) yourself, your `using` statement is what triggers replay. ## Paint Versus CreateCanvas -Use `Paint(...)` for normal `Mutate(...)` and `Clone(...)` pipelines. It follows ImageSharp's processor model and handles each frame for you. +Use [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) for normal `Mutate(...)` and `Clone(...)` pipelines. It follows ImageSharp's processor model and handles each frame for you. -Use `CreateCanvas(...)` when you already have an image frame and want to manage the canvas lifetime yourself. Disposing the canvas replays the recorded work into the target frame. +Use [`CreateCanvas(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvasFactoryExtensions.CreateCanvas*) when you already have an image frame and want to manage the canvas lifetime yourself. Disposing the canvas replays the recorded work into the target frame. ```csharp using SixLabors.ImageSharp; @@ -73,9 +72,9 @@ canvas.Draw(Pens.Dash(Color.Navy, 3), new Rectangle(18, 18, 284, 144)); ## Clear and Fill -Use `Fill(...)` when you want normal brush compositing. Use `Clear(...)` when you want to replace pixels in the covered area, including replacing them with transparent pixels. +Use [`Fill(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Fill*) when you want normal brush compositing. Use [`Clear(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Clear*) when you want to replace pixels in the covered area, including replacing them with transparent pixels. -`Clear(...)` can target the full canvas, a rectangle, or any `IPath`. It also honors the active clip state created by `Save(...)`, so clears can be scoped by both the supplied clear shape and the current canvas state. +[`Clear(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Clear*) can target the full canvas, a rectangle, or any [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath). It also honors the active clip state created by [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*), so clears can be scoped by both the supplied clear shape and the current canvas state. ```csharp using SixLabors.ImageSharp; @@ -114,25 +113,25 @@ image.Mutate(ctx => ctx.Paint(canvas => ## State and Storage -`Save()` stores the current drawing state on a stack and `Restore()` returns to the previous state. The state includes drawing options, clip paths, target bounds, and layer information for later commands. +[`Save()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save) stores the current drawing state on a stack and [`Restore()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Restore) returns to the previous state. The state includes drawing options, clip paths, target bounds, and layer information for later commands. -The overload `Save(DrawingOptions, params IPath[])` stores the supplied `DrawingOptions` instance by reference. Treat options passed to `Save(...)` as owned by the active canvas state until that state has been restored. +The overload [`Save(DrawingOptions, params IPath[])`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*) stores the supplied [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) instance by reference. Treat options passed to [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*) as owned by the active canvas state until that state has been restored. -The active state reference is captured when each command is recorded. Later `Save(...)` or `Restore()` calls do not replace the state for commands already in the command buffer, but mutating a referenced `DrawingOptions` instance can still affect commands that captured that same instance. +The active state reference is captured when each command is recorded. Later [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*) or [`Restore()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Restore) calls do not replace the state for commands already in the command buffer, but mutating a referenced [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) instance can still affect commands that captured that same instance. The state captured for drawing includes: -- `DrawingOptions`, including graphics options, shape options, and transform -- clip paths supplied to `Save(DrawingOptions, params IPath[])` +- [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions), including graphics options, shape options, and transform +- clip paths supplied to [`Save(DrawingOptions, params IPath[])`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*) - target bounds for the active canvas or region - destination offset for region canvases - whether the command is being recorded inside a layer -`Save()` pushes a normal state frame. `SaveLayer(...)` pushes a layer state frame. Only layer state frames create layer boundary commands when restored. +[`Save()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save) pushes a normal state frame. [`SaveLayer(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.SaveLayer*) pushes a layer state frame. Only layer state frames create layer boundary commands when restored. ## Save and Restore State -`Save(...)` pushes the current drawing state. The overload that accepts `DrawingOptions` and clip paths replaces the active state until you call `Restore()`. +[`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*) pushes the current drawing state. The overload that accepts [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) and clip paths replaces the active state until you call [`Restore()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Restore). ```csharp using SixLabors.ImageSharp; diff --git a/articles/imagesharp.drawing/gettingstarted.md b/articles/imagesharp.drawing/gettingstarted.md index 7bd412fbb..98b4b477f 100644 --- a/articles/imagesharp.drawing/gettingstarted.md +++ b/articles/imagesharp.drawing/gettingstarted.md @@ -7,7 +7,7 @@ ImageSharp.Drawing adds vector drawing, brush and pen styling, and text renderin 1. Create or load an `Image`. 2. Call `Mutate(...)`. -3. Use `Paint(...)` to receive a `DrawingCanvas`. +3. Use [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) to receive a [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas). 4. Draw onto the canvas with brushes, pens, paths, shapes, images, or text. The same canvas can mix all of those operations. This model scales from small badges to poster-style artwork, route maps, typography sheets, image masking, and WebGPU scenes. @@ -35,7 +35,7 @@ image.Mutate(ctx => ctx.Paint(canvas => image.Save("star.png"); ``` -`Paint(...)` creates a canvas for each frame being processed. Drawing is recorded through that canvas and applied when the paint operation runs. +[`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) creates a canvas for each frame being processed. Drawing is recorded through that canvas and applied when the paint operation runs. ## Combine Drawing Operations @@ -90,7 +90,7 @@ image.Save("composition.png"); ## Use Drawing Options -`DrawingOptions` controls the shared drawing state used by the canvas. The most common settings are graphics options for blending and antialiasing, shape options for fill behavior, and transforms for vector output. +[`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls the shared drawing state used by the canvas. The most common settings are graphics options for blending and antialiasing, shape options for fill behavior, and transforms for vector output. ```csharp using System.Numerics; @@ -126,7 +126,7 @@ image.Mutate(ctx => ctx.Paint(options, canvas => ## Draw Text -Text drawing uses SixLabors.Fonts for font discovery, shaping, measurement, and layout. Use `RichTextOptions` when you draw directly to a canvas. +Text drawing uses SixLabors.Fonts for font discovery, shaping, measurement, and layout. Use [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) when you draw directly to a canvas. ```csharp using SixLabors.Fonts; diff --git a/articles/imagesharp.drawing/index.md b/articles/imagesharp.drawing/index.md index d65308cb2..5fcc5dbbe 100644 --- a/articles/imagesharp.drawing/index.md +++ b/articles/imagesharp.drawing/index.md @@ -7,16 +7,18 @@ ImageSharp.Drawing is designed from the ground up to be high-performance, flexib ### Start Here -- [Getting Started](gettingstarted.md) introduces the `Paint(...)` and `DrawingCanvas` workflow. +- [Getting Started](gettingstarted.md) introduces the [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) and [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) workflow. - [Canvas Drawing](canvas.md) covers canvas state, clipping, regions, and applying ImageSharp processors to drawn regions. - [Primitive Drawing Helpers](primitives.md) covers rectangles, ellipses, arcs, pies, lines, and Bezier helpers. - [Paths and Shapes](pathsandshapes.md) covers built-in shapes, custom paths, and fill rules. - [Brushes and Pens](brushesandpens.md) covers solid, pattern, and gradient fills plus stroke options. - [Clipping, Regions, and Layers](clippingregionslayers.md) covers clip paths, region canvases, save/restore state, and isolated layer composition. -- [Images, Masks, and Processing](imagesandprocessing.md) covers `DrawImage(...)`, image brushes, clipping masks, and `Apply(...)`. +- [Images, Masks, and Processing](imagesandprocessing.md) covers [`DrawImage(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawImage*), image brushes, clipping masks, and [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*). - [Transforms and Composition](transformsandcomposition.md) covers transforms, blending, alpha composition, and antialiasing. -- [Drawing Text](text.md) covers `RichTextOptions`, measuring, and text along paths. +- [Drawing Text](text.md) covers [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), measuring, and text along paths. - [WebGPU](webgpu.md) covers GPU-backed windows, external surfaces, and offscreen render targets. +- [Migrating from System.Drawing](migratingfromsystemdrawing.md) maps common GDI+ drawing concepts to ImageSharp.Drawing. +- [Migrating from SkiaSharp](migratingfromskiasharp.md) maps common SkiaSharp drawing concepts to ImageSharp.Drawing. - [Recipes](recipes.md) provides copy-pasteable solutions for common drawing tasks. - [Troubleshooting](troubleshooting.md) covers common canvas, clipping, text, image, and WebGPU issues. @@ -26,7 +28,7 @@ Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/d ImageSharp.Drawing is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with ImageSharp.Drawing 3.0.0, projects that directly depend on ImageSharp.Drawing require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with ImageSharp.Drawing 3.0.0, projects that directly depend on ImageSharp.Drawing require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation @@ -60,3 +62,41 @@ paket add SixLabors.ImageSharp.Drawing --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. + +### How to use the license file + +Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. + +If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: + +```xml + + path/to/sixlabors.lic + +``` + +If you do not want to store the license on disk, pass the license string directly from an environment variable or secret store. When extracting the value from `sixlabors.lic`, use the full file contents, not only the `Key` field: + +```xml + + $(SIXLABORS_LICENSE_KEY) + +``` + +You can also pass the key to common .NET CLI commands. + +PowerShell: + +```powershell +dotnet build -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +``` + +Bash and other shells that expand environment variables with `$NAME`: + +```bash +dotnet build -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +``` + +Build as normal after the file or property is configured. If the license is missing or invalid, the build fails with a clear error. You do not need to reference the licensing package directly; it is carried by Six Labors libraries. diff --git a/articles/imagesharp.drawing/migratingfromskiasharp.md b/articles/imagesharp.drawing/migratingfromskiasharp.md new file mode 100644 index 000000000..63c7fd73b --- /dev/null +++ b/articles/imagesharp.drawing/migratingfromskiasharp.md @@ -0,0 +1,432 @@ +# Migrating from SkiaSharp + +If you are coming from SkiaSharp, the biggest adjustment is the rendering model. SkiaSharp code is usually centered on an `SKCanvas` supplied by the destination you are drawing to: a bitmap, raster surface, GPU surface, document, or picture recorder. ImageSharp.Drawing works inside the ImageSharp processing pipeline and records drawing commands through [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) before replaying them to the active backend. + +That difference is useful. The same drawing code can target normal CPU-backed images, retained scenes, and WebGPU-backed surfaces while keeping the same shape, brush, pen, text, and image composition model. + +## Core Type Mapping + +| SkiaSharp concept | ImageSharp.Drawing equivalent | +|---|---| +| `SKBitmap` / `SKImage` / `SKSurface` | `Image` for CPU images, or a WebGPU surface/render target for GPU output | +| `SKCanvas` | [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) inside [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions), or a canvas created from an image frame or backend | +| `SKPaint` fill | [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush), usually [`Brushes.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes.Solid*), gradient brushes, image brushes, or pattern brushes | +| `SKPaint` stroke | [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), usually [`Pens.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Solid*) or a custom `Pen` with stroke options | +| `SKColor` | `Color`, or a concrete pixel type such as `Rgba32` when working directly with pixels | +| `SKRect` / `SKRoundRect` | `Rectangle`, `RectangleF`, and shape types such as [`RectangularPolygon`](xref:SixLabors.ImageSharp.Drawing.RectangularPolygon) | +| `SKPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shapes such as [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon) | +| `SKMatrix` | `Matrix4x4` transforms, commonly constructed from `Matrix3x2` | +| `SKImageFilter` / `SKMaskFilter` | `Apply(...)` with ImageSharp processors for region-scoped effects | +| `SKTextBlob` / text drawing | [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), [`TextBlock`](xref:SixLabors.Fonts.TextBlock), Fonts shaping, and [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) | + +## Drawing Targets and Paint Pipelines + +In SkiaSharp, you draw through the `SKCanvas` provided by the current destination. A canvas backed by a raster bitmap or raster surface writes to pixels visible to the CPU. A GPU-surface canvas targets GPU work that is flushed or submitted later. A document or picture-recorder canvas records drawing commands instead of exposing writable pixels. + +For simple bitmap code, that often looks like this: + +SkiaSharp: + +SkiaSharp positions text by baseline. Offset by ascent when you want to match a top-left drawing origin. + +```csharp +using SkiaSharp; + +using SKBitmap bitmap = new(420, 240); +using SKCanvas canvas = new(bitmap); +using SKPaint paint = new() +{ + Color = SKColors.CornflowerBlue, + IsAntialias = true +}; + +canvas.Clear(SKColors.White); +canvas.DrawRect(SKRect.Create(40, 40, 260, 110), paint); +``` + +In ImageSharp.Drawing, draw inside an ImageSharp mutation pipeline: + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +image.Mutate(context => context.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(40, 40, 260, 110)); +})); +``` + +Use `Image.Mutate(...)` when you want to modify an existing image. Use `Image.Clone(...)` when your old SkiaSharp code created a new output from an existing source while leaving the source unchanged. + +## Paint Becomes Brush and Pen + +SkiaSharp uses `SKPaint` as a general drawing state object. The same type can represent fill, stroke, antialiasing, shaders, blend modes, filters, text settings, and more. + +ImageSharp.Drawing splits those concepts into smaller objects: + +- [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush) describes how an area is filled. +- [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen) describes how outlines are stroked. +- [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls antialiasing, transforms, blending, and shape behavior. +- [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) controls text layout and shaping. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKPaint fill = new() +{ + Color = SKColor.Parse("#2f80ed"), + IsAntialias = true, + Style = SKPaintStyle.Fill +}; + +using SKPaint stroke = new() +{ + Color = SKColor.Parse("#1b3f72"), + IsAntialias = true, + StrokeWidth = 4, + Style = SKPaintStyle.Stroke +}; + +canvas.DrawRect(SKRect.Create(48, 42, 280, 126), fill); +canvas.DrawRect(SKRect.Create(48, 42, 280, 126), stroke); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.ParseHex("#2f80ed")), new RectangleF(48, 42, 280, 126)); + canvas.Draw(Pens.Solid(Color.ParseHex("#1b3f72"), 4), new RectangleF(48, 42, 280, 126)); +})); +``` + +This is usually the cleanest migration path: create brushes and pens where SkiaSharp code previously configured fill and stroke paints. + +## Paths and Shapes + +SkiaSharp path code usually builds an `SKPath`, then fills or strokes it. ImageSharp.Drawing uses [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) for incremental construction and [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath) for the finished geometry. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKPath triangle = new(); +triangle.MoveTo(80, 180); +triangle.LineTo(160, 48); +triangle.LineTo(240, 180); +triangle.Close(); + +using SKPaint fill = new() +{ + Color = SKColors.Gold, + IsAntialias = true, + Style = SKPaintStyle.Fill +}; + +using SKPaint stroke = new() +{ + Color = SKColors.DarkGoldenrod, + IsAntialias = true, + StrokeWidth = 4, + Style = SKPaintStyle.Stroke +}; + +canvas.DrawPath(triangle, fill); +canvas.DrawPath(triangle, stroke); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + PathBuilder builder = new(); + builder.MoveTo(new PointF(80, 180)); + builder.LineTo(new PointF(160, 48)); + builder.LineTo(new PointF(240, 180)); + builder.CloseFigure(); + + IPath triangle = builder.Build(); + + canvas.Fill(Brushes.Solid(Color.Gold), triangle); + canvas.Draw(Pens.Solid(Color.DarkGoldenrod, 4), triangle); +})); +``` + +For common geometry, prefer the built-in shapes instead of manually building paths: + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKPaint fill = new() +{ + Color = SKColors.MediumSeaGreen, + IsAntialias = true, + Style = SKPaintStyle.Fill +}; + +canvas.DrawOval(SKRect.Create(70, 72, 220, 96), fill); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), new EllipsePolygon(180, 120, 220, 96)); +})); +``` + +## Transforms and Canvas State + +SkiaSharp commonly uses `Save()`, `Restore()`, `Translate()`, `Scale()`, and `RotateDegrees()` on the canvas. ImageSharp.Drawing exposes the same idea through canvas state and `Matrix4x4` transforms. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKPaint fillPaint = new() +{ + Color = SKColors.HotPink, + IsAntialias = true, + Style = SKPaintStyle.Fill +}; + +using SKPaint strokePaint = new() +{ + Color = SKColors.White, + IsAntialias = true, + StrokeWidth = 3, + Style = SKPaintStyle.Stroke +}; + +canvas.Save(); +canvas.Translate(210, 120); +canvas.Scale(1.2F, 0.8F); +canvas.DrawRect(SKRect.Create(-70, -24, 140, 48), fillPaint); +canvas.DrawRect(SKRect.Create(-70, -24, 140, 48), strokePaint); +canvas.Restore(); +``` + +ImageSharp.Drawing: + +```csharp +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + DrawingOptions options = new() + { + Transform = new( + Matrix3x2.CreateScale(1.2F, 0.8F) * + Matrix3x2.CreateTranslation(210, 120)) + }; + + _ = canvas.Save(options); + canvas.Fill(Brushes.Solid(Color.HotPink), new RectangleF(-70, -24, 140, 48)); + canvas.Draw(Pens.Solid(Color.White, 3), new RectangleF(-70, -24, 140, 48)); + + canvas.Restore(); +})); +``` + +ImageSharp.Drawing uses `Matrix4x4` because the same transform model works across CPU rendering, retained scenes, and WebGPU output. For normal 2D drawing, construct it from `Matrix3x2` so the affine values stay familiar. + +## Image Composition + +SkiaSharp image composition often uses `DrawImage(...)` or `DrawBitmap(...)`. In ImageSharp.Drawing, use [`DrawImage(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawImage*) inside [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) when the operation belongs with the rest of the drawing commands. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKBitmap source = SKBitmap.Decode("photo.jpg"); +using SKBitmap output = new(640, 360); +using SKCanvas canvas = new(output); + +using SKPaint strokePaint = new() +{ + Color = SKColors.White, + IsAntialias = true, + StrokeWidth = 4, + Style = SKPaintStyle.Stroke +}; + +canvas.Clear(SKColors.White); +canvas.DrawBitmap(source, SKRect.Create(32, 32, 320, 220)); +canvas.DrawRect(SKRect.Create(32, 32, 320, 220), strokePaint); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image output = new(640, 360, Color.White.ToPixel()); + +output.Mutate(context => context.Paint(canvas => +{ + canvas.DrawImage(source, source.Bounds, new RectangleF(32, 32, 320, 220)); + canvas.Draw(Pens.Solid(Color.White, 4), new RectangleF(32, 32, 320, 220)); +})); +``` + +Keep source images alive until the canvas has replayed. Inside `Paint(...)`, replay is owned by the processing operation. If you create and manage a canvas yourself, dispose or flush it before disposing source images used by drawing commands. + +## Region Effects + +SkiaSharp often applies blur, masking, or filters through paint filters or image filters. ImageSharp.Drawing uses [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) to run normal ImageSharp processors inside a rectangle or path. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKPaint shadowPaint = new() +{ + Color = SKColors.Black.WithAlpha(89), + ImageFilter = SKImageFilter.CreateBlur(10, 10), + IsAntialias = true, + Style = SKPaintStyle.Fill +}; + +using SKPaint panelFillPaint = new() +{ + Color = SKColors.White, + IsAntialias = true, + Style = SKPaintStyle.Fill +}; + +using SKPaint panelStrokePaint = new() +{ + Color = SKColors.LightGray, + IsAntialias = true, + StrokeWidth = 1, + Style = SKPaintStyle.Stroke +}; + +canvas.DrawRect(SKRect.Create(70, 72, 280, 110), shadowPaint); +canvas.DrawRect(SKRect.Create(62, 58, 280, 110), panelFillPaint); +canvas.DrawRect(SKRect.Create(62, 58, 280, 110), panelStrokePaint); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.35F)), new Rectangle(70, 72, 280, 110)); + + // Blur a larger region so the softened shadow can spread beyond the source rectangle. + canvas.Apply(new Rectangle(60, 62, 300, 130), region => region.GaussianBlur(10)); + + canvas.Fill(Brushes.Solid(Color.White), new Rectangle(62, 58, 280, 110)); + canvas.Draw(Pens.Solid(Color.LightGray, 1), new Rectangle(62, 58, 280, 110)); +})); +``` + +On GPU-backed canvases, `Apply(...)` may require readback into the CPU ImageSharp pipeline. Keep the affected region tight, just as you would keep Skia image filters scoped to the area that actually needs the effect. + +## Text + +SkiaSharp text drawing can start simple, but richer layout usually involves `SKTextBlob`, font managers, shaping, and manual measurement. ImageSharp.Drawing uses SixLabors.Fonts directly, so advanced text layout is part of the normal drawing API. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKTypeface typeface = SKTypeface.FromFile("Inter.ttf"); +using SKFont font = new(typeface, 32); +using SKPaint paint = new() +{ + Color = SKColors.Black, + IsAntialias = true +}; + +SKFontMetrics metrics; +font.GetFontMetrics(out metrics); + +canvas.DrawText("Fast text layout for generated graphics", 48, 48 - metrics.Ascent, font, paint); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + FontCollection collection = new(); + FontFamily family = collection.Add("Inter.ttf"); + Font font = family.CreateFont(32); + + RichTextOptions options = new(font) + { + Origin = new PointF(48, 48) + }; + + canvas.DrawText(options, "Fast text layout for generated graphics", Brushes.Solid(Color.Black), pen: null); +})); +``` + +For manual line flow, measurement, caret movement, or rich spans, use the Fonts docs alongside the Drawing text guide. + +## Practical Migration Strategy + +For most SkiaSharp migrations: + +1. Move bitmap load/save work to ImageSharp. +2. Replace `SKCanvas` drawing blocks with `image.Mutate(context => context.Paint(canvas => ...))`. +3. Replace fill `SKPaint` objects with [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush) instances. +4. Replace stroke `SKPaint` objects with [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen) instances. +5. Replace `SKPath` construction with [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), or built-in shape types. +6. Replace canvas transform calls with saved canvas state and `Matrix4x4` values constructed from `Matrix3x2`. +7. Replace image filters with `Apply(...)` where a normal ImageSharp processor gives the same effect. +8. Move text code to [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), [`TextBlock`](xref:SixLabors.Fonts.TextBlock), and the Fonts layout APIs when measurement or wrapping matters. + +You do not need to migrate everything at once. ImageSharp.Drawing is usually easiest to adopt by moving one rendering workflow at a time: generate the same output image, replace the paint/path/text concepts with the closest Drawing equivalents, then simplify once the new model is in place. diff --git a/articles/imagesharp.drawing/migratingfromsystemdrawing.md b/articles/imagesharp.drawing/migratingfromsystemdrawing.md new file mode 100644 index 000000000..2ee1dae4a --- /dev/null +++ b/articles/imagesharp.drawing/migratingfromsystemdrawing.md @@ -0,0 +1,355 @@ +# Migrating from System.Drawing + +If you are coming from `System.Drawing`, the biggest adjustment is moving from a `Graphics` object over a `Bitmap` to an ImageSharp image pipeline with [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) and [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas). + +The drawing concepts still map cleanly. `Graphics` becomes [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas), `Brush` and `Pen` become ImageSharp.Drawing brushes and pens, `GraphicsPath` becomes [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) or [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and text moves to the Fonts-powered [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) APIs. + +For core image loading, saving, pixel formats, and raw pixel access, see the ImageSharp [Migrating from System.Drawing](../imagesharp/migratingfromsystemdrawing.md) guide. This page focuses on drawing code. + +## Core Type Mapping + +| `System.Drawing` concept | ImageSharp.Drawing equivalent | +|---|---| +| `Bitmap` | `Image` | +| `Graphics` | [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) inside [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions), or a canvas created from an image frame | +| `System.Drawing.Color` | `SixLabors.ImageSharp.Color`, or a concrete pixel type such as `Rgba32` | +| `SolidBrush` / `TextureBrush` | `Brushes.Solid(...)`, image brushes, pattern brushes, gradient brushes | +| `Pen` | [`SixLabors.ImageSharp.Drawing.Processing.Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), usually through [`Pens.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Solid*) | +| `Rectangle` / `RectangleF` | `Rectangle` / `RectangleF` | +| `GraphicsPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shape types | +| `Matrix` | `Matrix4x4`, commonly constructed from `Matrix3x2` | +| `Graphics.DrawImage(...)` | `DrawingCanvas.DrawImage(...)` | +| `Graphics.DrawString(...)` | [`DrawingCanvas.DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) with [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) | + +## Graphics vs Paint Pipelines + +In `System.Drawing`, drawing usually starts by creating a `Graphics` object from a `Bitmap`: + +System.Drawing: + +```csharp +using System.Drawing; + +using Bitmap bitmap = new(420, 240); +using Graphics graphics = Graphics.FromImage(bitmap); +using SolidBrush brush = new(Color.CornflowerBlue); + +graphics.Clear(Color.White); +graphics.FillRectangle(brush, new Rectangle(40, 40, 260, 110)); +``` + +In ImageSharp.Drawing, draw inside an ImageSharp mutation pipeline: + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = new(420, 240, Color.White.ToPixel()); + +image.Mutate(context => context.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(40, 40, 260, 110)); +})); +``` + +Use `Mutate(...)` when you want to update an image in place. Use `Clone(...)` when the old code created a separate output bitmap while keeping the source unchanged. + +## Brushes and Pens + +`System.Drawing` separates filled shapes and stroked outlines through `Brush` and `Pen`. ImageSharp.Drawing keeps the same mental model, but uses its own brush and pen types. + +System.Drawing: + +```csharp +using System.Drawing; + +using SolidBrush fill = new(Color.FromArgb(255, 47, 128, 237)); +using Pen stroke = new(Color.FromArgb(255, 27, 63, 114), 4); + +graphics.FillRectangle(fill, new RectangleF(48, 42, 280, 126)); +graphics.DrawRectangle(stroke, 48, 42, 280, 126); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32(47, 128, 237, 255))), new RectangleF(48, 42, 280, 126)); + canvas.Draw(Pens.Solid(Color.FromPixel(new Rgba32(27, 63, 114, 255)), 4), new RectangleF(48, 42, 280, 126)); +})); +``` + +## Paths and Shapes + +`GraphicsPath` maps to [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) when you are constructing custom geometry. For simple rectangles, ellipses, arcs, and lines, prefer the built-in Drawing helpers and shape types. + +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Drawing2D; + +using GraphicsPath triangle = new(); +triangle.StartFigure(); +triangle.AddLine(80, 180, 160, 48); +triangle.AddLine(160, 48, 240, 180); +triangle.CloseFigure(); + +using SolidBrush fill = new(Color.Gold); +using Pen stroke = new(Color.DarkGoldenrod, 4); + +graphics.FillPath(fill, triangle); +graphics.DrawPath(stroke, triangle); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + PathBuilder builder = new(); + builder.MoveTo(new PointF(80, 180)); + builder.LineTo(new PointF(160, 48)); + builder.LineTo(new PointF(240, 180)); + builder.CloseFigure(); + + IPath triangle = builder.Build(); + + canvas.Fill(Brushes.Solid(Color.Gold), triangle); + canvas.Draw(Pens.Solid(Color.DarkGoldenrod, 4), triangle); +})); +``` + +For common geometry, use shape types directly: + +System.Drawing: + +```csharp +using System.Drawing; + +using SolidBrush fill = new(Color.MediumSeaGreen); + +graphics.FillEllipse(fill, new RectangleF(70, 72, 220, 96)); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + // EllipsePolygon takes center and size; this matches the System.Drawing bounds above. + canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), new EllipsePolygon(180, 120, 220, 96)); +})); +``` + +## Transforms and Canvas State + +`System.Drawing.Graphics` stores transform state on the `Graphics` object. ImageSharp.Drawing stores transform state in [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions), which can be saved onto the canvas state stack. + +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Drawing2D; + +using SolidBrush fill = new(Color.HotPink); +using Pen stroke = new(Color.White, 3); +using Matrix transform = new(1.2F, 0, 0, 0.8F, 210, 120); + +GraphicsState state = graphics.Save(); +graphics.Transform = transform; +graphics.FillRectangle(fill, new RectangleF(-70, -24, 140, 48)); +graphics.DrawRectangle(stroke, -70, -24, 140, 48); +graphics.Restore(state); +``` + +ImageSharp.Drawing: + +```csharp +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + DrawingOptions options = new() + { + Transform = new( + Matrix3x2.CreateScale(1.2F, 0.8F) * + Matrix3x2.CreateTranslation(210, 120)) + }; + + _ = canvas.Save(options); + canvas.Fill(Brushes.Solid(Color.HotPink), new RectangleF(-70, -24, 140, 48)); + canvas.Draw(Pens.Solid(Color.White, 3), new RectangleF(-70, -24, 140, 48)); + + canvas.Restore(); +})); +``` + +ImageSharp.Drawing uses `Matrix4x4` for canvas transforms so the same drawing state can represent normal 2D affine transforms and projective transforms. For normal 2D drawing, construct it from `Matrix3x2`. + +## Image Composition + +If your `System.Drawing` code uses `Graphics.DrawImage(...)`, use `DrawImage(...)` inside `Paint(...)` when the image placement belongs with the rest of the drawing commands. + +System.Drawing: + +```csharp +using System.Drawing; + +using Bitmap source = new("photo.jpg"); +using Bitmap output = new(640, 360); +using Graphics graphics = Graphics.FromImage(output); + +using Pen stroke = new(Color.White, 4); + +graphics.Clear(Color.White); +graphics.DrawImage(source, new RectangleF(32, 32, 320, 220)); +graphics.DrawRectangle(stroke, 32, 32, 320, 220); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image source = Image.Load("photo.jpg"); +using Image output = new(640, 360, Color.White.ToPixel()); + +output.Mutate(context => context.Paint(canvas => +{ + canvas.DrawImage(source, source.Bounds, new RectangleF(32, 32, 320, 220)); + canvas.Draw(Pens.Solid(Color.White, 4), new RectangleF(32, 32, 320, 220)); +})); +``` + +Keep source images alive until the canvas has replayed. Inside `Paint(...)`, replay is owned by the processing operation. If you create and manage a canvas yourself, dispose or flush it before disposing source images used by drawing commands. + +## Clipping + +`Graphics.SetClip(...)` maps to saving canvas state with clip paths. Restore the state when the clipped drawing is complete. + +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Drawing2D; + +using GraphicsPath clip = new(); +clip.AddEllipse(60, 40, 260, 160); + +GraphicsState state = graphics.Save(); +graphics.SetClip(clip); +graphics.FillRectangle(Brushes.CornflowerBlue, new Rectangle(20, 20, 360, 200)); +graphics.Restore(state); +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + _ = canvas.Save(new DrawingOptions(), new EllipsePolygon(190, 120, 260, 160)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(20, 20, 360, 200)); + + canvas.Restore(); +})); +``` + +## Text + +`Graphics.DrawString(...)` handles simple text drawing. ImageSharp.Drawing uses SixLabors.Fonts through `DrawText(...)`, so wrapping, alignment, shaping, fallback, and rich text options are part of the normal text pipeline. + +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Text; + +using PrivateFontCollection collection = new(); +collection.AddFontFile("Inter.ttf"); + +using Font font = new(collection.Families[0], 32); +using StringFormat format = new() +{ + Alignment = StringAlignment.Center +}; + +graphics.DrawString( + "Fast text layout for generated graphics", + font, + Brushes.Black, + new RectangleF(48, 48, 320, 120), + format); + +``` + +ImageSharp.Drawing: + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Processing; + +image.Mutate(context => context.Paint(canvas => +{ + FontCollection collection = new(); + FontFamily family = collection.Add("Inter.ttf"); + Font font = family.CreateFont(32); + + RichTextOptions options = new(font) + { + Origin = new PointF(48, 48), + WrappingLength = 320, + HorizontalAlignment = HorizontalAlignment.Center + }; + + canvas.DrawText(options, "Fast text layout for generated graphics", Brushes.Solid(Color.Black), pen: null); +})); +``` + +## Practical Migration Strategy + +For most `System.Drawing` drawing migrations: + +1. Move bitmap load/save work to ImageSharp. +2. Replace `Graphics.FromImage(...)` blocks with `image.Mutate(context => context.Paint(canvas => ...))`. +3. Replace `SolidBrush`, `TextureBrush`, and gradient brushes with ImageSharp.Drawing brushes. +4. Replace `System.Drawing.Pen` with `Pens.Solid(...)` or a custom ImageSharp.Drawing pen. +5. Replace `GraphicsPath` with [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), or built-in shape types. +6. Replace `Graphics` transform state with saved canvas state and `Matrix4x4` values constructed from `Matrix3x2`. +7. Replace `SetClip(...)` with `Save(options, clipPaths)` and `Restore()`. +8. Replace `DrawString(...)` with [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*), [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), and the Fonts layout APIs when wrapping or shaping matters. + +You do not have to migrate all drawing code at once. Start with one rendering workflow, match the output, then simplify the code once the ImageSharp.Drawing model is in place. diff --git a/articles/imagesharp.drawing/pathsandshapes.md b/articles/imagesharp.drawing/pathsandshapes.md index cd4a9566a..97d3b61a0 100644 --- a/articles/imagesharp.drawing/pathsandshapes.md +++ b/articles/imagesharp.drawing/pathsandshapes.md @@ -4,15 +4,15 @@ ImageSharp.Drawing separates geometry from painting. Shapes and paths describe w The core geometry types are: -- `IPath` for any path-like shape that can be filled or stroked. -- `Path` for an open path made from line segments, arcs, and curves. -- `Polygon` for a closed path. -- `ComplexPolygon` for a shape made from multiple paths, such as an outer contour with holes. -- `Polygon`, `RectangularPolygon`, `EllipsePolygon`, `RegularPolygon`, `Star`, and `Pie` for common shapes. -- `PathBuilder` when you want to construct a custom path from line and curve commands. -- `PathCollection` when one operation should cover several paths. +- [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath) for any path-like shape that can be filled or stroked. +- [`Path`](xref:SixLabors.ImageSharp.Drawing.Path) for an open path made from line segments, arcs, and curves. +- [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon) for a closed path. +- [`ComplexPolygon`](xref:SixLabors.ImageSharp.Drawing.ComplexPolygon) for a shape made from multiple paths, such as an outer contour with holes. +- [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon), [`RectangularPolygon`](xref:SixLabors.ImageSharp.Drawing.RectangularPolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`Star`](xref:SixLabors.ImageSharp.Drawing.Star), and [`Pie`](xref:SixLabors.ImageSharp.Drawing.Pie) for common shapes. +- [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) when you want to construct a custom path from line and curve commands. +- [`PathCollection`](xref:SixLabors.ImageSharp.Drawing.PathCollection) when one operation should cover several paths. -`IPath.PathType` tells you whether a path is open, closed, or mixed. A mixed path is a composite path containing both open and closed figures. +[`IPath.PathType`](xref:SixLabors.ImageSharp.Drawing.IPath.PathType) tells you whether a path is open, closed, or mixed. A mixed path is a composite path containing both open and closed figures. ## Built-In Shapes @@ -48,7 +48,7 @@ image.Mutate(ctx => ctx.Paint(canvas => Open paths are useful for strokes, polylines, and curved baselines. Closed paths enclose an area and are the normal input for fills. -`Path` is open by default. `Polygon` is closed. `PathBuilder.CloseFigure()` closes the current figure before starting the next one. +[`Path`](xref:SixLabors.ImageSharp.Drawing.Path) is open by default. [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon) is closed. [`PathBuilder.CloseFigure()`](xref:SixLabors.ImageSharp.Drawing.PathBuilder.CloseFigure) closes the current figure before starting the next one. ```csharp using SixLabors.ImageSharp; @@ -88,7 +88,7 @@ When you fill an open path, ImageSharp.Drawing closes it for fill processing. Pr ## Custom Paths and Figures -Use `PathBuilder` for custom geometry. Build the path once, then reuse it for fill and stroke operations. +Use [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) for custom geometry. Build the path once, then reuse it for fill and stroke operations. ```csharp using SixLabors.ImageSharp; @@ -116,7 +116,7 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` -`PathBuilder` supports multiple figures. If the builder contains more than one figure, `Build()` returns a `ComplexPolygon`. Each figure keeps its own open or closed state. +[`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) supports multiple figures. If the builder contains more than one figure, [`Build()`](xref:SixLabors.ImageSharp.Drawing.PathBuilder.Build*) returns a [`ComplexPolygon`](xref:SixLabors.ImageSharp.Drawing.ComplexPolygon). Each figure keeps its own open or closed state. ```csharp using SixLabors.ImageSharp; @@ -146,11 +146,11 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` -Use `PathBuilder.StartFigure()` when you want to begin a new figure without closing the previous one. Use `CloseAllFigures()` when every current figure should be closed. +Use [`PathBuilder.StartFigure()`](xref:SixLabors.ImageSharp.Drawing.PathBuilder.StartFigure) when you want to begin a new figure without closing the previous one. Use [`CloseAllFigures()`](xref:SixLabors.ImageSharp.Drawing.PathBuilder.CloseAllFigures) when every current figure should be closed. ## Complex Polygons and Holes -`ComplexPolygon` represents multiple paths as one path. It is useful when a shape has multiple contours, or when you want to model an outer contour and one or more holes. +[`ComplexPolygon`](xref:SixLabors.ImageSharp.Drawing.ComplexPolygon) represents multiple paths as one path. It is useful when a shape has multiple contours, or when you want to model an outer contour and one or more holes. ```csharp using SixLabors.ImageSharp; @@ -191,7 +191,7 @@ The fill rule decides how overlapping contours inside a complex polygon are inte ## Path Collections -`PathCollection` groups paths so one draw or fill call can apply the same brush, pen, and drawing state to all of them. +[`PathCollection`](xref:SixLabors.ImageSharp.Drawing.PathCollection) groups paths so one draw or fill call can apply the same brush, pen, and drawing state to all of them. ```csharp using SixLabors.ImageSharp; @@ -214,11 +214,11 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` -Use a `PathCollection` when paths remain independent. Use `ComplexPolygon` or `Clip(...)` when the contours need to be interpreted together as one shape. +Use a [`PathCollection`](xref:SixLabors.ImageSharp.Drawing.PathCollection) when paths remain independent. Use [`ComplexPolygon`](xref:SixLabors.ImageSharp.Drawing.ComplexPolygon) or [`Clip(...)`](xref:SixLabors.ImageSharp.Drawing.ClipPathExtensions.Clip*) when the contours need to be interpreted together as one shape. ## Clipping and Boolean Operations -`Clip(...)` creates a new path from a subject path and one or more clipping paths. The operation comes from `ShapeOptions.BooleanOperation`. The default boolean operation is `Difference`, which subtracts the clipping paths from the subject. +[`Clip(...)`](xref:SixLabors.ImageSharp.Drawing.ClipPathExtensions.Clip*) creates a new path from a subject path and one or more clipping paths. The operation comes from [`ShapeOptions.BooleanOperation`](xref:SixLabors.ImageSharp.Drawing.Processing.ShapeOptions.BooleanOperation). The default boolean operation is [`Difference`](xref:SixLabors.ImageSharp.Drawing.BooleanOperation.Difference), which subtracts the clipping paths from the subject. ```csharp using SixLabors.ImageSharp; diff --git a/articles/imagesharp.drawing/primitives.md b/articles/imagesharp.drawing/primitives.md index 4765d5860..222435054 100644 --- a/articles/imagesharp.drawing/primitives.md +++ b/articles/imagesharp.drawing/primitives.md @@ -1,8 +1,8 @@ # Primitive Drawing Helpers -Primitive helpers are convenience methods on `DrawingCanvas` for common geometry. Use them when the shape is simple and you do not need to keep an `IPath` instance around. +Primitive helpers are convenience methods on [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) for common geometry. Use them when the shape is simple and you do not need to keep an [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath) instance around. -The helpers still follow the same rules as path drawing: fills use brushes, strokes use pens, `DrawingOptions` controls antialiasing and transforms, and active canvas state applies to the recorded command. +The helpers still follow the same rules as path drawing: fills use brushes, strokes use pens, [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls antialiasing and transforms, and active canvas state applies to the recorded command. ## Rectangles, Ellipses, Lines, and Beziers @@ -75,7 +75,7 @@ image.Mutate(ctx => ctx.Paint(canvas => ## When to Use Paths Instead -Use `PathBuilder`, `Polygon`, `ComplexPolygon`, or a built-in shape type when you need to: +Use [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon), [`ComplexPolygon`](xref:SixLabors.ImageSharp.Drawing.ComplexPolygon), or a built-in shape type when you need to: - reuse or transform the same geometry; - combine multiple figures into one shape; diff --git a/articles/imagesharp.drawing/softshadow.md b/articles/imagesharp.drawing/softshadow.md index 21f4229c2..f7c8efbfc 100644 --- a/articles/imagesharp.drawing/softshadow.md +++ b/articles/imagesharp.drawing/softshadow.md @@ -1,6 +1,6 @@ # Create a Soft Shadow -Draw the shadow shape first, flush it, then apply a blur to the shadow region before drawing the foreground object. `Apply(...)` is a replay barrier, so only commands recorded before the barrier are processed. +Draw the shadow shape first, then apply a blur to the shadow region before drawing the foreground object. `Apply(...)` is a replay barrier, so only commands recorded before the barrier are processed. ```csharp using SixLabors.ImageSharp; @@ -18,8 +18,7 @@ image.Mutate(ctx => ctx.Paint(canvas => { canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.35F)), shadowBounds); - // Flush makes the shadow pixels available to the blur barrier before the panel is drawn. - canvas.Flush(); + // Apply seals earlier drawing commands before the blur is replayed. canvas.Apply(shadowBounds, region => region.GaussianBlur(10)); canvas.Fill(Brushes.Solid(Color.White), panelBounds); diff --git a/articles/imagesharp.drawing/text.md b/articles/imagesharp.drawing/text.md index 5b565e976..f5b884b5f 100644 --- a/articles/imagesharp.drawing/text.md +++ b/articles/imagesharp.drawing/text.md @@ -106,7 +106,7 @@ Run indices are counted in grapheme clusters, not UTF-16 code units. `Start` is ## Draw Prepared Text -Use [TextBlock](../fonts/textblock.md) when the same text will be measured, wrapped, inspected, or drawn more than once. `TextBlock` keeps the prepared text layout work in the Fonts layer, and `DrawingCanvas.DrawText(...)` places that prepared block onto the canvas. +Use [TextBlock](../fonts/textblock.md) when the same text will be measured, wrapped, inspected, or drawn more than once. [`TextBlock`](xref:SixLabors.Fonts.TextBlock) keeps the prepared text layout work in the Fonts layer, and [`DrawingCanvas.DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) places that prepared block onto the canvas. ```csharp using SixLabors.Fonts; @@ -139,7 +139,7 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` -For manual line flow, choose the `TextBlock` API based on the coordinate space you want to draw from: +For manual line flow, choose the [`TextBlock`](xref:SixLabors.Fonts.TextBlock) API based on the coordinate space you want to draw from: - Use `TextBlock.GetLineLayouts(...)` when the text still behaves as one stacked block. Each returned `LineLayout` is positioned in block coordinates, including the cumulative advance of the lines before it, so it is ready to draw relative to the block origin. - Use `TextBlock.EnumerateLineLayouts()` when each line is placed independently. Each `LineLayout` is line-local, as if it were the first line in the block, and the caller supplies the final canvas position or path when calling `DrawingCanvas.DrawText(...)`. @@ -148,7 +148,7 @@ The line-local enumerator is the right fit for text that flows through different ## Wrap and Align Text -`RichTextOptions` inherits the core Fonts text options and adds ImageSharp.Drawing-specific rich text behavior. +[`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) inherits the core Fonts text options and adds ImageSharp.Drawing-specific rich text behavior. ```csharp using SixLabors.Fonts; @@ -219,7 +219,7 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Draw Text Along a Path -Text can also follow an `IPath`. +Text can also follow an [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath). ```csharp using SixLabors.Fonts; diff --git a/articles/imagesharp.drawing/transformsandcomposition.md b/articles/imagesharp.drawing/transformsandcomposition.md index 2e22b8b4c..3d2bc2436 100644 --- a/articles/imagesharp.drawing/transformsandcomposition.md +++ b/articles/imagesharp.drawing/transformsandcomposition.md @@ -1,6 +1,6 @@ # Transforms and Composition -`DrawingOptions` carries the transform, graphics options, and shape options used by canvas commands. Use it when drawing state should change for a group of operations. +[`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) carries the transform, graphics options, and shape options used by canvas commands. Use it when drawing state should change for a group of operations. ## Transform Drawing @@ -30,7 +30,7 @@ using Image image = new(420, 260, Color.White.ToPixel()); DrawingOptions rotated = new() { - Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.32F, new Vector2(210, 130))) + Transform = new(Matrix3x2.CreateRotation(0.32F, new Vector2(210, 130))) }; RectangularPolygon panel = new(92, 70, 236, 120); diff --git a/articles/imagesharp.drawing/troubleshooting.md b/articles/imagesharp.drawing/troubleshooting.md index fc615d451..7f41029a1 100644 --- a/articles/imagesharp.drawing/troubleshooting.md +++ b/articles/imagesharp.drawing/troubleshooting.md @@ -83,13 +83,12 @@ This matters for emoji, combining marks, flags, and other user-perceived charact ## Processors Run Before Earlier Drawing -Canvas operations are ordered, but image processors operate at replay barriers. If you need a processor such as blur, opacity, or a mask operation to include drawing that has already been recorded, flush the canvas before applying the processor. +Canvas operations are ordered, but image processors operate at replay barriers. A processor such as blur, opacity, or a mask operation includes drawing that was recorded before the `Apply(...)` call. ```csharp canvas.Fill(Color.Black, shadowShape); -// Flush seals the shadow geometry before the blur processor is applied. -canvas.Flush(); +// Apply seals the shadow geometry before the blur processor is applied. canvas.Apply(x => x.GaussianBlur(8)); ``` @@ -123,7 +122,7 @@ using (frame) } ``` -Resize the `WebGPUExternalSurface` when the framebuffer size changes. If you need to read pixels back to the CPU, use a pixel type that matches the target texture format, for example `Rgba32` with an `Rgba8Unorm` target. +Resize the [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when the framebuffer size changes. If you need to read pixels back to the CPU, use a pixel type that matches the target texture format, for example `Rgba32` with an `Rgba8Unorm` target. ## A Good Debugging Order diff --git a/articles/imagesharp.drawing/webgpu.md b/articles/imagesharp.drawing/webgpu.md index a7f0ca720..722b6c6ce 100644 --- a/articles/imagesharp.drawing/webgpu.md +++ b/articles/imagesharp.drawing/webgpu.md @@ -1,6 +1,6 @@ # WebGPU -ImageSharp.Drawing.WebGPU provides a GPU-backed drawing target for the same `DrawingCanvas` API used by the CPU image pipeline. +ImageSharp.Drawing.WebGPU provides a GPU-backed drawing target for the same [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) API used by the CPU image pipeline. Use the WebGPU package when you want ImageSharp.Drawing to render into a native WebGPU surface or an offscreen GPU texture. Use the regular ImageSharp.Drawing package when you want to draw directly into an `Image` on the CPU. @@ -8,7 +8,7 @@ Use the WebGPU package when you want ImageSharp.Drawing to render into a native WebGPU is a modern, explicit GPU API. It gives an application access to a graphics adapter, a device, command queues, textures, buffers, shaders, and presentation surfaces. It is conceptually similar to modern native graphics APIs such as Vulkan, Metal, and Direct3D 12, but it exposes a portable WebGPU programming model. -In ImageSharp.Drawing, WebGPU is not a browser feature. It is a native rendering backend used by .NET applications through the `SixLabors.ImageSharp.Drawing.WebGPU` package. The package creates or attaches to native WebGPU surfaces, records `DrawingCanvas` commands, lowers those commands into GPU work, and renders them into a WebGPU texture. +In ImageSharp.Drawing, WebGPU is not a browser feature. It is a native rendering backend used by .NET applications through the `SixLabors.ImageSharp.Drawing.WebGPU` package. The package creates or attaches to native WebGPU surfaces, records [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) commands, lowers those commands into GPU work, and renders them into a WebGPU texture. The most important difference from normal ImageSharp drawing is the destination: @@ -19,7 +19,7 @@ Use WebGPU when the destination is interactive, GPU-owned, or repeatedly redrawn ## How ImageSharp.Drawing Uses WebGPU -The WebGPU backend keeps the public drawing model the same. You still draw with `DrawingCanvas`, `Brush`, `Pen`, `IPath`, `RichTextOptions`, layers, clips, and retained scenes. +The WebGPU backend keeps the public drawing model the same. You still draw with [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas), [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush), [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), layers, clips, and retained scenes. The difference is what happens when the canvas flushes or is disposed: @@ -35,11 +35,11 @@ That means WebGPU drawing is still deferred like the rest of the canvas API. The The public WebGPU API is target-first. -- `WebGPUEnvironment` probes support and configures the library-managed WebGPU environment before first use. -- `WebGPUWindow` owns a native window, WebGPU surface, device resources, and render loop. -- `WebGPUExternalSurface` attaches to a native drawable owned by another toolkit or host application. -- `WebGPURenderTarget` owns an offscreen GPU texture and can read it back into an ImageSharp image. -- `WebGPUSurfaceFrame` represents one acquired presentable frame. Dispose it to render and present the frame. +- [`WebGPUEnvironment`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment) probes support and configures the library-managed WebGPU environment before first use. +- [`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) owns a native window, WebGPU surface, device resources, and render loop. +- [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) attaches to a native drawable owned by another toolkit or host application. +- [`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) owns an offscreen GPU texture and can read it back into an ImageSharp image. +- [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) represents one acquired presentable frame. Dispose it to render and present the frame. Most application code should start by choosing the target type. You do not normally create devices, queues, or command encoders yourself. @@ -77,7 +77,7 @@ paket add SixLabors.ImageSharp.Drawing.WebGPU --version VERSION_NUMBER ## Check WebGPU Support -Use `WebGPUEnvironment` when an application needs to check support before constructing a WebGPU window, external surface, or render target. +Use [`WebGPUEnvironment`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment) when an application needs to check support before constructing a WebGPU window, external surface, or render target. ```csharp using SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -100,11 +100,11 @@ if (compute != WebGPUEnvironmentError.Success) } ``` -Assign `WebGPUEnvironment.Options` before any other WebGPU object is created. The library-managed WebGPU environment is initialized on first use. +Assign [`WebGPUEnvironment.Options`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment.Options) before any other WebGPU object is created. The library-managed WebGPU environment is initialized on first use. `ProbeAvailability()` checks whether the package can initialize the WebGPU API, create an instance, acquire an adapter, acquire a device, and get the default queue. `ProbeComputePipelineSupport()` checks whether the acquired device can create a trivial compute pipeline. The compute-pipeline probe is useful because the drawing backend depends on compute work for the staged raster pipeline. -The result is a `WebGPUEnvironmentError`. `Success` is the only successful value. Other values tell you which step failed, such as API initialization, adapter acquisition, device acquisition, queue acquisition, or compute-pipeline creation. +The result is a [`WebGPUEnvironmentError`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironmentError). `Success` is the only successful value. Other values tell you which step failed, such as API initialization, adapter acquisition, device acquisition, queue acquisition, or compute-pipeline creation. ```csharp using SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -174,7 +174,7 @@ Use `Fifo` for most applications. Use `Immediate` or `Mailbox` only when latency ## Draw to a Window -`WebGPUWindow` owns the platform window, WebGPU device resources, and frame acquisition. The render callback receives a `WebGPUSurfaceFrame`, and the frame exposes the `DrawingCanvas` for that render. +[`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) owns the platform window, WebGPU device resources, and frame acquisition. The render callback receives a [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame), and the frame exposes the [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) for that render. ```csharp using SixLabors.ImageSharp; @@ -207,7 +207,7 @@ window.Run((WebGPUSurfaceFrame frame) => `Run(Action)` is the simplest model. The window acquires a frame, gives you the frame and its canvas, disposes the frame after the callback, and presents the result. -Use the `WebGPUSurfaceFrame` overload when you need frame lifetime control or the elapsed render time. +Use the [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) overload when you need frame lifetime control or the elapsed render time. ```csharp using SixLabors.ImageSharp; @@ -229,7 +229,7 @@ window.Run((frame, elapsed) => }); ``` -`WebGPUWindow` also exposes window events and properties such as title, size, framebuffer size, render scale, position, visibility, focus, state, border, frame rate limits, and present mode. `FramebufferSize` is the size that matters for the WebGPU surface. `ClientSize` is the window coordinate size. +[`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) also exposes window events and properties such as title, size, framebuffer size, render scale, position, visibility, focus, state, border, frame rate limits, and present mode. `FramebufferSize` is the size that matters for the WebGPU surface. `ClientSize` is the window coordinate size. ## Manual Frame Acquisition @@ -265,7 +265,7 @@ while (!window.IsClosing) ## Draw to an Existing Surface -Use `WebGPUExternalSurface` when another toolkit owns the window or native drawable. Create a `WebGPUSurfaceHost` for the platform handle, notify the surface when the drawable framebuffer changes size, and acquire one `WebGPUSurfaceFrame` for each render. +Use [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when another toolkit owns the window or native drawable. Create a [`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) for the platform handle, notify the surface when the drawable framebuffer changes size, and acquire one [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) for each render. ```csharp using SixLabors.ImageSharp; @@ -299,22 +299,22 @@ void RunWin32Surface(nint hwnd, nint hinstance) } ``` -`WebGPUSurfaceHost` includes factory methods for GLFW, SDL, Win32, X11, Cocoa, UIKit, Wayland, WinRT, Android, Vivante, and EGL hosts. +[`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) includes factory methods for GLFW, SDL, Win32, X11, Cocoa, UIKit, Wayland, WinRT, Android, Vivante, and EGL hosts. The host application remains responsible for: - creating and owning the native window or drawable -- providing the correct native handles to `WebGPUSurfaceHost` +- providing the correct native handles to [`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) - calling `Resize(...)` when the drawable framebuffer size changes - calling `TryAcquireFrame(...)` from its render loop -- disposing each acquired `WebGPUSurfaceFrame` +- disposing each acquired [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) - keeping native handles valid for the lifetime of the external surface -Use `WebGPUExternalSurface` when ImageSharp.Drawing should render into an existing UI framework or native application instead of creating its own window. +Use [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when ImageSharp.Drawing should render into an existing UI framework or native application instead of creating its own window. ## Draw Offscreen -`WebGPURenderTarget` renders into an offscreen GPU texture. Create a canvas, draw into it, dispose the canvas to flush the drawing work, then read the result back when CPU image access is needed. +[`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) renders into an offscreen GPU texture. Create a canvas, draw into it, dispose the canvas to flush the drawing work, then read the result back when CPU image access is needed. ```csharp using SixLabors.ImageSharp; @@ -346,11 +346,11 @@ Readback copies GPU texture data into CPU memory. It is useful when you need an ## Choosing a Target -Use `WebGPUWindow` when ImageSharp.Drawing should own the application window and render loop. +Use [`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) when ImageSharp.Drawing should own the application window and render loop. -Use `WebGPUExternalSurface` when an existing application, UI framework, or native toolkit owns the window and event loop. +Use [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when an existing application, UI framework, or native toolkit owns the window and event loop. -Use `WebGPURenderTarget` when you want GPU rendering without a visible window, or when the output needs to be read back into an ImageSharp image. +Use [`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) when you want GPU rendering without a visible window, or when the output needs to be read back into an ImageSharp image. ## When Not to Use WebGPU @@ -374,8 +374,8 @@ Prefer WebGPU when: The important lifetime rules are: -- Dispose a `DrawingCanvas` created from `WebGPURenderTarget.CreateCanvas()` to submit its recorded work. -- Dispose a `WebGPUSurfaceFrame` to submit and present the frame. +- Dispose a [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) created from [`WebGPURenderTarget.CreateCanvas()`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget.CreateCanvas*) to submit its recorded work. +- Dispose a [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) to submit and present the frame. - Keep retained scenes alive until every canvas or frame that recorded them has been disposed. - Keep source images used by image brushes alive until the WebGPU canvas has replayed. - Call `Resize(...)` on external surfaces before acquiring the next frame after a framebuffer resize. diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index aaef8f30f..1f3c6db84 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -9,7 +9,7 @@ The current package targets .NET 8 and is built on top of [ImageSharp](../images ImageSharp.Web is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Web/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with ImageSharp.Web 4.0.0, projects that directly depend on ImageSharp.Web require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with ImageSharp.Web 4.0.0, projects that directly depend on ImageSharp.Web require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ## Install ImageSharp.Web @@ -44,6 +44,44 @@ paket add SixLabors.ImageSharp.Web --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. +## How to use the license file + +Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. + +If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: + +```xml + + path/to/sixlabors.lic + +``` + +If you do not want to store the license on disk, pass the license string directly from an environment variable or secret store. When extracting the value from `sixlabors.lic`, use the full file contents, not only the `Key` field: + +```xml + + $(SIXLABORS_LICENSE_KEY) + +``` + +You can also pass the key to common .NET CLI commands. + +PowerShell: + +```powershell +dotnet build -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +``` + +Bash and other shells that expand environment variables with `$NAME`: + +```bash +dotnet build -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +``` + +Build as normal after the file or property is configured. If the license is missing or invalid, the build fails with a clear error. You do not need to reference the licensing package directly; it is carried by Six Labors libraries. + ## Start Here - [Getting Started](gettingstarted.md) covers the minimal ASP.NET Core setup and the default provider and cache behavior. diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index fd8b127b1..9567628df 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -9,7 +9,7 @@ This section is written as a guided set of articles rather than a flat feature l ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ## Install ImageSharp @@ -44,6 +44,44 @@ paket add SixLabors.ImageSharp --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. +## How to use the license file + +Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. + +If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: + +```xml + + path/to/sixlabors.lic + +``` + +If you do not want to store the license on disk, pass the license string directly from an environment variable or secret store. When extracting the value from `sixlabors.lic`, use the full file contents, not only the `Key` field: + +```xml + + $(SIXLABORS_LICENSE_KEY) + +``` + +You can also pass the key to common .NET CLI commands. + +PowerShell: + +```powershell +dotnet build -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +``` + +Bash and other shells that expand environment variables with `$NAME`: + +```bash +dotnet build -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +``` + +Build as normal after the file or property is configured. If the license is missing or invalid, the build fails with a clear error. You do not need to reference the licensing package directly; it is carried by Six Labors libraries. + ## Start Here - [Getting Started](gettingstarted.md) walks through the core image types and the first end-to-end processing workflow. @@ -58,6 +96,7 @@ paket add SixLabors.ImageSharp --version VERSION_NUMBER - [Configuration](configuration.md), [Memory Management](memorymanagement.md), and [Security Considerations](security.md) cover production-focused setup. - [Troubleshooting](troubleshooting.md) covers the common failure modes around format detection, streams, memory, and disposal. - [Migrating from System.Drawing](migratingfromsystemdrawing.md) maps common GDI-style workflows to ImageSharp APIs. +- [Migrating from SkiaSharp](migratingfromskiasharp.md) maps common SkiaSharp image workflows to ImageSharp APIs. - [Recipes](recipes.md) provides copy-pasteable solutions for common tasks. ## Implicit Usings diff --git a/articles/imagesharp/migratingfromskiasharp.md b/articles/imagesharp/migratingfromskiasharp.md new file mode 100644 index 000000000..0ad1d185f --- /dev/null +++ b/articles/imagesharp/migratingfromskiasharp.md @@ -0,0 +1,185 @@ +# Migrating from SkiaSharp + +If you are coming from SkiaSharp, the biggest adjustment is separating core image work from drawing work. ImageSharp owns loading, saving, metadata, pixel buffers, color conversion, and processing pipelines. ImageSharp.Drawing owns vector drawing, text, paths, and canvas composition. + +This page focuses on core ImageSharp workflows. For canvas, brush, pen, path, transform, and text migration examples, see the ImageSharp.Drawing [Migrating from SkiaSharp](../imagesharp.drawing/migratingfromskiasharp.md) guide. + +## Core Type Mapping + +| SkiaSharp concept | ImageSharp equivalent | +|---|---| +| `SKBitmap` / `SKPixmap` | [`Image`](xref:SixLabors.ImageSharp.Image`1), pixel buffers, or row access APIs | +| `SKImage` | [`Image`](xref:SixLabors.ImageSharp.Image) or [`Image`](xref:SixLabors.ImageSharp.Image`1) | +| `SKColor` | [`Color`](xref:SixLabors.ImageSharp.Color), or a concrete pixel type such as [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32) | +| `SKColorType` / `SKAlphaType` | generic `TPixel` plus [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo) | +| `SKBitmap.Decode(...)` | `Image.Load(...)` or `Image.Load(...)` | +| `SKImage.Encode(...)` | `Save(...)`, `SaveAsJpeg(...)`, `SaveAsPng(...)`, or explicit encoder types | +| `SKPixmap.GetPixelColor(...)` | indexers or `ProcessPixelRows(...)` | +| `SKPixmap` / `InstallPixels(...)` | `LoadPixelData(...)`, `WrapMemory(...)`, `CopyPixelDataTo(...)`, or `ProcessPixelRows(...)` | + +## Loading, Processing, and Saving + +A typical SkiaSharp decode, resize, and encode flow maps to ImageSharp loading, mutation, and saving. + +SkiaSharp: + +```csharp +using SkiaSharp; + +using SKBitmap source = SKBitmap.Decode("input.jpg"); +using SKBitmap output = source.Resize(new SKImageInfo(400, 300), SKFilterQuality.High); +using SKImage image = SKImage.FromBitmap(output); +using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + +using FileStream stream = File.OpenWrite("output.png"); +data.SaveTo(stream); +``` + +ImageSharp: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("input.jpg"); + +image.Mutate(context => context.Resize(400, 300)); +image.SaveAsPng("output.png"); +``` + +ImageSharp processors run through `Mutate(...)` for in-place updates or `Clone(...)` when you want a separate output image. + +## Pixels: Prefer Row Access Over Per-Pixel APIs + +If your SkiaSharp code reads or writes individual pixels, the closest ImageSharp equivalent is the image indexer. + +SkiaSharp: + +```csharp +using SkiaSharp; + +SKColor pixel = bitmap.GetPixel(10, 20); +bitmap.SetPixel(10, 20, SKColors.White); +``` + +ImageSharp: + +```csharp +using SixLabors.ImageSharp.PixelFormats; + +Rgba32 pixel = image[10, 20]; +image[10, 20] = Rgba32.White; +``` + +For real throughput, move to row access instead of per-pixel calls. + +SkiaSharp: + +```csharp +using SkiaSharp; + +for (int y = 0; y < bitmap.Height; y++) +{ + for (int x = 0; x < bitmap.Width / 2; x++) + { + SKColor left = bitmap.GetPixel(x, y); + SKColor right = bitmap.GetPixel(bitmap.Width - x - 1, y); + + bitmap.SetPixel(x, y, right); + bitmap.SetPixel(bitmap.Width - x - 1, y, left); + } +} +``` + +ImageSharp: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = Image.Load("input.png"); + +image.ProcessPixelRows(accessor => +{ + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + row.Reverse(); + } +}); +``` + +## Color and Pixel Format + +SkiaSharp often carries pixel layout through `SKImageInfo`, `SKColorType`, and `SKAlphaType`. ImageSharp makes the working pixel format explicit through `Image`. + +SkiaSharp: + +```csharp +using SkiaSharp; + +SKImageInfo info = new(640, 360, SKColorType.Rgba8888, SKAlphaType.Unpremul); +using SKBitmap bitmap = new(info); +``` + +ImageSharp: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = new(640, 360); +``` + +Use [`Color`](xref:SixLabors.ImageSharp.Color) when you want a pixel-agnostic color value, and use a concrete pixel type such as [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32) when the memory layout matters. + +## Raw Pixel Buffers + +SkiaSharp code that installs or peeks pixel memory usually maps to one of ImageSharp's explicit raw-memory APIs. + +SkiaSharp: + +```csharp +using SkiaSharp; + +SKImageInfo info = new(320, 200, SKColorType.Rgba8888, SKAlphaType.Unpremul); +using SKBitmap bitmap = new(); + +bitmap.InstallPixels(info, pixels, info.RowBytes); +``` + +ImageSharp: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = Image.LoadPixelData(pixels, 320, 200); +``` + +Use `LoadPixelData(...)` when ImageSharp should own a normal image copy. Use `WrapMemory(...)` when you need ImageSharp to operate over existing memory without copying. Use `CopyPixelDataTo(...)` when you need to export pixels. + +## Drawing APIs + +If your SkiaSharp code mainly uses `SKCanvas`, `SKPaint`, `SKPath`, or text drawing, use the ImageSharp.Drawing migration guide: + +- [Migrating from SkiaSharp in ImageSharp.Drawing](../imagesharp.drawing/migratingfromskiasharp.md) + +## Practical Migration Strategy + +For most SkiaSharp image migrations: + +1. Replace decode and encode code with `Image.Load(...)` and `Save(...)` or format-specific save methods. +2. Replace `SKBitmap` pixel storage with `Image`. +3. Replace `SKColorType` and `SKAlphaType` branching with explicit `TPixel` choices. +4. Replace per-pixel loops with `ProcessPixelRows(...)`. +5. Replace raw memory interop with `LoadPixelData(...)`, `WrapMemory(...)`, or `CopyPixelDataTo(...)`. +6. Move canvas drawing code to ImageSharp.Drawing rather than mixing it into the core image migration. + +## Related Topics + +- [Getting Started](gettingstarted.md) +- [Processing Images](processing.md) +- [Working with Pixel Buffers](pixelbuffers.md) +- [Interop and Raw Memory](interop.md) +- [Migrating from SkiaSharp in ImageSharp.Drawing](../imagesharp.drawing/migratingfromskiasharp.md) diff --git a/articles/imagesharp/migratingfromsystemdrawing.md b/articles/imagesharp/migratingfromsystemdrawing.md index 5abde8e69..a100e6336 100644 --- a/articles/imagesharp/migratingfromsystemdrawing.md +++ b/articles/imagesharp/migratingfromsystemdrawing.md @@ -20,15 +20,29 @@ Once that shift lands, most everyday workflows map over cleanly. A typical `System.Drawing` workflow translates to: +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Imaging; + +using Bitmap source = new("input.jpg"); +using Bitmap output = new(400, 300); +using Graphics graphics = Graphics.FromImage(output); + +graphics.DrawImage(source, 0, 0, 400, 300); +output.Save("output.png", ImageFormat.Png); +``` + +ImageSharp: + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using Image image = Image.Load("input.jpg"); -image.Mutate(context => context - .AutoOrient() - .Resize(400, 300)); +image.Mutate(context => context.Resize(400, 300)); image.SaveAsPng("output.png"); ``` @@ -39,6 +53,17 @@ Instead of mutating through a separate `Graphics` object, ImageSharp uses proces If you used `Bitmap.GetPixel()` or `Bitmap.SetPixel()` heavily, the closest ImageSharp equivalent is the indexer: +System.Drawing: + +```csharp +using System.Drawing; + +Color pixel = bitmap.GetPixel(10, 20); +bitmap.SetPixel(10, 20, Color.White); +``` + +ImageSharp: + ```csharp using SixLabors.ImageSharp.PixelFormats; @@ -48,6 +73,53 @@ image[10, 20] = Rgba32.White; For real throughput, move to `ProcessPixelRows(...)` instead. That is the ImageSharp replacement for most `LockBits`-driven loops: +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; + +BitmapData data = bitmap.LockBits( + new Rectangle(0, 0, bitmap.Width, bitmap.Height), + ImageLockMode.ReadWrite, + PixelFormat.Format32bppArgb); + +try +{ + int stride = Math.Abs(data.Stride); + int byteCount = stride * data.Height; + byte[] pixels = new byte[byteCount]; + + Marshal.Copy(data.Scan0, pixels, 0, pixels.Length); + + for (int y = 0; y < data.Height; y++) + { + int rowStart = y * stride; + + // Format32bppArgb stores one pixel in four bytes, so reverse pixels rather than individual bytes. + for (int x = 0; x < data.Width / 2; x++) + { + int left = rowStart + (x * 4); + int right = rowStart + ((data.Width - x - 1) * 4); + + for (int b = 0; b < 4; b++) + { + (pixels[left + b], pixels[right + b]) = (pixels[right + b], pixels[left + b]); + } + } + } + + Marshal.Copy(pixels, 0, data.Scan0, pixels.Length); +} +finally +{ + bitmap.UnlockBits(data); +} +``` + +ImageSharp: + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -79,6 +151,19 @@ That means: Instead of storing a runtime `PixelFormat` enum and branching on it later, ImageSharp encourages you to choose a generic working type: +System.Drawing: + +```csharp +using System.Drawing; +using System.Drawing.Imaging; + +using Bitmap bitmap = new("input.tiff"); + +bool isArgb = bitmap.PixelFormat == PixelFormat.Format32bppArgb; +``` + +ImageSharp: + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index 7162e9b43..48bfebbeb 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -10,7 +10,7 @@ Under the hood, the boolean-operation pipeline is based on a Martinez-Rueda swee PolygonClipper is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with PolygonClipper 1.0.0, projects that directly depend on SixLabors.PolygonClipper require a `sixlabors.lic` file to compile. By default, place the file next to your project file, or set `SixLaborsLicenseFile` in your project or shared props file to point to a central location. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with PolygonClipper 1.0.0, projects that directly depend on SixLabors.PolygonClipper require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation @@ -45,6 +45,44 @@ paket add SixLabors.PolygonClipper --version VERSION_NUMBER >[!WARNING] >Prerelease versions installed via the [Visual Studio NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) require the "include prerelease" checkbox to be checked. +### How to use the license file + +Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. + +If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: + +```xml + + path/to/sixlabors.lic + +``` + +If you do not want to store the license on disk, pass the license string directly from an environment variable or secret store. When extracting the value from `sixlabors.lic`, use the full file contents, not only the `Key` field: + +```xml + + $(SIXLABORS_LICENSE_KEY) + +``` + +You can also pass the key to common .NET CLI commands. + +PowerShell: + +```powershell +dotnet build -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$env:SIXLABORS_LICENSE_KEY" +``` + +Bash and other shells that expand environment variables with `$NAME`: + +```bash +dotnet build -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +dotnet publish -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" +``` + +Build as normal after the file or property is configured. If the license is missing or invalid, the build fails with a clear error. You do not need to reference the licensing package directly; it is carried by Six Labors libraries. + ### Start Here - [Getting Started](gettingstarted.md) walks through building a polygon from contours and vertices, then running a first boolean operation. diff --git a/articles/toc.md b/articles/toc.md index d0328ada4..3f17bbeee 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -31,6 +31,7 @@ ## [Security Considerations](imagesharp/security.md) ## [Troubleshooting](imagesharp/troubleshooting.md) ## [Migrating from System.Drawing](imagesharp/migratingfromsystemdrawing.md) +## [Migrating from SkiaSharp](imagesharp/migratingfromskiasharp.md) ## [Recipes](imagesharp/recipes.md) ### [Generate Thumbnails](imagesharp/thumbnails.md) ### [Convert Between Formats](imagesharp/formatconversion.md) @@ -48,6 +49,8 @@ ## [Transforms and Composition](imagesharp.drawing/transformsandcomposition.md) ## [Drawing Text](imagesharp.drawing/text.md) ## [WebGPU](imagesharp.drawing/webgpu.md) +## [Migrating from System.Drawing](imagesharp.drawing/migratingfromsystemdrawing.md) +## [Migrating from SkiaSharp](imagesharp.drawing/migratingfromskiasharp.md) ## [Recipes](imagesharp.drawing/recipes.md) ### [Add a Text Watermark](imagesharp.drawing/watermark.md) ### [Clip an Image to a Shape](imagesharp.drawing/clipimagetoshape.md) diff --git a/docfx.json b/docfx.json index b93250e6d..c5b5994b3 100644 --- a/docfx.json +++ b/docfx.json @@ -21,6 +21,9 @@ { "files": [ "ext/ImageSharp.Drawing/src/**.csproj" + ], + "exclude": [ + "ext/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU.ShaderGen/**.csproj" ] } ], @@ -148,4 +151,4 @@ "_appFaviconPath": "public/favicon.ico" } } -} \ No newline at end of file +} diff --git a/ext/Fonts b/ext/Fonts index 4bbe910a5..23ce95c50 160000 --- a/ext/Fonts +++ b/ext/Fonts @@ -1 +1 @@ -Subproject commit 4bbe910a5905eba99425455678c1bdd86c157541 +Subproject commit 23ce95c502b489bb01687ab322091a1640214df9 diff --git a/ext/ImageSharp b/ext/ImageSharp index 936a65bdb..2b1c7e0cf 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit 936a65bdbf2209d5e4d549c5719d7c02869c4ea2 +Subproject commit 2b1c7e0cf1d38494c9467f47b68b1d33a8aaf356 diff --git a/ext/ImageSharp.Drawing b/ext/ImageSharp.Drawing index 6d2010b0b..c063e6ca3 160000 --- a/ext/ImageSharp.Drawing +++ b/ext/ImageSharp.Drawing @@ -1 +1 @@ -Subproject commit 6d2010b0b94ea7c1172e5feae8185c471b3980d1 +Subproject commit c063e6ca3c238aed30efe19e9f82ee4cde2240f6 diff --git a/ext/PolygonClipper b/ext/PolygonClipper index 56d393bca..c6ce03e9d 160000 --- a/ext/PolygonClipper +++ b/ext/PolygonClipper @@ -1 +1 @@ -Subproject commit 56d393bca8d2349aa8828c7357e14379e66c056a +Subproject commit c6ce03e9dc74f51bcf839c4bf1814e659cbc74b0 diff --git a/index.md b/index.md index 06bbc164a..5852a9922 100644 --- a/index.md +++ b/index.md @@ -1,68 +1,66 @@ # Six Labors Documentation -We aim to provide modern, cross-platform, incredibly powerful yet beautifully simple graphics libraries. Built against .NET, our libraries can be used in device, cloud, and embedded/IoT scenarios. +Six Labors builds high-performance, cross-platform graphics libraries for modern .NET. The libraries are designed for production workloads across cloud services, desktop applications, mobile devices, and embedded/IoT environments. -You can find tutorials, examples and API details covering all Six Labors projects. +This documentation covers the full Six Labors stack: ImageSharp for image processing, ImageSharp.Drawing for 2D vector drawing, ImageSharp.Web for ASP.NET Core image middleware, Fonts for advanced text layout, and PolygonClipper for geometry operations. >[!NOTE] >Documentation for previous releases can be found at . ### [API documentation](api/index.md) -Detailed documentation for the entire API available across our projects. +Browse the generated API reference for every public type, method, property, and option across the Six Labors projects. ### Project Documentation -Our libraries are split into focused projects that work well together. They cover image processing, drawing, web middleware, fonts, and polygon clipping while keeping a consistent developer experience across the stack. - -You can find documentation for each project in the links below. +Each library is focused, but the projects are designed to work together as one graphics stack. Start with the product area closest to your task, then follow the linked guides into formats, drawing, text, middleware, or geometry as needed.
-
+
ImageSharp Logo
ImageSharp
-

Fully featured 2D graphics library.

+

Image processing for .NET with broad format support, pixel-level control, and rich metadata handling.

Learn More
-
+
ImageSharp.Drawing
-

2D polygon Manipulation and Drawing.

+

2D drawing and text rendering for ImageSharp with paths, brushes, and rich typography.

Learn More
-
+
ImageSharp.Web
-

ASP.NET Core Image Manipulation Middleware.

+

On-the-fly image processing, caching, and secure delivery for ASP.NET Core.

Learn More
-
+
Fonts
-

Font Loading and Drawing API.

+

Advanced font loading, shaping, measuring, layout, and rendering for .NET.

Learn More
-
+
PolygonClipper Logo
PolygonClipper
-

High-performance polygon clipping and stroking.

+

Boolean operations, normalization, and stroke geometry for .NET.

Learn More @@ -72,13 +70,4 @@ You can find documentation for each project in the links below. ### [Examples Repository](https://github.com/SixLabors/Samples) -We have implemented short self-contained sample projects for a few specific use cases, including: - -1. [Avatar with rounded corners](https://github.com/SixLabors/Samples/tree/main/ImageSharp/AvatarWithRoundedCorner)
- Crops rounded corners of a source image leaving a nice rounded avatar. -2. [Draw watermark on image](https://github.com/SixLabors/Samples/tree/main/ImageSharp/DrawWaterMarkOnImage)
- Draw water mark over an image automatically scaling the font size to fill the available space. -3. [Change default encoder options](https://github.com/SixLabors/Samples/tree/main/ImageSharp/ChangeDefaultEncoderOptions)
- Provides an example on how you go about switching out the registered encoder for a file format and changing its default options in the process. -4. [Draw text along a path](https://github.com/SixLabors/Samples/tree/main/ImageSharp/DrawingTextAlongAPath)
- Draw some text following the contours of a path. +The [Six Labors Samples](https://github.com/SixLabors/Samples) repository contains small, self-contained projects that show common workflows end to end. diff --git a/templates/modern/public/main.css b/templates/modern/public/main.css index 972c5e4f4..0b93aa83a 100644 --- a/templates/modern/public/main.css +++ b/templates/modern/public/main.css @@ -188,4 +188,111 @@ header .navbar { .icon { max-width: 20px; margin-right: 0.5rem; +} + +/* ################### + * #### CODE BLOCKS ## + * ################### + */ + +.frame { + position: relative; + margin: 2rem 1rem; +} + +.frame::before, +.frame::after { + content: ""; + position: absolute; + z-index: 0; + top: 0; + left: 0; + right: 0; + height: 100%; + border-radius: 0.75rem; +} + +.frame::before { + transform: rotate(-2.5deg); + background-color: #efefef; +} + +.frame::after { + transform: rotate(2.5deg); + background-image: linear-gradient(45deg, #e4d101 0%, #e30183 100%); +} + +.frame > pre { + border-radius: 0.75rem; + position: relative; + z-index: 1; + overflow-x: hidden; +} + +.frame > pre::before, +.frame > pre::after, +.frame > pre code::before { + content: ""; + position: absolute; + z-index: 1; + pointer-events: none; + top: 1rem; + left: 0.75rem; + border-radius: 100%; + width: 0.75rem; + height: 0.75rem; +} + +.frame > pre::before { + background-color: #ef4444; +} + +.frame > pre::after { + left: 1.75rem; + background-color: #fbbf24; +} + +.frame > pre code::before { + left: 2.75rem; + background-color: #4ade80; +} + +.frame > pre code::after { + content: ""; + position: absolute; + z-index: 1; + pointer-events: none; + top: 2.5rem; + left: 0.75rem; + right: 0.75rem; + height: 1px; + background-color: #e30183; + opacity: 0.25; +} + +.frame > pre code { + padding: 3.5rem 0.75rem 0.75rem 0.75rem !important; + max-width: 100%; + overflow-x: auto; +} + +@media (min-width: 1200px) { + .frame::before, + .frame::after { + left: -0.5rem; + right: -0.5rem; + } + + .frame::before { + transform: rotate(2deg); + } + + .frame::after { + transform: rotate(-2deg); + } +} + +/* Fixes for code action disapearring due to the dark-mode hack for pre */ +pre>.code-action{ + color: #fff!important; } \ No newline at end of file diff --git a/templates/modern/public/main.js b/templates/modern/public/main.js index b1c6ea436..7a4c0f9a1 100644 --- a/templates/modern/public/main.js +++ b/templates/modern/public/main.js @@ -1 +1,38 @@ -export default {} +export default { + start: () => { + // DocFX calls custom startup code before its own PDF branch completes. + // Leave PDF renders untouched so Chromium only waits on DocFX's built-ins. + if (navigator.userAgent.includes("docfx/pdf") || window.location.pathname.endsWith(".pdf")) { + return; + } + + // API reference pages contain many generated signature blocks. Keep the + // framed treatment for authored article pages. + if (document.body.dataset.yamlMime === "ManagedReference") { + return; + } + + const article = document.querySelector("article"); + if (!article) { + return; + } + + for (const pre of article.querySelectorAll("pre")) { + // Keep Highlight.js colors consistent everywhere, including DocFX tab + // groups whose structure we should not wrap. + pre.dataset.bsTheme = "dark"; + + if (pre.closest(".frame")) { + continue; + } + + // Match the main sixlabors.com code-frame markup after DocFX has emitted + // Markdown output, without adding a custom build-time post-processor. + const frame = document.createElement("div"); + frame.className = "frame"; + frame.dataset.bsTheme = "dark"; + pre.before(frame); + frame.append(pre); + } + } +}; From 0051b4571eb3704a85f3c9a08d41d02996bdaeec Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 May 2026 21:28:07 +1000 Subject: [PATCH 17/21] Update polygon names --- articles/imagesharp.drawing/badge.md | 4 ++-- articles/imagesharp.drawing/gettingstarted.md | 2 +- articles/imagesharp.drawing/imagesandprocessing.md | 2 +- articles/imagesharp.drawing/migratingfromskiasharp.md | 2 +- articles/imagesharp.drawing/pathsandshapes.md | 10 +++++----- articles/imagesharp.drawing/text.md | 2 +- .../imagesharp.drawing/transformsandcomposition.md | 2 +- articles/imagesharp.drawing/webgpu.md | 8 ++++---- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/articles/imagesharp.drawing/badge.md b/articles/imagesharp.drawing/badge.md index 221ed9eb8..690c117cd 100644 --- a/articles/imagesharp.drawing/badge.md +++ b/articles/imagesharp.drawing/badge.md @@ -12,7 +12,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 180, Color.Transparent.ToPixel()); -RectangularPolygon badge = new(24, 36, 372, 108); +RectanglePolygon badge = new(24, 36, 372, 108); Font font = SystemFonts.CreateFont("Arial", 38, FontStyle.Bold); PointF gradientStart = new(24, 36); PointF gradientEnd = new(396, 144); @@ -44,7 +44,7 @@ image.Mutate(ctx => ctx.Paint(canvas => image.Save("badge.png"); ``` -Use a path type that matches the badge geometry you want. [`RectangularPolygon`](xref:SixLabors.ImageSharp.Drawing.RectangularPolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`Star`](xref:SixLabors.ImageSharp.Drawing.Star), and custom [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) paths can all be filled and stroked through the same canvas calls. +Use a path type that matches the badge geometry you want. [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`StarPolygon`](xref:SixLabors.ImageSharp.Drawing.StarPolygon), and custom [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) paths can all be filled and stroked through the same canvas calls. ## Related Topics diff --git a/articles/imagesharp.drawing/gettingstarted.md b/articles/imagesharp.drawing/gettingstarted.md index 98b4b477f..52ce057d7 100644 --- a/articles/imagesharp.drawing/gettingstarted.md +++ b/articles/imagesharp.drawing/gettingstarted.md @@ -23,7 +23,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(320, 200, Color.White.ToPixel()); -Star star = new(x: 160, y: 100, prongs: 5, innerRadii: 42, outerRadii: 86); +StarPolygon star = new(x: 160, y: 100, prongs: 5, innerRadii: 42, outerRadii: 86); Pen outline = Pens.DashDot(Color.MidnightBlue, 4); image.Mutate(ctx => ctx.Paint(canvas => diff --git a/articles/imagesharp.drawing/imagesandprocessing.md b/articles/imagesharp.drawing/imagesandprocessing.md index 937dc1081..41a0b0633 100644 --- a/articles/imagesharp.drawing/imagesandprocessing.md +++ b/articles/imagesharp.drawing/imagesandprocessing.md @@ -54,7 +54,7 @@ using SixLabors.ImageSharp.Processing; using Image source = Image.Load("photo.jpg"); using Image image = new(420, 260, Color.White.ToPixel()); -Star star = new(x: 210, y: 130, prongs: 5, innerRadii: 62, outerRadii: 118); +StarPolygon star = new(x: 210, y: 130, prongs: 5, innerRadii: 62, outerRadii: 118); RectangleF sourceRegion = new(0, 0, source.Width, source.Height); ImageBrush brush = new(source, sourceRegion, new Point(-120, -70)); diff --git a/articles/imagesharp.drawing/migratingfromskiasharp.md b/articles/imagesharp.drawing/migratingfromskiasharp.md index 63c7fd73b..e40b2651f 100644 --- a/articles/imagesharp.drawing/migratingfromskiasharp.md +++ b/articles/imagesharp.drawing/migratingfromskiasharp.md @@ -13,7 +13,7 @@ That difference is useful. The same drawing code can target normal CPU-backed im | `SKPaint` fill | [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush), usually [`Brushes.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes.Solid*), gradient brushes, image brushes, or pattern brushes | | `SKPaint` stroke | [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), usually [`Pens.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Solid*) or a custom `Pen` with stroke options | | `SKColor` | `Color`, or a concrete pixel type such as `Rgba32` when working directly with pixels | -| `SKRect` / `SKRoundRect` | `Rectangle`, `RectangleF`, and shape types such as [`RectangularPolygon`](xref:SixLabors.ImageSharp.Drawing.RectangularPolygon) | +| `SKRect` / `SKRoundRect` | `Rectangle`, `RectangleF`, and shape types such as [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon) | | `SKPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shapes such as [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon) | | `SKMatrix` | `Matrix4x4` transforms, commonly constructed from `Matrix3x2` | | `SKImageFilter` / `SKMaskFilter` | `Apply(...)` with ImageSharp processors for region-scoped effects | diff --git a/articles/imagesharp.drawing/pathsandshapes.md b/articles/imagesharp.drawing/pathsandshapes.md index 97d3b61a0..108fc2ea6 100644 --- a/articles/imagesharp.drawing/pathsandshapes.md +++ b/articles/imagesharp.drawing/pathsandshapes.md @@ -8,7 +8,7 @@ The core geometry types are: - [`Path`](xref:SixLabors.ImageSharp.Drawing.Path) for an open path made from line segments, arcs, and curves. - [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon) for a closed path. - [`ComplexPolygon`](xref:SixLabors.ImageSharp.Drawing.ComplexPolygon) for a shape made from multiple paths, such as an outer contour with holes. -- [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon), [`RectangularPolygon`](xref:SixLabors.ImageSharp.Drawing.RectangularPolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`Star`](xref:SixLabors.ImageSharp.Drawing.Star), and [`Pie`](xref:SixLabors.ImageSharp.Drawing.Pie) for common shapes. +- [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon), [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`StarPolygon`](xref:SixLabors.ImageSharp.Drawing.StarPolygon), and [`PiePolygon`](xref:SixLabors.ImageSharp.Drawing.PiePolygon) for common shapes. - [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) when you want to construct a custom path from line and curve commands. - [`PathCollection`](xref:SixLabors.ImageSharp.Drawing.PathCollection) when one operation should cover several paths. @@ -28,8 +28,8 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 260, Color.White.ToPixel()); EllipsePolygon ellipse = new(new PointF(120, 110), new SizeF(160, 96)); -Star star = new(x: 292, y: 128, prongs: 7, innerRadii: 34, outerRadii: 72); -Pie pie = new(new PointF(120, 202), new SizeF(120, 86), startAngle: -30, sweepAngle: 245); +StarPolygon star = new(x: 292, y: 128, prongs: 7, innerRadii: 34, outerRadii: 72); +PiePolygon pie = new(new PointF(120, 202), new SizeF(120, 86), startAngle: -30, sweepAngle: 245); image.Mutate(ctx => ctx.Paint(canvas => { @@ -230,7 +230,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 240, Color.White.ToPixel()); EllipsePolygon subject = new(new PointF(190, 120), new SizeF(260, 154)); -Star cutout = new(x: 226, y: 120, prongs: 6, innerRadii: 38, outerRadii: 82); +StarPolygon cutout = new(x: 226, y: 120, prongs: 6, innerRadii: 38, outerRadii: 82); ShapeOptions clipOptions = new() { @@ -271,7 +271,7 @@ IPath shiftedPath = closedPath.Translate(0, 24); float length = openPath.ComputeLength(); float area = closedPath.ComputeArea(); RectangleF bounds = shiftedPath.Bounds; -RectangularPolygon boundsPath = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); +RectanglePolygon boundsPath = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); image.Mutate(ctx => ctx.Paint(canvas => { diff --git a/articles/imagesharp.drawing/text.md b/articles/imagesharp.drawing/text.md index f5b884b5f..a4b07db05 100644 --- a/articles/imagesharp.drawing/text.md +++ b/articles/imagesharp.drawing/text.md @@ -128,7 +128,7 @@ RichTextOptions options = new(font) TextBlock block = new("Prepared text can be measured and drawn with the same shaping.", options); TextMetrics metrics = block.Measure(wrappingLength: 520); -RectangularPolygon layoutBox = new(60, 48, 520, metrics.Advance.Height + 24); +RectanglePolygon layoutBox = new(60, 48, 520, metrics.Advance.Height + 24); image.Mutate(ctx => ctx.Paint(canvas => { diff --git a/articles/imagesharp.drawing/transformsandcomposition.md b/articles/imagesharp.drawing/transformsandcomposition.md index 3d2bc2436..492f72f82 100644 --- a/articles/imagesharp.drawing/transformsandcomposition.md +++ b/articles/imagesharp.drawing/transformsandcomposition.md @@ -33,7 +33,7 @@ DrawingOptions rotated = new() Transform = new(Matrix3x2.CreateRotation(0.32F, new Vector2(210, 130))) }; -RectangularPolygon panel = new(92, 70, 236, 120); +RectanglePolygon panel = new(92, 70, 236, 120); image.Mutate(ctx => ctx.Paint(canvas => { diff --git a/articles/imagesharp.drawing/webgpu.md b/articles/imagesharp.drawing/webgpu.md index 722b6c6ce..bba112642 100644 --- a/articles/imagesharp.drawing/webgpu.md +++ b/articles/imagesharp.drawing/webgpu.md @@ -194,7 +194,7 @@ using WebGPUWindow window = new(options); window.Run((WebGPUSurfaceFrame frame) => { DrawingCanvas canvas = frame.Canvas; - RectangularPolygon panel = new(64, 72, 320, 180); + RectanglePolygon panel = new(64, 72, 320, 180); EllipsePolygon marker = new(new PointF(224, 162), new SizeF(120, 82)); // Run supplies a frame canvas and presents it after the callback completes. @@ -256,7 +256,7 @@ while (!window.IsClosing) { DrawingCanvas canvas = frame.Canvas; canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new RectangularPolygon(40, 40, 180, 120)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new RectanglePolygon(40, 40, 180, 120)); } } ``` @@ -289,7 +289,7 @@ void RunWin32Surface(nint hwnd, nint hinstance) using (frame) { - RectangularPolygon content = new(48, 48, 320, 160); + RectanglePolygon content = new(48, 48, 320, 160); // The external UI loop owns when Render is called; the frame owns presentation. frame.Canvas.Clear(Brushes.Solid(Color.White)); @@ -327,7 +327,7 @@ using WebGPURenderTarget target = new(640, 360); using (DrawingCanvas canvas = target.CreateCanvas()) { - RectangularPolygon background = new(0, 0, target.Width, target.Height); + RectanglePolygon background = new(0, 0, target.Width, target.Height); EllipsePolygon highlight = new(new PointF(320, 180), new SizeF(260, 140)); // Disposing the canvas flushes the recorded drawing commands to the GPU target. From d6317550cc9f0b4a4755913b5aaa80f570cc3066 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 May 2026 21:44:04 +1000 Subject: [PATCH 18/21] Flesh out docs a little, add new licensing guide --- articles/fonts/checkglyphcoverage.md | 4 ++++ articles/fonts/fittexttowidth.md | 4 ++++ articles/fonts/index.md | 4 ++++ articles/fonts/inspectfontfiles.md | 4 ++++ articles/fonts/listsystemfonts.md | 4 ++++ articles/fonts/recipes.md | 9 +++++++++ articles/fonts/useopentypefeatures.md | 4 ++++ articles/imagesharp.drawing/annotations.md | 4 ++++ articles/imagesharp.drawing/badge.md | 4 ++++ articles/imagesharp.drawing/clipimagetoshape.md | 4 ++++ .../imagesharp.drawing/clippingregionslayers.md | 2 ++ articles/imagesharp.drawing/imagesandprocessing.md | 9 ++++++++- articles/imagesharp.drawing/index.md | 4 ++++ articles/imagesharp.drawing/primitives.md | 8 ++++++++ articles/imagesharp.drawing/recipes.md | 9 +++++++++ articles/imagesharp.drawing/softshadow.md | 14 ++++++++++---- .../imagesharp.drawing/transformsandcomposition.md | 5 ++++- articles/imagesharp.drawing/watermark.md | 4 ++++ articles/imagesharp.web/gettingstarted.md | 4 ++++ articles/imagesharp.web/index.md | 4 ++++ articles/imagesharp.web/processingcommands.md | 8 ++++++++ articles/imagesharp/cropandcanvas.md | 8 ++++++++ articles/imagesharp/formatconversion.md | 7 +++++++ articles/imagesharp/identify.md | 3 +++ articles/imagesharp/index.md | 4 ++++ articles/imagesharp/pixelformats.md | 4 ++++ articles/imagesharp/processing.md | 7 +++++++ articles/imagesharp/recipes.md | 7 +++++++ articles/imagesharp/stripmetadata.md | 4 ++++ articles/imagesharp/thumbnails.md | 7 +++++++ articles/polygonclipper/gettingstarted.md | 6 ++++++ articles/polygonclipper/index.md | 4 ++++ articles/polygonclipper/normalization.md | 6 ++++++ 33 files changed, 177 insertions(+), 6 deletions(-) diff --git a/articles/fonts/checkglyphcoverage.md b/articles/fonts/checkglyphcoverage.md index b90fa31b8..1b8d44d19 100644 --- a/articles/fonts/checkglyphcoverage.md +++ b/articles/fonts/checkglyphcoverage.md @@ -39,4 +39,8 @@ This is a simple way to decide whether you need `FallbackFontFamilies` before yo If you want a broader face-level view instead of checking a specific string, use [`Font.FontMetrics.GetAvailableCodePoints()`](xref:SixLabors.Fonts.FontMetrics.GetAvailableCodePoints*). +Glyph coverage is only the first question. A font can contain glyphs for individual code points but still lack the shaping behavior, marks, variation sequences, or color glyph data needed for the text to look right in a real script. Use coverage checks to choose candidate fallback families, then measure or render with the same `TextOptions` you will use in production. + +Emoji and complex scripts are the usual cases where this distinction matters. A visible emoji can be a grapheme made from several code points, and Arabic, Indic, or Southeast Asian scripts can require shaping features that are not captured by a one-code-point probe. + For the conceptual fallback guidance, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). For face-level coverage inspection, see [Font Metrics](fontmetrics.md). diff --git a/articles/fonts/fittexttowidth.md b/articles/fonts/fittexttowidth.md index 105d60458..b228c3112 100644 --- a/articles/fonts/fittexttowidth.md +++ b/articles/fonts/fittexttowidth.md @@ -42,6 +42,10 @@ This is a simple and predictable approach for titles and short labels. If you ne For multiline text, also set `WrappingLength` and measure with the same layout options you plan to render with. +The important rule is that fitting and rendering must use the same layout inputs. Font family, style, size, DPI, culture, wrapping length, fallback fonts, OpenType features, and text direction can all affect measured advance. If any of those differ between the fitting pass and the final drawing pass, the text can still overflow or wrap differently. + +For interactive systems, consider a two-stage search: probe coarse sizes first, then refine around the best candidate. That keeps the recipe easy to adapt without turning every label fit into a long linear measurement loop. + >[!NOTE] >This example is intentionally naive. It remeasures from scratch on each iteration to keep the recipe easy to follow. Production layout engines would usually cache measurements, font instances, or intermediate fit results instead of doing a full linear probe every time. diff --git a/articles/fonts/index.md b/articles/fonts/index.md index 3e0cad97a..7e1f9545c 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -9,6 +9,10 @@ It supports TrueType and OpenType fonts, including CFF1 and CFF2 outlines, WOFF Fonts is often used underneath [ImageSharp.Drawing](../imagesharp.drawing/index.md), but it is not limited to image rendering. You can also use it for font inspection, text measurement, shaping, and custom rendering pipelines. +The main thing to learn early is the difference between font assets, font instances, and text layout. A font collection tells you what families are available, a `Font` chooses a family/style/size, and `TextOptions` describes how a specific piece of text should be shaped, wrapped, aligned, measured, or rendered. `TextBlock` builds on that by preparing layout once so you can inspect lines, hit-test, move carets, or draw the same shaped text consistently. + +The Unicode pages are part of the practical API story, not a side topic. Most real text is not one UTF-16 code unit per visible character, and indexes used for rich text, placeholders, selection, and hit testing need to be understood in terms of graphemes and shaped layout rather than raw `char` positions. + ### License Fonts is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/Fonts/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. diff --git a/articles/fonts/inspectfontfiles.md b/articles/fonts/inspectfontfiles.md index 5b14cfe61..778a8b550 100644 --- a/articles/fonts/inspectfontfiles.md +++ b/articles/fonts/inspectfontfiles.md @@ -19,6 +19,8 @@ string version = description.GetNameById(CultureInfo.InvariantCulture, KnownName This is useful for import tools, font pickers, diagnostics, and file-inspection utilities. +Use invariant names when you need stable storage, configuration, or logs. Use culture-specific names when presenting font choices to people, because many families expose localized names that are more useful in UI than the invariant English metadata. + ### Inspect a font collection such as a `.ttc` ```csharp @@ -36,4 +38,6 @@ foreach (FontDescription description in descriptions.Span) If you do want to load the collection afterward, use [`FontCollection.AddCollection(...)`](xref:SixLabors.Fonts.FontCollection.AddCollection*). +Inspection does not add the font to a collection. That separation is useful for upload validation and tooling: you can reject, categorize, or display font metadata before deciding whether the file should participate in normal font resolution. + For the broader metadata API, see [Font Metadata and Inspection](fontmetadata.md). diff --git a/articles/fonts/listsystemfonts.md b/articles/fonts/listsystemfonts.md index 6b4e51079..529ecf165 100644 --- a/articles/fonts/listsystemfonts.md +++ b/articles/fonts/listsystemfonts.md @@ -2,6 +2,8 @@ This recipe is useful when you want a quick picture of what the current machine can actually provide through [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts), whether for diagnostics, UI pickers, or culture-aware name resolution. +System fonts are environment-dependent. A font that exists on a developer workstation may be missing from a container, CI agent, Linux server, or customer machine. For predictable rendering, ship the fonts you require and load them into a private [`FontCollection`](xref:SixLabors.Fonts.FontCollection). Use `SystemFonts` when the goal is to use what the host operating system already provides. + ### List installed families ```csharp @@ -42,4 +44,6 @@ if (SystemFonts.TryGetByCulture("Yu Gothic", japanese, out FontFamily family)) This is especially useful when a family's localized name differs from the invariant name you would use elsewhere. +Culture-aware lookup is about names, not shaping. After you resolve a family, still use the correct `TextOptions.Culture`, fallback families, and layout settings for the text you are measuring or rendering. + For the fuller system-font API surface, see [System Fonts](systemfonts.md). diff --git a/articles/fonts/recipes.md b/articles/fonts/recipes.md index fc0e8eb23..933c63c8c 100644 --- a/articles/fonts/recipes.md +++ b/articles/fonts/recipes.md @@ -2,10 +2,19 @@ These pages are the quick-start side of the Fonts docs. They are meant for the moment when you know roughly what you want to do and would rather start from a short working example than read the full conceptual guide first. +Each recipe focuses on one common decision: sizing text to fit, choosing fonts, inspecting font assets, enabling OpenType features, or checking glyph coverage before rendering. Use them as starting points, then follow the linked conceptual articles when you need to understand layout coordinates, Unicode behavior, fallback, shaping, or custom rendering in more detail. + - [Fit Text to a Target Width](fittexttowidth.md) - [Inspect Font Files and Collections](inspectfontfiles.md) - [List System Fonts and Resolve by Culture](listsystemfonts.md) - [Use OpenType Features for Numbers and Fractions](useopentypefeatures.md) - [Check Glyph Coverage Before Choosing Fallbacks](checkglyphcoverage.md) +## How to Adapt a Recipe + +- Keep font loading separate from per-text measurement or rendering work when the same font is reused. +- Treat text indexes as grapheme-aware unless a page explicitly discusses code points or UTF-16 units. +- Use `TextOptions` for layout decisions such as origin, wrapping, alignment, DPI, culture, and fallback fonts. +- Use `TextBlock` when you need prepared layout, line inspection, hit testing, caret movement, or selection. + Use the conceptual guides when you need the bigger picture. Use these recipes when you want a practical starting point quickly. diff --git a/articles/fonts/useopentypefeatures.md b/articles/fonts/useopentypefeatures.md index 5bd34d7a4..f12c2c643 100644 --- a/articles/fonts/useopentypefeatures.md +++ b/articles/fonts/useopentypefeatures.md @@ -32,6 +32,8 @@ TextOptions options = new(font) This only has an effect if the font actually provides the requested feature. +Feature requests are not guaranteed substitutions. Fonts decide which features they expose and which scripts, languages, and glyph sequences those features apply to. If output must be exact, test with the production font files rather than assuming a tag will be honored everywhere. + ### Combine multiple features ```csharp @@ -55,4 +57,6 @@ TextOptions options = new(font) Use the same `TextOptions` for both `TextMeasurer` and `TextRenderer` so the measured result matches the rendered result. +OpenType features can change glyph choice, advance widths, ligature formation, and mark placement. That means they are layout inputs, not just visual decoration applied after measuring. + For the fuller feature model, see [OpenType Features](opentypefeatures.md). diff --git a/articles/imagesharp.drawing/annotations.md b/articles/imagesharp.drawing/annotations.md index 031a75f0a..977675f4c 100644 --- a/articles/imagesharp.drawing/annotations.md +++ b/articles/imagesharp.drawing/annotations.md @@ -2,6 +2,8 @@ Annotations are just normal drawing commands layered over an existing image. Use pens for outlines and guides, transparent fills for highlights, and text layout options for labels. +Treat annotation geometry as part of the image coordinate system. That makes the overlay deterministic: the highlight rectangle, guide line, and label origin all describe exact positions on the final image. If you resize the image first, compute the annotation positions after resizing so the callouts still point at the right pixels. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -40,6 +42,8 @@ image.Save("annotated.jpg"); Keep annotation geometry in image coordinates. If you need a local coordinate system for a panel or inset, use `CreateRegion(...)` or a saved transform. +Prefer translucent fills for highlighting because they preserve the source image context. Use an outline pen or text stroke when the annotation must remain readable over both light and dark image regions. + ## Related Topics - [Canvas Drawing](canvas.md) diff --git a/articles/imagesharp.drawing/badge.md b/articles/imagesharp.drawing/badge.md index 690c117cd..6cbbb9a09 100644 --- a/articles/imagesharp.drawing/badge.md +++ b/articles/imagesharp.drawing/badge.md @@ -2,6 +2,8 @@ Small generated badges usually combine a filled shape, an outline, and centered text. Build the shape once, then use the same path for fill and stroke so the border exactly follows the filled area. +This pattern works well for status chips, Open Graph badges, generated labels, and small UI assets. Keep the badge geometry, gradient, and text layout separate: the same path controls fill and stroke, while `RichTextOptions` controls how the label sits inside the shape. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -46,6 +48,8 @@ image.Save("badge.png"); Use a path type that matches the badge geometry you want. [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon), [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon), [`RegularPolygon`](xref:SixLabors.ImageSharp.Drawing.RegularPolygon), [`StarPolygon`](xref:SixLabors.ImageSharp.Drawing.StarPolygon), and custom [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) paths can all be filled and stroked through the same canvas calls. +If the label can vary, set `WrappingLength` smaller than the badge width and use centered alignment. That gives long values room to wrap instead of spilling into the border. + ## Related Topics - [Primitive Drawing Helpers](primitives.md) diff --git a/articles/imagesharp.drawing/clipimagetoshape.md b/articles/imagesharp.drawing/clipimagetoshape.md index 47198ba89..b36673e5c 100644 --- a/articles/imagesharp.drawing/clipimagetoshape.md +++ b/articles/imagesharp.drawing/clipimagetoshape.md @@ -2,6 +2,8 @@ Use `Save(DrawingOptions, params IPath[])` with `BooleanOperation.Intersection` when later drawing should be limited to a shape. This is useful for avatars, shaped thumbnails, masked hero images, and photo badges. +The important idea is that clipping is canvas state. Once saved, the clip applies to every later command until `Restore()` is called. Draw the clipped image while that state is active, then restore before drawing borders, labels, shadows, or other elements that should sit outside the mask. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -42,6 +44,8 @@ image.Save("avatar.png"); Keep the source image alive until the drawing operation has replayed. The `Paint(...)` pipeline handles the canvas lifetime for this example. +Use a destination rectangle that matches the visible shape bounds when you want predictable cropping. Use a larger destination rectangle when the source image should intentionally bleed beyond the shape, for example to zoom into a face inside an avatar. + ## Related Topics - [Clipping, Regions, and Layers](clippingregionslayers.md) diff --git a/articles/imagesharp.drawing/clippingregionslayers.md b/articles/imagesharp.drawing/clippingregionslayers.md index 0dfc39a7b..2dda2c93f 100644 --- a/articles/imagesharp.drawing/clippingregionslayers.md +++ b/articles/imagesharp.drawing/clippingregionslayers.md @@ -2,6 +2,8 @@ Canvas state controls where later commands can draw and how grouped commands are composed. The three main tools are `Save(...)` with clip paths, `CreateRegion(...)`, and `SaveLayer(...)`. +Use a clip when later commands should be constrained by vector geometry. Use a region when you want a rectangular child layout with local coordinates. Use a layer when several commands should be rendered together first, then blended or composited back as one result. + ## Clip Later Commands `Save(DrawingOptions, params IPath[])` pushes a new state with the supplied options and clip paths. The clip paths are combined with each command by `ShapeOptions.BooleanOperation`. diff --git a/articles/imagesharp.drawing/imagesandprocessing.md b/articles/imagesharp.drawing/imagesandprocessing.md index 41a0b0633..dbe367040 100644 --- a/articles/imagesharp.drawing/imagesandprocessing.md +++ b/articles/imagesharp.drawing/imagesandprocessing.md @@ -2,6 +2,8 @@ ImageSharp.Drawing can draw images through the canvas, use images as brushes, and run ImageSharp processors inside drawing regions. +Use `DrawImage(...)` when you want to place a rectangular source image into a rectangular destination. Use an image brush when the image should behave like a fill for arbitrary geometry. Use `Apply(...)` when you need normal ImageSharp processors to affect only the pixels covered by a rectangle or path. + ## Draw an Image `DrawImage(...)` copies a source rectangle from an image into a destination rectangle on the canvas. The destination is affected by the current transform and clip state. @@ -40,6 +42,8 @@ image.Mutate(ctx => ctx.Paint(canvas => Keep the source image alive until the canvas has replayed the command. With `Paint(...)`, that means the source must remain alive until `Mutate(...)` completes. +Choose the source rectangle in source-image coordinates and the destination rectangle in canvas coordinates. That separation is useful when you want to crop from a large source image while placing the selected pixels into a fixed layout region. + ## Use an Image as a Brush Use `ImageBrush` when an image should fill any path as a texture. This is different from `DrawImage(...)`: the brush samples image pixels while the supplied path controls coverage. @@ -60,13 +64,14 @@ ImageBrush brush = new(source, sourceRegion, new Point(-120, -70)); image.Mutate(ctx => ctx.Paint(canvas => { - // The star path controls coverage; the brush supplies the sampled image pixels. canvas.Fill(brush, star); canvas.Draw(Pens.Solid(Color.DarkSlateGray, 3), star); })); ``` +An image brush is best when the same texture should fill a shape, text path, or repeated decorative element. For one-off rectangular placement, `DrawImage(...)` is usually easier to reason about. + ## Apply Processors Inside a Shape `Apply(...)` runs normal ImageSharp processors inside a rectangle, path, or path builder. It is a replay barrier: commands before it affect the pixels being processed, and commands after it do not. @@ -95,3 +100,5 @@ image.Mutate(ctx => ctx.Paint(canvas => ``` On GPU-backed canvases, `Apply(...)` requires the affected pixels to be read back, processed by the CPU pipeline, and written back before presentation. Keep regions as small as the effect allows. + +The placement of `Apply(...)` matters. Commands recorded before it contribute pixels to the processor input; commands recorded after it are drawn over the processed result. This makes it possible to blur or pixelate an image region, then draw a crisp outline or label on top. diff --git a/articles/imagesharp.drawing/index.md b/articles/imagesharp.drawing/index.md index 5fcc5dbbe..be2cb5c12 100644 --- a/articles/imagesharp.drawing/index.md +++ b/articles/imagesharp.drawing/index.md @@ -5,6 +5,10 @@ ImageSharp.Drawing is a library built on top of ImageSharp to provide 2D drawing ImageSharp.Drawing is designed from the ground up to be high-performance, flexible, and extensible. It provides vector geometry, brush and pen styling, canvas drawing, image compositing, and text rendering building blocks for custom images. +The core model is deliberately small: geometry describes coverage, brushes and pens describe how pixels are produced, drawing options describe state, and [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) records commands that are replayed into an ImageSharp processing pipeline. That makes the same drawing code useful for one-off image generation, templated graphics, server-side rendering, retained canvas scenes, and GPU-backed output. + +Read the articles as a progression. Start with the canvas workflow, learn the geometry and styling types, then move into text, image composition, transforms, and WebGPU when the job needs them. + ### Start Here - [Getting Started](gettingstarted.md) introduces the [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) and [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) workflow. diff --git a/articles/imagesharp.drawing/primitives.md b/articles/imagesharp.drawing/primitives.md index 222435054..349137a6b 100644 --- a/articles/imagesharp.drawing/primitives.md +++ b/articles/imagesharp.drawing/primitives.md @@ -4,8 +4,12 @@ Primitive helpers are convenience methods on [`DrawingCanvas`](xref:SixLabors.Im The helpers still follow the same rules as path drawing: fills use brushes, strokes use pens, [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls antialiasing and transforms, and active canvas state applies to the recorded command. +Primitive calls record a drawing command immediately. They are a good fit for marks, guides, simple badges, outlines, and other geometry that is only used once. If the same geometry must be filled, stroked, clipped, transformed, measured, or shared between commands, create a path or polygon object instead. + ## Rectangles, Ellipses, Lines, and Beziers +Rectangle helpers use a top-left coordinate plus width and height. Ellipse helpers use a center point plus size, matching [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon). Lines and Beziers use explicit points in canvas coordinates, so they are easy to combine with image-space measurements. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; @@ -37,10 +41,14 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` +Use the rectangle and ellipse helpers when the geometry exists only for that command. For example, `DrawEllipse(...)` is concise for a one-off ring, while `new EllipsePolygon(...)` is better when the same ellipse must be clipped, filled, and outlined. + ## Arcs and Pies Arc and pie helpers take a center point, a size, a rotation angle, a start angle, and a sweep angle. Positive and negative sweeps are both valid, which makes clockwise and counter-clockwise segments easy to express. +Arc helpers draw or fill the curved segment of an ellipse. Pie helpers close the segment back to the center, creating a wedge. Use [`PiePolygon`](xref:SixLabors.ImageSharp.Drawing.PiePolygon) when the wedge is part of reusable geometry. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; diff --git a/articles/imagesharp.drawing/recipes.md b/articles/imagesharp.drawing/recipes.md index 591afd7e8..e7f84e922 100644 --- a/articles/imagesharp.drawing/recipes.md +++ b/articles/imagesharp.drawing/recipes.md @@ -2,6 +2,8 @@ These pages are the quick-start side of the ImageSharp.Drawing docs. They focus on practical drawing tasks that combine canvas commands, brushes, pens, images, text, clipping, and processors. +Each recipe is intentionally complete enough to show the shape of a real workflow: create or load an image, set up reusable drawing objects outside the canvas callback, record the drawing commands, then save the result. After using a recipe, follow the related conceptual pages to understand the canvas state, lifetime, and composition behavior behind it. + ## Common Tasks - [Add a Text Watermark](watermark.md) for anchored, semi-transparent text over an image. @@ -10,6 +12,13 @@ These pages are the quick-start side of the ImageSharp.Drawing docs. They focus - [Add Callouts and Annotations](annotations.md) for overlays, markers, outlines, and dashed guides. - [Create a Soft Shadow](softshadow.md) for shadowed panels and grouped drawing effects. +## How to Adapt a Recipe + +- Keep images, brushes, pens, fonts, and paths alive until the `Paint(...)` operation has completed. +- Create reusable geometry before the callback when more than one command needs the same shape. +- Use `Save(...)` and `Restore()` when a clip, transform, or drawing option should affect only part of the recipe. +- Put `Apply(...)` after the drawing commands that should be processed and before any crisp outlines or labels. + ## Related Topics - [Canvas Drawing](canvas.md) diff --git a/articles/imagesharp.drawing/softshadow.md b/articles/imagesharp.drawing/softshadow.md index f7c8efbfc..c2f18d4e3 100644 --- a/articles/imagesharp.drawing/softshadow.md +++ b/articles/imagesharp.drawing/softshadow.md @@ -2,6 +2,8 @@ Draw the shadow shape first, then apply a blur to the shadow region before drawing the foreground object. `Apply(...)` is a replay barrier, so only commands recorded before the barrier are processed. +The blur region should be larger than the original shadow shape. Gaussian blur spreads pixels outward, so using the exact shape bounds clips the soft edge. A good rule of thumb is to expand the processing rectangle by at least the blur radius on every side. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -11,15 +13,17 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 240, Color.White.ToPixel()); -Rectangle shadowBounds = new(70, 72, 280, 110); +Rectangle shadowOffsetBounds = new(70, 72, 280, 110); +Rectangle blurBounds = new(60, 62, 300, 130); Rectangle panelBounds = new(62, 58, 280, 110); image.Mutate(ctx => ctx.Paint(canvas => { - canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.35F)), shadowBounds); + // The shadow is intentionally offset from the panel before the blur spreads it outward. + canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.35F)), shadowOffsetBounds); - // Apply seals earlier drawing commands before the blur is replayed. - canvas.Apply(shadowBounds, region => region.GaussianBlur(10)); + // Apply seals earlier drawing commands before the blur is replayed, and the expanded region preserves the feathered edge. + canvas.Apply(blurBounds, region => region.GaussianBlur(10)); canvas.Fill(Brushes.Solid(Color.White), panelBounds); canvas.Draw(Pens.Solid(Color.LightGray, 1), panelBounds); @@ -30,6 +34,8 @@ image.Save("shadow.png"); Keep the blur region tight. On CPU canvases this reduces the amount of image data processed, and on GPU-backed canvases it reduces readback and upload work. +Use `SaveLayer(...)` instead when the foreground and shadow need to be composed as one group over existing content. Use `Apply(...)` when you want a normal ImageSharp processor, such as blur, pixelation, or color adjustment, to affect only part of the drawing timeline. + ## Related Topics - [Canvas Drawing](canvas.md) diff --git a/articles/imagesharp.drawing/transformsandcomposition.md b/articles/imagesharp.drawing/transformsandcomposition.md index 492f72f82..42b49c402 100644 --- a/articles/imagesharp.drawing/transformsandcomposition.md +++ b/articles/imagesharp.drawing/transformsandcomposition.md @@ -2,6 +2,8 @@ [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) carries the transform, graphics options, and shape options used by canvas commands. Use it when drawing state should change for a group of operations. +The safest way to think about transforms and composition is as scoped canvas state. Save the options that should affect a group, draw the affected commands, then restore the previous state before drawing labels, guides, or other unaffected output. + ## Transform Drawing `DrawingOptions.Transform` is applied to vector output before rasterization. For strokes, the path is stroked in local geometry space and the generated outline is transformed for drawing. @@ -16,6 +18,8 @@ For normal drawing, construct the value from `Matrix3x2`. That keeps rotation, s Matrix4x4 transform = new(Matrix3x2.CreateRotation(angle, center)); ``` +When more than one 2D operation is needed, compose the `Matrix3x2` expression first and wrap the final result in `Matrix4x4`. Keeping the 2D operations together makes order explicit and avoids hand-written matrix values for ordinary scale, rotate, skew, and translate cases. + Use the full `Matrix4x4` form when you need transforms that cannot be expressed by `Matrix3x2`, such as perspective-style projection. The canvas, path, text, brush, image, and WebGPU paths all carry the same transform type, so code can move between CPU drawing, retained scenes, and GPU rendering without changing the public drawing model. ```csharp @@ -109,7 +113,6 @@ DrawingOptions aliased = new() image.Mutate(ctx => ctx.Paint(aliased, canvas => { - // With antialiasing disabled, integer rectangle corners render as full covered pixels. canvas.Fill(Brushes.Solid(Color.White), new Rectangle(10, 10, 44, 28)); })); diff --git a/articles/imagesharp.drawing/watermark.md b/articles/imagesharp.drawing/watermark.md index 931b5befe..5d5f0cec3 100644 --- a/articles/imagesharp.drawing/watermark.md +++ b/articles/imagesharp.drawing/watermark.md @@ -2,6 +2,8 @@ Use `DrawText(...)` with alignment options when a watermark should stay anchored to an image edge. The text layout options keep the placement declarative, so you do not need to measure the string manually. +Anchor the watermark by choosing an origin near the desired edge, then set horizontal and vertical alignment relative to that origin. This keeps the code stable when the watermark text changes length or the image size changes. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -35,6 +37,8 @@ image.Save("watermarked.jpg"); Use a subtle fill alpha and a darker outline when the watermark must remain readable over mixed image content. +For repeated export workflows, create the font and text options once per image size, then draw inside the `Paint(...)` callback. Use wrapping when the watermark can contain user or tenant names that may be longer than expected. + ## Related Topics - [Drawing Text](text.md) diff --git a/articles/imagesharp.web/gettingstarted.md b/articles/imagesharp.web/gettingstarted.md index d1835f224..3433c9a12 100644 --- a/articles/imagesharp.web/gettingstarted.md +++ b/articles/imagesharp.web/gettingstarted.md @@ -21,6 +21,8 @@ app.Run(); `app.UseImageSharp()` must appear before `app.UseStaticFiles()`. If static files run first, requests such as `/images/photo.jpg` or `/images/photo.jpg?width=400` will be served directly from disk and ImageSharp.Web will never see them. +Treat the middleware order as part of the image contract for your application. Anything registered before ImageSharp.Web can short-circuit the request before image processing happens. Anything registered after it will normally see the processed response only when ImageSharp.Web chooses not to handle the request. + ## What the Default Registration Includes `AddImageSharp()` wires up the core middleware services plus a sensible default pipeline: @@ -90,6 +92,8 @@ builder.Services.AddImageSharp(options => Relative paths are resolved against the application content root. If your app does not define a web root, set both `ProviderRootPath` and `CacheRootPath` explicitly. +Keep source storage and cache storage conceptually separate. The provider root is where original images come from. The cache root is disposable derived output and can usually be cleared, rebuilt, or moved to cheaper storage without losing source assets. In clustered deployments, choose cache storage that matches your invalidation and sharing requirements. + ## Next Steps - [Configuration and Pipeline](configuration.md) diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 1f3c6db84..5537a9f19 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -4,6 +4,10 @@ ImageSharp.Web is Six Labors' high-performance ASP.NET Core image middleware for The current package targets .NET 8 and is built on top of [ImageSharp](../imagesharp/index.md). The middleware is intentionally modular: you can change how commands are parsed, where source images come from, how cache keys are built, where processed images are stored, and whether image requests must be signed. +The practical model is a web request pipeline. A provider resolves the original image, a parser turns the request into commands, processors transform the image, an encoder writes the response, and a cache stores the result so the next matching request can avoid the expensive work. Most configuration choices are about one of those stages. + +Use ImageSharp.Web when image variants are determined by HTTP requests: responsive thumbnails, CDN-backed transformations, signed URLs, tenant-specific providers, or cached format conversion. Use core ImageSharp directly when processing is an offline job, queue worker, or application workflow that is not naturally request-driven. + ## License ImageSharp.Web is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Web/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. diff --git a/articles/imagesharp.web/processingcommands.md b/articles/imagesharp.web/processingcommands.md index 73c5b05e9..04680e377 100644 --- a/articles/imagesharp.web/processingcommands.md +++ b/articles/imagesharp.web/processingcommands.md @@ -12,6 +12,10 @@ The default [`QueryCollectionRequestParser`](xref:SixLabors.ImageSharp.Web.Comma - Processors run in the order their first recognized command appears in the request, not in a hard-coded global order. - Values are parsed with invariant culture by default. If you turn that off, parsing follows `CultureInfo.CurrentCulture`. +That ordering rule is important when you add custom processors. A processor should declare the command keys that activate it, and callers should build URLs in the order they want the pipeline to run. For example, resizing before a custom watermark processor is not the same as watermarking before resizing. + +Command parsing and HMAC validation are also connected. Unknown commands are removed before the final command collection is validated and executed, so a signed URL only protects the command surface the application actually recognizes. If you need a locked-down public API, combine HMAC with presets or a custom parser that exposes only the transformations you want users to request. + ## Resize Resize commands are handled by [`ResizeWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.ResizeWebProcessor) and map to [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions). @@ -35,6 +39,8 @@ Resize commands are handled by [`ResizeWebProcessor`](xref:SixLabors.ImageSharp. `orient` is easy to confuse with `autoorient`. The short version is that `orient` only changes resize math, while `autoorient` actually rotates or flips the decoded image. +For responsive-image URLs, prefer specifying only the dimension that is actually constrained by layout. Use both `width` and `height` only when the output must occupy an exact box, then choose the `rmode` that matches the design: `max` to fit inside, `crop` to fill, `pad` to preserve everything with extra canvas, and `stretch` only when distortion is acceptable. + ## Auto-Orient [`AutoOrientWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.AutoOrientWebProcessor) applies EXIF orientation to the decoded image before later processors run. @@ -95,6 +101,8 @@ This is most useful when flattening transparent images before converting them to /images/logo.png?bgcolor=white&format=jpg&quality=85 ``` +If `bgcolor` is omitted and the output format cannot represent alpha, transparent pixels must still be resolved by the encoder or format behavior. Set the background color explicitly when brand colors, UI previews, or predictable JPEG output matter. + ## Related Topics - [Configuration and Pipeline](configuration.md) diff --git a/articles/imagesharp/cropandcanvas.md b/articles/imagesharp/cropandcanvas.md index b8a2d4c56..78c8ec9da 100644 --- a/articles/imagesharp/cropandcanvas.md +++ b/articles/imagesharp/cropandcanvas.md @@ -4,6 +4,8 @@ Cropping and canvas operations are closely related, but they solve different pro Thinking about those as separate questions makes the API much easier to navigate. +Coordinate choices matter here. Crop rectangles describe source pixels you want to keep. Padding and canvas-style operations describe the destination bounds you want after the operation. When a workflow feels confusing, write those two rectangles down separately: source region first, output canvas second. + ## Crop to an Explicit Rectangle Use `Crop()` when you know the exact rectangle you want to keep: @@ -19,6 +21,8 @@ image.Mutate(x => x.Crop(new Rectangle(100, 80, 1200, 800))); This removes everything outside the requested bounds. +The crop rectangle is expressed in the image's current coordinate space. If the source may contain EXIF orientation, call `AutoOrient()` before choosing crop coordinates that should match what a person sees. + ## Crop by Width and Height If the crop should start at the top-left corner, you can pass just width and height: @@ -47,6 +51,8 @@ image.Mutate(x => x.Pad(1200, 1200, Color.White)); This is useful when generating square thumbnails, social cards, or export assets that require a fixed output size. +Padding does not scale the original image. If the image must fit inside a larger box with a background, resize to the intended content size first, then pad to the final canvas size. + ## Fill Transparent Areas or Flatten Onto a Background Use `BackgroundColor()` to fill transparent pixels or composite the current image over a solid color: @@ -77,6 +83,8 @@ image.Mutate(x => x.EntropyCrop()); This can be useful for removing large flat borders or whitespace-like areas before additional processing. +Automatic cropping is content-driven, so treat it as a convenience rather than a layout contract. It is useful for cleanup workflows, but explicit rectangles or resize anchors are better when output dimensions must be predictable. + ## Combine Crop and Resize Cropping and resizing are often used together: diff --git a/articles/imagesharp/formatconversion.md b/articles/imagesharp/formatconversion.md index 55241932b..fc8498799 100644 --- a/articles/imagesharp/formatconversion.md +++ b/articles/imagesharp/formatconversion.md @@ -45,6 +45,8 @@ image.Save("output.jpg", new JpegEncoder }); ``` +Choose the flattening color deliberately. White is common for documents and many web layouts, but logos, UI assets, and product imagery may need a brand color, a page background color, or a checkerboard-style review workflow before final export. + ## Convert JPEG to WebP Use a WebP encoder when you want to move a photographic source to a more modern delivery format: @@ -62,6 +64,8 @@ image.Save("output.webp", new WebpEncoder }); ``` +For web delivery, compare both file size and visual quality against your JPEG baseline. WebP often wins for photographic content, but the right quality value is product-specific and should be chosen against representative images rather than one sample. + ## Convert Any Input to PNG PNG is a good target when you want lossless output or transparency support: @@ -75,6 +79,8 @@ using Image image = Image.Load("input.bin"); image.Save("output.png", new PngEncoder()); ``` +PNG is not automatically the best "safe" target for every input. It preserves sharp graphics and transparency well, but photographic sources can become much larger than JPEG or WebP. Use PNG when lossless output, alpha, indexed color, or broad compatibility matter more than smallest file size. + ## Choose the Output Based on Pixel Info When you want format conversion to respect the source characteristics, inspect the encoded pixel type first and then choose the encoder accordingly: @@ -114,5 +120,6 @@ else - Converting a transparent image to JPEG requires flattening or compositing first. - ImageSharp uses bridged metadata and pixel-type information to pick good destination settings when the target format can represent them. - If you care about exact output tradeoffs, use an explicit encoder rather than relying only on the file extension. +- Format conversion is also a metadata decision. Decide whether orientation, color profiles, animation timing, and authoring metadata should be preserved, transformed, or stripped. For more on format behavior and encoder options, see [Image Formats](imageformats.md). For more on inspecting pixel types before a conversion, see [Read Image Info Without Decoding](identify.md) and [Pixel Formats](pixelformats.md). diff --git a/articles/imagesharp/identify.md b/articles/imagesharp/identify.md index 33c4c934c..b4b4c830d 100644 --- a/articles/imagesharp/identify.md +++ b/articles/imagesharp/identify.md @@ -74,6 +74,8 @@ Console.WriteLine(format.Name); This is useful when file extensions are missing or untrustworthy. +Use `DetectFormat()` when routing depends only on the encoded format. Use `Identify()` when you need dimensions, frame count, pixel type, or metadata-driven decisions. `DetectFormat()` answers a narrower question and does less work. + ## Use Async APIs For asynchronous workflows, use `IdentifyAsync()`: @@ -94,5 +96,6 @@ Console.WriteLine(imageInfo.Height); - `ImageInfo.PixelType` includes color model, alpha behavior, bit depth, and component precision without decoding the full image. - `ImageInfo.GetPixelMemorySize()` estimates decoded pixel memory before you commit to a full load. - `Image.DetectFormat()` is focused on encoded format detection, while `Image.Identify()` returns the broader inspection result. +- Identification is not a replacement for decode-time error handling. It is a cheap preflight step; malformed input can still fail later when pixels are decoded. For more detail, see [Loading, Identifying, and Saving](loadingandsaving.md), [Working with Metadata](metadata.md), [Convert Between Formats](formatconversion.md), and [Pixel Formats](pixelformats.md). diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index 9567628df..1ae36295c 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -4,6 +4,10 @@ ImageSharp is the high-performance part of the Six Labors stack you reach for wh This section is written as a guided set of articles rather than a flat feature list. Start with [Getting Started](gettingstarted.md) if you are new to the library, then branch into loading, processing, formats, or lower-level pixel work as your needs get more specific. +The core model is: choose an image type, load or create pixels, run ordered processing operations, then save with an explicit encoder when output behavior matters. `Image` is convenient when you do not need direct pixel access, while `Image` makes the in-memory pixel format part of the type so high-performance row processing and format-specific work stay explicit. + +For production code, the important choices are usually not individual method names. They are whether to identify before decoding, which pixel format to use in memory, how much metadata to preserve, which encoder settings define acceptable output, and when to customize configuration for formats, memory, or security. + ## License ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. diff --git a/articles/imagesharp/pixelformats.md b/articles/imagesharp/pixelformats.md index bf09699c4..69e11a279 100644 --- a/articles/imagesharp/pixelformats.md +++ b/articles/imagesharp/pixelformats.md @@ -39,6 +39,10 @@ Choose a `TPixel` based on the kind of in-memory work you need to do: - Use lower-memory formats such as [`Rgb24`](xref:SixLabors.ImageSharp.PixelFormats.Rgb24) or [`L8`](xref:SixLabors.ImageSharp.PixelFormats.L8) when you know you do not need the extra channels or precision. - Use higher-precision formats such as [`Rgb48`](xref:SixLabors.ImageSharp.PixelFormats.Rgb48), [`Rgba64`](xref:SixLabors.ImageSharp.PixelFormats.Rgba64), or [`RgbaVector`](xref:SixLabors.ImageSharp.PixelFormats.RgbaVector) when your pipeline benefits from more precision. +For application code, a good default rule is to decode into the format you plan to process in, not necessarily the format the file used on disk. A JPEG may decode naturally into RGB-like data, but `Image` can still be the right working type if the next step composites with alpha, draws overlays, or passes pixels to an API that expects RGBA. Conversely, `Image` can be the right type for masks, analysis, and grayscale-only processing where carrying color channels would just waste memory. + +Do not use a higher-precision format only because the source file is high quality. Use it when the operations you run benefit from the extra range or precision, such as repeated color transforms, scientific-style image data, high-bit-depth exports, or workflows where banding from 8-bit intermediate values would be visible. + If you want to inspect pixel characteristics before a full decode, [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) exposes [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo). See [Read Image Info Without Decoding](identify.md) for more on that workflow. ## Defining Custom Pixel Formats diff --git a/articles/imagesharp/processing.md b/articles/imagesharp/processing.md index 0548479fb..f1a89436f 100644 --- a/articles/imagesharp/processing.md +++ b/articles/imagesharp/processing.md @@ -7,6 +7,8 @@ The main entry points are [`Mutate`](xref:SixLabors.ImageSharp.Processing.Proces - `Mutate()` applies processors to the current image. - `Clone()` creates a deep copy and applies the processors to that copy. +Processors are deliberately composable. Each call in the pipeline receives the result of the previous call, so the code order is also the image-processing order. That makes pipelines easy to read, but it also means a misplaced operation can change the result significantly. + ## Mutate the Current Image Use `Mutate()` when you want to transform the current image in place: @@ -27,6 +29,8 @@ image.Save("output.jpg"); This is the most common choice for request processing, thumbnails, and one-way export workflows. +Use `Mutate()` when the loaded image is an intermediate value and there is no need to keep the original pixels. This keeps ownership simple and avoids a second full image allocation. + ## Clone When You Need to Preserve the Original Use `Clone()` when the original image must remain unchanged: @@ -45,6 +49,8 @@ thumbnail.Save("thumbnail.jpg"); This is useful when you need multiple derived outputs from the same source image. +Use `Clone()` when the original image is a reusable source asset: for example, generating several thumbnail sizes, producing multiple export formats, or running a preview operation while keeping an editable original. + ## Build Ordered Pipelines Processor order matters. For example, auto-orienting before resizing usually produces more predictable results than resizing first and correcting orientation later: @@ -67,6 +73,7 @@ As a rule of thumb: - Normalize orientation early. - Crop before expensive down-stream work when the crop meaningfully reduces the pixel area. - Apply output-specific effects near the end of the pipeline. +- Save with an explicit encoder when output quality, metadata, compression, or compatibility matters. ## Common Processing Topics diff --git a/articles/imagesharp/recipes.md b/articles/imagesharp/recipes.md index 26b9ca8e1..464e499da 100644 --- a/articles/imagesharp/recipes.md +++ b/articles/imagesharp/recipes.md @@ -9,6 +9,13 @@ These pages are the fast path through the ImageSharp docs. They skip most of the - [Strip Metadata](stripmetadata.md) for removing EXIF, ICC, IPTC, XMP, and related metadata before export. - [Read Image Info Without Decoding](identify.md) for dimensions, frame count, pixel info, and format detection without a full decode. +## How to Adapt a Recipe + +- Use `Identify(...)` before decoding when routing decisions only need dimensions, metadata, or format detection. +- Use `Mutate(...)` when changing an existing image and `Clone(...)` when the original image must be preserved. +- Choose encoder options deliberately when file size, quality, metadata retention, or color profile behavior matters. +- Keep stream ownership clear: load from streams that stay readable for the load call, then save to streams you control. + ## Related Topics - [Loading, Identifying, and Saving](loadingandsaving.md) diff --git a/articles/imagesharp/stripmetadata.md b/articles/imagesharp/stripmetadata.md index 14377240a..bcd79e59d 100644 --- a/articles/imagesharp/stripmetadata.md +++ b/articles/imagesharp/stripmetadata.md @@ -2,6 +2,8 @@ Removing metadata is usually about one of three goals: smaller files, less personal information, or a cleaner normalized export. ImageSharp makes that straightforward, but it helps to be clear about whether you want the encoder to skip metadata on write or whether you want to clear profiles in memory first. +The choice matters because metadata is not one thing. EXIF can contain camera settings, timestamps, thumbnails, orientation, and GPS data. ICC and CICP data affect color interpretation. XMP and IPTC often contain authoring, rights, caption, and workflow information. Stripping everything is correct for privacy-sensitive exports, but not always correct for archival, print, or color-managed workflows. + ## Strip Metadata with the Encoder The simplest approach, when you control the output encoder, is to set [`ImageEncoder.SkipMetadata`](xref:SixLabors.ImageSharp.Formats.ImageEncoder.SkipMetadata) to `true`: @@ -46,5 +48,7 @@ This approach is useful when you want to inspect or edit metadata before decidin - `SkipMetadata = true` is usually the easiest option when you are already choosing an explicit encoder. - Manual profile clearing gives you more control over which metadata survives. - Saving to a different format can also change which metadata can be represented in the output. +- If color fidelity matters, think carefully before removing ICC or CICP metadata. Convert to an intended working/output color space first when the source profile is meaningful. +- If orientation matters, call `AutoOrient()` before stripping EXIF orientation metadata so the pixels are physically normalized. For more detail, see [Working with Metadata](metadata.md). diff --git a/articles/imagesharp/thumbnails.md b/articles/imagesharp/thumbnails.md index 1ad3d493e..1dc3234c8 100644 --- a/articles/imagesharp/thumbnails.md +++ b/articles/imagesharp/thumbnails.md @@ -7,6 +7,8 @@ The usual patterns are: - fit the image within a bounding box while preserving aspect ratio, and - create a fixed-size thumbnail that fills the target area by cropping. +Before choosing between them, decide what the thumbnail promises to users. A catalog image often needs to show the whole object, so fit-within-box is safer. Avatars, cards, and masonry layouts usually need consistent dimensions, so crop-to-fill is a better match. That product decision should drive the resize mode rather than the other way around. + ## Fit Within a Bounding Box Use [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) with [`ResizeMode.Max`](xref:SixLabors.ImageSharp.Processing.ResizeMode) when you want the full image to fit inside a target box: @@ -31,6 +33,8 @@ image.Save("thumbnail.jpg", new JpegEncoder { Quality = 85 }); This keeps the whole image visible and preserves aspect ratio. +The output may be smaller than the requested box in one dimension. That is the point of `ResizeMode.Max`: it respects both the maximum bounds and the source aspect ratio. If your downstream layout requires an exact canvas size, resize first and then pad onto a fixed background. + ## Create a Square Center-Crop Thumbnail Use [`ResizeMode.Crop`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Crop) to fill the target bounds and crop the overflow: @@ -55,6 +59,8 @@ image.Save("avatar.jpg"); This is the usual pattern for avatars, cards, and tile-based UI. +For user-generated photos, consider exposing a focal point or crop anchor instead of always using the center. Faces, products, and text are not always centered in the source image. + ## Keep Transparency in Thumbnails If the source image uses transparency and you want to preserve it, save the thumbnail to a format that supports alpha, such as PNG or WebP: @@ -80,5 +86,6 @@ image.Save("thumbnail.png", new PngEncoder()); - `AutoOrient()` is usually the right first step for user-uploaded photos. - `ResizeMode.Max` is for fit-within-box results. - `ResizeMode.Crop` is for fixed output dimensions that must be fully filled. +- Use explicit encoders when thumbnail quality, metadata, color profile behavior, or file size needs to be predictable. For more detail on resizing behavior, see [Resizing Images](resize.md). diff --git a/articles/polygonclipper/gettingstarted.md b/articles/polygonclipper/gettingstarted.md index 1e313326c..0de0c6343 100644 --- a/articles/polygonclipper/gettingstarted.md +++ b/articles/polygonclipper/gettingstarted.md @@ -8,6 +8,8 @@ The fastest way to get comfortable with PolygonClipper is to think in terms of t From there, most applications either run a boolean operation with [`PolygonClipper`](xref:SixLabors.PolygonClipper.PolygonClipper) or generate stroke-outline geometry with [`PolygonStroker`](xref:SixLabors.PolygonClipper.PolygonStroker). +PolygonClipper does not attach units or coordinate-system meaning to vertices. Your application decides whether a vertex represents pixels, points, millimeters, tiles, or world coordinates. The important part is to use one consistent coordinate space for all inputs to a single operation. + ## Build Two Input Polygons This example creates two rectangles, then intersects them: @@ -40,6 +42,8 @@ Console.WriteLine($"Vertices: {result.VertexCount}"); You do not need to repeat the first vertex at the end of a contour for normal polygon operations. Contours are treated as implicitly closed. +The example builds contours clockwise, but real inputs often arrive from drawing tools, path importers, or GIS-style data with mixed orientation. Use [Normalization and Winding](normalization.md) when you need to turn messy single-polygon input into clean positive-winding output before a downstream system consumes it. + ## Prefer the Static Entry Points Most applications should call the static methods: @@ -68,3 +72,5 @@ for (int i = 0; i < result.Count; i++) ``` That contour hierarchy is one of the main things PolygonClipper preserves for you. If you want to understand how parent contours, holes, and winding fit together, the next page to read is [Polygons, Contours, and Holes](polygonsandcontours.md). + +Do not assume that one operation returns one contour. Intersections can split a region into multiple islands, differences can create holes, and normalization can reorganize self-intersecting input. Production callers should usually iterate the returned polygon rather than indexing directly into the first contour. diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index 48bfebbeb..9704f930a 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -6,6 +6,10 @@ The current package targets [.NET 8](https://learn.microsoft.com/en-us/dotnet/co Under the hood, the boolean-operation pipeline is based on a Martinez-Rueda sweep-line approach for complex polygon clipping, while normalization uses a separate Vatti/Clipper2-inspired cleanup path for resolving self-intersections and overlaps into positive-winding output. You do not need to understand those algorithms to use the library well, but it helps explain why PolygonClipper is comfortable with complex contour topology. +The library works with geometry, not pixels. Coordinates are numeric vertices, contours are rings, and polygons are collections of rings with explicit hierarchy for holes. That makes PolygonClipper useful before rendering, export, hit testing, path cleanup, or any workflow where you need the region itself rather than a raster mask. + +Most users should begin with the static boolean, normalization, and stroking entry points. They take ordinary polygon inputs and return new polygon geometry, so the result can be inspected, transformed, rendered, serialized, or handed to another geometry pipeline. + ### License PolygonClipper is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. diff --git a/articles/polygonclipper/normalization.md b/articles/polygonclipper/normalization.md index 1d585c09a..e5b97806a 100644 --- a/articles/polygonclipper/normalization.md +++ b/articles/polygonclipper/normalization.md @@ -4,6 +4,8 @@ Boolean operations combine two polygons. Normalization is different: it cleans u That makes [`PolygonClipper.Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) the right tool when your input geometry is already yours, but its contours are messy enough that you want a cleaner region description before export, rendering, or further processing. +Think of normalization as converting drawn or imported edges into filled-region geometry. It is not a visual simplifier and it is not a general-purpose path optimizer. The result describes the same filled area using contours that downstream geometry code can reason about more consistently. + ## When to Use [`Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) Normalization is useful when: @@ -32,12 +34,16 @@ Polygon normalized = PolygonClipper.Normalize(input); The output may have a different contour count and different contour hierarchy than the input. That is expected. Normalization is free to split or reorganize the input region as needed to produce clean positive-winding output. +That also means normalization should happen at a clear boundary in your pipeline. Normalize imported or user-authored geometry before you cache, export, or combine it with other trusted geometry. Avoid normalizing repeatedly after every small edit unless your application specifically needs canonical output at each step. + ## Positive Winding Matters The source describes normalization in terms of positive fill semantics. In practice, that means the result is intended for consumers that care about winding-consistent filled regions rather than raw overlapping edges. This is especially useful when you are moving polygon data into a renderer, exporter, or geometry pipeline that expects contours to describe filled regions cleanly. +Positive winding does not mean every original contour keeps its original orientation. It means the returned polygon is organized around positive filled-region semantics after overlaps and self-intersections have been resolved. + ## Implementation Note Normalization is a separate pipeline from the two-input boolean operations. In PolygonClipper it follows a Vatti/Clipper2-inspired approach focused on turning overlapping or self-intersecting contour input into a canonical positive-winding result. From 627177d0fb11ba7693c80e124061bc9337773e38 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 May 2026 02:00:23 +1000 Subject: [PATCH 19/21] Add significant padding to articles. --- api/index.md | 8 +- articles/fonts/caretsandselection.md | 7 + articles/fonts/checkglyphcoverage.md | 7 + articles/fonts/colorfonts.md | 7 + articles/fonts/customrendering.md | 7 + articles/fonts/fallbackfonts.md | 7 + articles/fonts/fittexttowidth.md | 7 + articles/fonts/fontmetadata.md | 7 + articles/fonts/fontmetrics.md | 7 + articles/fonts/gettingstarted.md | 7 + articles/fonts/hintingandshaping.md | 2 +- articles/fonts/index.md | 7 + articles/fonts/inspectfontfiles.md | 7 + articles/fonts/listsystemfonts.md | 7 + articles/fonts/measuringtext.md | 7 + articles/fonts/opentypefeatures.md | 7 + articles/fonts/recipes.md | 8 + articles/fonts/systemfonts.md | 7 + articles/fonts/textblock.md | 8 + articles/fonts/texthittesting.md | 7 + articles/fonts/textlayout.md | 10 +- articles/fonts/troubleshooting.md | 7 + articles/fonts/unicode.md | 8 + articles/fonts/useopentypefeatures.md | 7 + articles/fonts/variablefonts.md | 7 + articles/imagesharp.drawing/annotations.md | 175 +++++++- articles/imagesharp.drawing/badge.md | 14 +- articles/imagesharp.drawing/brushesandpens.md | 39 +- articles/imagesharp.drawing/canvas.md | 48 ++- .../imagesharp.drawing/clipimagetoshape.md | 10 + .../clippingregionslayers.md | 30 +- articles/imagesharp.drawing/gettingstarted.md | 37 +- .../imagesharp.drawing/imagesandprocessing.md | 26 +- articles/imagesharp.drawing/index.md | 21 +- .../migratingfromskiasharp.md | 57 ++- .../migratingfromsystemdrawing.md | 63 ++- articles/imagesharp.drawing/pathsandshapes.md | 28 +- articles/imagesharp.drawing/primitives.md | 17 +- articles/imagesharp.drawing/recipes.md | 14 + articles/imagesharp.drawing/softshadow.md | 12 +- articles/imagesharp.drawing/text.md | 22 +- .../transformsandcomposition.md | 37 +- .../imagesharp.drawing/troubleshooting.md | 39 +- articles/imagesharp.drawing/watermark.md | 12 +- articles/imagesharp.drawing/webgpu.md | 375 +++--------------- .../imagesharp.drawing/webgpuenvironment.md | 96 +++++ .../webgpuexternalsurface.md | 122 ++++++ .../imagesharp.drawing/webgpurendertarget.md | 104 +++++ articles/imagesharp.drawing/webgpuwindow.md | 143 +++++++ articles/imagesharp.web/configuration.md | 8 + articles/imagesharp.web/extensibility.md | 16 + articles/imagesharp.web/gettingstarted.md | 7 + articles/imagesharp.web/imagecaches.md | 12 + articles/imagesharp.web/imageproviders.md | 7 + articles/imagesharp.web/index.md | 7 + articles/imagesharp.web/processingcommands.md | 8 + articles/imagesharp.web/security.md | 12 + articles/imagesharp.web/taghelpers.md | 7 + articles/imagesharp.web/troubleshooting.md | 7 + articles/imagesharp/bmp.md | 15 + articles/imagesharp/colorandeffects.md | 12 + articles/imagesharp/configuration.md | 7 + articles/imagesharp/cropandcanvas.md | 10 + articles/imagesharp/cur.md | 13 + articles/imagesharp/exr.md | 15 + articles/imagesharp/formatconversion.md | 27 +- articles/imagesharp/gif.md | 18 + articles/imagesharp/ico.md | 15 + articles/imagesharp/identify.md | 7 + articles/imagesharp/imageformats.md | 7 + articles/imagesharp/index.md | 7 + articles/imagesharp/interop.md | 7 + articles/imagesharp/jpeg.md | 20 + articles/imagesharp/loadingandsaving.md | 13 +- articles/imagesharp/metadata.md | 10 + articles/imagesharp/migratingfromskiasharp.md | 7 + .../imagesharp/migratingfromsystemdrawing.md | 7 + articles/imagesharp/orientation.md | 7 + articles/imagesharp/pbm.md | 13 + articles/imagesharp/pixelbuffers.md | 7 + articles/imagesharp/pixelformats.md | 8 + articles/imagesharp/png.md | 18 + articles/imagesharp/processing.md | 18 + articles/imagesharp/qoi.md | 13 + articles/imagesharp/quantization.md | 7 + articles/imagesharp/recipes.md | 8 + articles/imagesharp/resize.md | 33 +- articles/imagesharp/stripmetadata.md | 7 + articles/imagesharp/tga.md | 13 + articles/imagesharp/thumbnails.md | 7 + articles/imagesharp/tiff.md | 16 + articles/imagesharp/troubleshooting.md | 7 + articles/imagesharp/webp.md | 18 + articles/polygonclipper/booleanoperations.md | 10 + articles/polygonclipper/gettingstarted.md | 7 + articles/polygonclipper/index.md | 7 + articles/polygonclipper/normalization.md | 7 + .../polygonclipper/polygonsandcontours.md | 13 + articles/polygonclipper/stroking.md | 8 + articles/toc.md | 4 + templates/modern/public/main.css | 31 +- templates/modern/public/main.js | 13 +- 102 files changed, 1832 insertions(+), 494 deletions(-) create mode 100644 articles/imagesharp.drawing/webgpuenvironment.md create mode 100644 articles/imagesharp.drawing/webgpuexternalsurface.md create mode 100644 articles/imagesharp.drawing/webgpurendertarget.md create mode 100644 articles/imagesharp.drawing/webgpuwindow.md diff --git a/api/index.md b/api/index.md index 3c0ff6ca7..af91b0177 100644 --- a/api/index.md +++ b/api/index.md @@ -1,3 +1,9 @@ # API Documentation -The API documentation is automatically generated from source-code-level comments. Often, more information can be found by looking into the [source code](https://github.com/sixlabors) itself. +The API documentation is generated from the public source comments for the Six Labors libraries. Use it when you need exact type names, overloads, constructor signatures, enum values, default behavior, inherited members, and namespace-level navigation. + +These reference pages are designed to sit alongside the article guides. Start with the articles when you are learning a feature or choosing an approach, then use the API reference when you need the precise member contract for implementation work. + +The API reference covers the libraries documented on this site, including ImageSharp, ImageSharp.Drawing, ImageSharp.Web, Fonts, and PolygonClipper. Each product area is grouped by namespace so you can move from high-level entry points, such as image processing extensions or drawing canvas APIs, down to the supporting options, primitives, and model types. + +When a reference page does not answer a design question, check the matching article section first. The source repositories remain available on [GitHub](https://github.com/sixlabors) for contributors and for cases where you need to inspect implementation details that are intentionally not part of the public API contract. diff --git a/articles/fonts/caretsandselection.md b/articles/fonts/caretsandselection.md index a82ed53cd..de90e06d8 100644 --- a/articles/fonts/caretsandselection.md +++ b/articles/fonts/caretsandselection.md @@ -144,3 +144,10 @@ Use the full [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) overloads for sel Per-line selection uses the line-box height rather than per-glyph height, which matches normal text editor and browser behavior: selecting mixed font sizes on the same line paints a consistent line-height rectangle rather than one rectangle per glyph height. The selection geometry stays visually stable across mixed fonts and font sizes. For a wider tour of the measurement model and how line metrics are derived, see [Measuring Text](measuringtext.md). + +### Practical guidance + +- Paint the selection rectangles returned by the API instead of reconstructing selection geometry yourself. +- Keep anchor and focus as logical text positions; let the metrics map them into visual rectangles. +- Use editor interaction mode when selections must include terminal blank lines. +- Test mixed LTR/RTL selections with real strings, not only simple Latin text. diff --git a/articles/fonts/checkglyphcoverage.md b/articles/fonts/checkglyphcoverage.md index 1b8d44d19..37a27afb2 100644 --- a/articles/fonts/checkglyphcoverage.md +++ b/articles/fonts/checkglyphcoverage.md @@ -44,3 +44,10 @@ Glyph coverage is only the first question. A font can contain glyphs for individ Emoji and complex scripts are the usual cases where this distinction matters. A visible emoji can be a grapheme made from several code points, and Arabic, Indic, or Southeast Asian scripts can require shaping features that are not captured by a one-code-point probe. For the conceptual fallback guidance, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). For face-level coverage inspection, see [Font Metrics](fontmetrics.md). + +### Practical guidance + +- Use coverage checks to choose fallback candidates, not to prove final rendered quality. +- Test grapheme clusters such as emoji sequences as whole strings with production layout options. +- Prefer face-level coverage inspection when building diagnostics or font picker tooling. +- Keep fallback order intentional so broad-coverage fonts do not hide preferred design choices. diff --git a/articles/fonts/colorfonts.md b/articles/fonts/colorfonts.md index 1e3d0eb9d..e150dddb6 100644 --- a/articles/fonts/colorfonts.md +++ b/articles/fonts/colorfonts.md @@ -103,3 +103,10 @@ Color-font support is part of text layout, not just final painting. If you measu Use the same [`TextOptions`](xref:SixLabors.Fonts.TextOptions) instance for both [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) and [`TextRenderer`](xref:SixLabors.Fonts.Rendering.TextRenderer) when you want a guaranteed match. For renderer implementation details, see [Custom Rendering](customrendering.md). For fallback across multiple families, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). + +### Practical guidance + +- Use the same `ColorFontSupport` setting when measuring and rendering. +- Test the actual emoji or color glyph set you intend to support; technologies vary by font. +- Decide fallback order deliberately when both monochrome and color families can cover the same text. +- Custom renderers should handle painted glyph callbacks even if most text is outline-based. diff --git a/articles/fonts/customrendering.md b/articles/fonts/customrendering.md index 4b4ec2f7f..a8d1defc7 100644 --- a/articles/fonts/customrendering.md +++ b/articles/fonts/customrendering.md @@ -143,3 +143,10 @@ public TextDecorations EnabledDecorations() ``` This makes it possible to render underline, overline, or strikeout using the same backend as the glyph outlines. + +### Practical guidance + +- Implement only the renderer callbacks your backend can honor correctly. +- Keep layout in Fonts and rendering in your backend; do not recompute shaping inside the renderer. +- Honor layer and paint callbacks when color-font output matters. +- Use decoration callbacks instead of drawing underlines from guessed metrics. diff --git a/articles/fonts/fallbackfonts.md b/articles/fonts/fallbackfonts.md index fe10e8d61..f7a27bfdb 100644 --- a/articles/fonts/fallbackfonts.md +++ b/articles/fonts/fallbackfonts.md @@ -108,3 +108,10 @@ If a script needs shaping support, make sure the selected font actually supports - Mixing many broad-coverage fonts can make fallback order hard to reason about. If layout still looks wrong after fallback is configured, see [Troubleshooting](troubleshooting.md). + +### Practical guidance + +- Put the preferred design family first, then add fallbacks in the order you want missing glyphs to be searched. +- Use `TextRuns` when a specific grapheme range must use a specific font rather than normal fallback. +- Validate fallback with real content, especially emoji, RTL text, CJK text, and combining marks. +- Remember that fallback solves missing glyphs; it does not guarantee matching style, metrics, or shaping behavior. diff --git a/articles/fonts/fittexttowidth.md b/articles/fonts/fittexttowidth.md index b228c3112..63554a3cf 100644 --- a/articles/fonts/fittexttowidth.md +++ b/articles/fonts/fittexttowidth.md @@ -50,3 +50,10 @@ For interactive systems, consider a two-stage search: probe coarse sizes first, >This example is intentionally naive. It remeasures from scratch on each iteration to keep the recipe easy to follow. Production layout engines would usually cache measurements, font instances, or intermediate fit results instead of doing a full linear probe every time. See [Measuring Text](measuringtext.md) and [Text Layout and Options](textlayout.md) for the fuller discussion. + +### Practical guidance + +- Fit with the same options you will use to render. +- Define a minimum readable size before starting the search. +- Use wrapping or truncation when shrinking would make the text unusable. +- Cache fit results when the same string, font family, and target width repeat often. diff --git a/articles/fonts/fontmetadata.md b/articles/fonts/fontmetadata.md index 89154258d..2e57f1b5b 100644 --- a/articles/fonts/fontmetadata.md +++ b/articles/fonts/fontmetadata.md @@ -113,3 +113,10 @@ FontDescription description = font.FontMetrics.Description; For loading fonts into collections, see [Loading Fonts and Collections](gettingstarted.md). For working with installed machine fonts, see [System Fonts](systemfonts.md). If you want the face-level metrics that drive layout and glyph inspection rather than just the descriptive metadata, see [Font Metrics](fontmetrics.md). + +### Practical guidance + +- Use metadata inspection before loading untrusted or user-supplied font files into normal collections. +- Store invariant names for stable configuration and localized names for UI. +- Inspect family styles before assuming bold or italic faces are available. +- Use font paths for diagnostics, not as the only identity for a face. diff --git a/articles/fonts/fontmetrics.md b/articles/fonts/fontmetrics.md index 06def22b5..f53720db8 100644 --- a/articles/fonts/fontmetrics.md +++ b/articles/fonts/fontmetrics.md @@ -229,3 +229,10 @@ Use [`FontMetrics`](xref:SixLabors.Fonts.FontMetrics) when you care about: - direct glyph inspection For face names and other descriptive metadata, see [Font Metadata and Inspection](fontmetadata.md). For variable-font usage, see [Variable Fonts](variablefonts.md). + +### Practical guidance + +- Use font metrics when layout, decoration, glyph coverage, or variation axes matter. +- Use font descriptions when the question is identity, naming, style, or version metadata. +- Treat glyph availability as a layout input, not as a guarantee of final script quality. +- Cache metrics-derived decisions with the font face and variation values that produced them. diff --git a/articles/fonts/gettingstarted.md b/articles/fonts/gettingstarted.md index a0621b52a..63a97acd5 100644 --- a/articles/fonts/gettingstarted.md +++ b/articles/fonts/gettingstarted.md @@ -121,6 +121,13 @@ Font font = family.CreateFont( The active variation values become part of the [`Font`](xref:SixLabors.Fonts.Font) instance, so the same family can be reused to create multiple design-space instances. +### Practical guidance + +- Use a private `FontCollection` when output must be stable across machines. +- Use `SystemFonts` for host-dependent behavior such as diagnostics, user font pickers, or "use what is installed here" workflows. +- Create `Font` instances for the size, style, culture, and variation values you actually intend to measure or render. +- Keep font loading separate from per-request or per-frame layout work when the same files are reused. + ### Next steps - Use [Measuring Text](measuringtext.md) when you need layout metrics before rendering. diff --git a/articles/fonts/hintingandshaping.md b/articles/fonts/hintingandshaping.md index a18cfd477..bdfccc761 100644 --- a/articles/fonts/hintingandshaping.md +++ b/articles/fonts/hintingandshaping.md @@ -60,7 +60,7 @@ The main shaping controls are: - [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) for horizontal and vertical layout behavior - [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) to request additional OpenType features such as fractions or tabular figures - [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode) to enable or disable font-provided kerning during shaping -- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) to add uniform letter spacing after the font's own spacing behavior +- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) to add uniform em-based spacing after rendered graphemes, after the font's own spacing behavior - [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) when the main font does not cover every glyph you need - [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) when different text ranges need different fonts, attributes, or decorations diff --git a/articles/fonts/index.md b/articles/fonts/index.md index 7e1f9545c..37b49d731 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -112,3 +112,10 @@ If you are new to Fonts, start with [Loading Fonts and Collections](gettingstart - [Custom Rendering](customrendering.md) - [Recipes](recipes.md) - [Troubleshooting](troubleshooting.md) + +### How to Use These Docs + +- Start with font loading and measurement before moving into shaping, fallback, and rendering. +- Use the Unicode pages whenever text ranges, caret movement, styling, or placeholders are involved. +- Use `TextBlock` pages when layout must be measured, inspected, interacted with, and rendered consistently. +- Use custom rendering only after the layout model is clear. diff --git a/articles/fonts/inspectfontfiles.md b/articles/fonts/inspectfontfiles.md index 778a8b550..340cef9c9 100644 --- a/articles/fonts/inspectfontfiles.md +++ b/articles/fonts/inspectfontfiles.md @@ -41,3 +41,10 @@ If you do want to load the collection afterward, use [`FontCollection.AddCollect Inspection does not add the font to a collection. That separation is useful for upload validation and tooling: you can reject, categorize, or display font metadata before deciding whether the file should participate in normal font resolution. For the broader metadata API, see [Font Metadata and Inspection](fontmetadata.md). + +### Practical guidance + +- Inspect uploaded font files before adding them to an application collection. +- Use collection inspection for `.ttc` and `.otc` files because one file can contain multiple faces. +- Store invariant names for stable configuration and localized names for display. +- Load the font only after metadata inspection says it belongs in the normal resolution path. diff --git a/articles/fonts/listsystemfonts.md b/articles/fonts/listsystemfonts.md index 529ecf165..25c6aeb71 100644 --- a/articles/fonts/listsystemfonts.md +++ b/articles/fonts/listsystemfonts.md @@ -47,3 +47,10 @@ This is especially useful when a family's localized name differs from the invari Culture-aware lookup is about names, not shaping. After you resolve a family, still use the correct `TextOptions.Culture`, fallback families, and layout settings for the text you are measuring or rendering. For the fuller system-font API surface, see [System Fonts](systemfonts.md). + +### Practical guidance + +- Use system font enumeration for diagnostics, not for deterministic rendering guarantees. +- Log search directories when investigating missing fonts in production. +- Prefer private font collections for document generation, tests, and server-rendered assets. +- Treat culture-aware resolution as name lookup; shaping still depends on `TextOptions` and font support. diff --git a/articles/fonts/measuringtext.md b/articles/fonts/measuringtext.md index 284aa0d5e..2107cb862 100644 --- a/articles/fonts/measuringtext.md +++ b/articles/fonts/measuringtext.md @@ -149,3 +149,10 @@ See [Hit Testing and Caret Movement](texthittesting.md) and [Selection and Bidi Always measure with the same `TextOptions` that you intend to render with. `Dpi`, `LineSpacing`, `WrappingLength`, `TextDirection`, `LayoutMode`, `KerningMode`, `Tracking`, `FeatureTags`, `TextRuns`, and fallback fonts all affect the final layout. For repeated measurement of the same string at different wrapping lengths, prefer [`TextBlock`](xref:SixLabors.Fonts.TextBlock) over calling [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) multiple times — it shapes the text once and varies wrapping per call. + +### Practical guidance + +- Measure advance when you need layout flow; measure bounds when you need ink or selection geometry. +- Keep the same `TextOptions` for measuring, rendering, hit testing, and selection. +- Use `TextBlock` when the same shaped text will be inspected or wrapped more than once. +- For UI text, test with the longest localized strings and fallback fonts, not only the default language. diff --git a/articles/fonts/opentypefeatures.md b/articles/fonts/opentypefeatures.md index 0b98c3b6d..ae10d51c9 100644 --- a/articles/fonts/opentypefeatures.md +++ b/articles/fonts/opentypefeatures.md @@ -127,3 +127,10 @@ Some OpenType features are especially relevant in vertical layout, such as [`Kno Those work alongside [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode); they do not replace it. For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For the broader shaping pipeline, see [Hinting and Shaping](hintingandshaping.md). + +### Practical guidance + +- Treat feature tags as shaping inputs that affect both measurement and rendering. +- Prefer known feature tags where available and raw four-character tags for font-specific features. +- Validate requested features with the actual production font. +- Be careful combining features that intentionally choose competing glyph forms. diff --git a/articles/fonts/recipes.md b/articles/fonts/recipes.md index 933c63c8c..416d357d5 100644 --- a/articles/fonts/recipes.md +++ b/articles/fonts/recipes.md @@ -4,6 +4,8 @@ These pages are the quick-start side of the Fonts docs. They are meant for the m Each recipe focuses on one common decision: sizing text to fit, choosing fonts, inspecting font assets, enabling OpenType features, or checking glyph coverage before rendering. Use them as starting points, then follow the linked conceptual articles when you need to understand layout coordinates, Unicode behavior, fallback, shaping, or custom rendering in more detail. +Text recipes are especially sensitive to hidden inputs. The font file, culture, DPI, fallback list, OpenType features, wrapping length, and text direction are all part of the layout. If a recipe measures text and your application later renders with different options, the result can be wider, taller, or shaped differently. + - [Fit Text to a Target Width](fittexttowidth.md) - [Inspect Font Files and Collections](inspectfontfiles.md) - [List System Fonts and Resolve by Culture](listsystemfonts.md) @@ -17,4 +19,10 @@ Each recipe focuses on one common decision: sizing text to fit, choosing fonts, - Use `TextOptions` for layout decisions such as origin, wrapping, alignment, DPI, culture, and fallback fonts. - Use `TextBlock` when you need prepared layout, line inspection, hit testing, caret movement, or selection. +## Practical Guidance + +Text output is only stable when the font assets and layout inputs are stable. If output must match across developer machines, CI, containers, and production servers, ship the required fonts and load them into a private `FontCollection` instead of relying on system fonts. Use the same `TextOptions` for measurement, hit testing, and rendering so the layout engine answers every question from the same contract. + +Fallback and shaping should be tested with the actual content your product supports: localized strings, emoji sequences, RTL text, CJK text, combining marks, and OpenType features. Cache font collections and prepared text at boundaries where the font files and layout options are known not to change; stale layout state is worse than no cache because it can look correct for simple strings and fail on real content. + Use the conceptual guides when you need the bigger picture. Use these recipes when you want a practical starting point quickly. diff --git a/articles/fonts/systemfonts.md b/articles/fonts/systemfonts.md index bdaa89a5e..9c94d02c0 100644 --- a/articles/fonts/systemfonts.md +++ b/articles/fonts/systemfonts.md @@ -102,3 +102,10 @@ The available system fonts are environment-specific. - If predictable output matters, prefer shipping the fonts you need and loading them into a [`FontCollection`](xref:SixLabors.Fonts.FontCollection). For file-based loading, see [Loading Fonts and Collections](gettingstarted.md). For metadata-only inspection, see [Font Metadata and Inspection](fontmetadata.md). + +### Practical guidance + +- Use `SystemFonts` for host-specific behavior and diagnostics. +- Use a private `FontCollection` for deterministic rendering. +- Log `SearchDirectories` when diagnosing missing fonts in containers or CI. +- Resolve by culture-aware names only when the user-facing font name is localized. diff --git a/articles/fonts/textblock.md b/articles/fonts/textblock.md index b24f43cc5..412a27f2d 100644 --- a/articles/fonts/textblock.md +++ b/articles/fonts/textblock.md @@ -110,3 +110,11 @@ Use `TextBlock` when: - You want to measure once and render later with the same prepared shaping. - You need per-line interaction (hit testing, carets, selection) — see [Hit Testing and Caret Movement](texthittesting.md). - You want to walk the laid-out text line by line without materializing every line up front. + +### Practical guidance + +Use `TextMeasurer` for one-off answers. Use `TextBlock` when the shaped text becomes state: it will be measured more than once, rendered later, inspected line by line, hit-tested, or used for caret and selection behavior. Preparing the block once keeps measurement and rendering tied to the same shaping result. + +The line-layout APIs differ by coordinate model. `GetLineLayouts(...)` returns lines positioned as one stacked block, which is what you want when the text paints as a normal paragraph or label. `EnumerateLineLayouts()` returns line-local layouts, which is what you want when another system places each line into columns, frames, paths, or virtualized rows. Choosing the wrong one usually shows up as doubled offsets or lines that are positioned correctly by themselves but not as a block. + +Keep the prepared block tied to the `TextOptions` that created it. If font, culture, fallback, feature tags, or direction changes, prepare a new block rather than trying to reuse old layout state. diff --git a/articles/fonts/texthittesting.md b/articles/fonts/texthittesting.md index 176094ac9..7d0a5e5bf 100644 --- a/articles/fonts/texthittesting.md +++ b/articles/fonts/texthittesting.md @@ -159,3 +159,10 @@ Hard line breaks at the end of non-empty lines are trimmed with other trailing b In `TextInteractionMode.Editor`, a terminal hard break also produces a blank line at the end of the text so the caret can land on it after the user types `Enter`. In `TextInteractionMode.Paragraph` that trailing blank line is omitted, matching paragraph-style layout. For more on the underlying measurement model and the `TextMetrics` shape, see [Measuring Text](measuringtext.md). For the full selection API, see [Selection and Bidi Drag](caretsandselection.md). + +### Practical guidance + +- Use `TextMetrics` for interaction that can cross line boundaries. +- Use `LineLayout` only when the caller already knows the interaction is line-local. +- Choose `TextInteractionMode.Editor` for editable text and `Paragraph` for display layout. +- Keep hit testing, caret movement, and selection tied to the same measured layout. diff --git a/articles/fonts/textlayout.md b/articles/fonts/textlayout.md index 20246467d..b962d8584 100644 --- a/articles/fonts/textlayout.md +++ b/articles/fonts/textlayout.md @@ -104,7 +104,7 @@ Fonts exposes several knobs that directly affect glyph layout: - [`LineSpacing`](xref:SixLabors.Fonts.TextOptions.LineSpacing) multiplies the line height. - [`TabWidth`](xref:SixLabors.Fonts.TextOptions.TabWidth) controls tab stops in space units. - [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode) enables, disables, or lets the engine decide about font-provided kerning during shaping. -- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) applies uniform letter-spacing and is measured in em. +- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) adds uniform spacing after each rendered grapheme. It is measured in em, so `0.02F` adds 2% of the current em size; it is not a multiplier like `LineSpacing`. - [`HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) is separate from shaping and controls TrueType grid fitting for the current size and DPI. ```csharp @@ -242,3 +242,11 @@ FontRectangle bounds = TextMeasurer.MeasureAdvance(text, options); ``` [`TextPlaceholderAlignment`](xref:SixLabors.Fonts.TextPlaceholderAlignment) controls how the placeholder box aligns with the surrounding line. `Baseline` uses the supplied baseline offset directly, while `AboveBaseline`, `BelowBaseline`, `Top`, `Bottom`, and `Middle` align the placeholder against the surrounding line box. + +### Practical guidance + +Treat `TextOptions` as the complete layout contract for a string. Font, culture, DPI, wrapping length, line spacing, direction, layout mode, fallback families, feature tags, text runs, and placeholders all participate in shaping and measurement. If you measure with one set of options and render with another, the result can move, wrap, or shape differently. + +Use grapheme indexes for `TextRun` ranges and placeholder insertion points. A placeholder is an insertion into the layout flow, not a replacement for characters in the source string, so its run is zero-length: `[Start, End)` with the same value for both ends. That keeps source text ranges stable while still reserving inline space for an object that your renderer draws separately. + +When text must fit inside a known region, set wrapping and alignment explicitly. Avoid measuring a string manually and then adjusting coordinates by hand; that bypasses the layout engine exactly where shaping, fallback, bidi order, and line metrics matter most. diff --git a/articles/fonts/troubleshooting.md b/articles/fonts/troubleshooting.md index fec9ad53f..5a212b092 100644 --- a/articles/fonts/troubleshooting.md +++ b/articles/fonts/troubleshooting.md @@ -103,3 +103,10 @@ Use [`font.FontMetrics.TryGetVariationAxes(...)`](xref:SixLabors.Fonts.FontMetri [`SystemFonts`](xref:SixLabors.Fonts.SystemFonts) is convenient, but it is not deterministic across environments. Different machines can have different installed families, versions, and script coverage. If you need repeatable output across CI, servers, containers, and user machines, ship your own fonts and load them through `FontCollection`. + +### Debugging checklist + +- Confirm the font file or system family is actually available in the current environment. +- Confirm measurement and rendering use the same `TextOptions`. +- Check whether indexes are grapheme indexes, code-point indexes, or UTF-16 indexes. +- Inspect fallback coverage before assuming a missing glyph is a renderer problem. diff --git a/articles/fonts/unicode.md b/articles/fonts/unicode.md index 0c8bab356..604cbf6fd 100644 --- a/articles/fonts/unicode.md +++ b/articles/fonts/unicode.md @@ -186,3 +186,11 @@ Fonts uses additional Unicode logic internally during layout, including line-bre - `CodePoint` If you are debugging a `TextRun` range, a missing glyph, or a mismatch between visible text and string indices, start by checking whether you are reasoning in `char`, `CodePoint`, or grapheme units. + +### Practical guidance + +Use grapheme indexes for user-visible ranges: styling, selection, caret movement, placeholder insertion, and rich text runs. That is the unit closest to what a person thinks of as one visible text element, even when it is made from multiple code points. + +Use code points when the question is about Unicode scalar values: probing glyph availability, inspecting script coverage, or understanding encoded sequence length. Use UTF-16 indexes only when interoperating with raw .NET string storage or APIs that explicitly require `char` offsets. + +Never assume visible characters, code points, and UTF-16 code units have the same count. That assumption is the root cause of most off-by-one text range bugs in emoji, combining marks, and complex scripts. diff --git a/articles/fonts/useopentypefeatures.md b/articles/fonts/useopentypefeatures.md index f12c2c643..91d62fed7 100644 --- a/articles/fonts/useopentypefeatures.md +++ b/articles/fonts/useopentypefeatures.md @@ -60,3 +60,10 @@ Use the same `TextOptions` for both `TextMeasurer` and `TextRenderer` so the mea OpenType features can change glyph choice, advance widths, ligature formation, and mark placement. That means they are layout inputs, not just visual decoration applied after measuring. For the fuller feature model, see [OpenType Features](opentypefeatures.md). + +### Practical guidance + +- Verify that the production font actually exposes the requested feature tags. +- Use the same feature tags for measurement and rendering. +- Be careful combining mutually exclusive numeric features such as figure styles. +- Prefer `KnownFeatureTags` for standard features and `Tag.Parse(...)` for font-specific stylistic sets. diff --git a/articles/fonts/variablefonts.md b/articles/fonts/variablefonts.md index 4e435686e..31a1a5731 100644 --- a/articles/fonts/variablefonts.md +++ b/articles/fonts/variablefonts.md @@ -119,3 +119,10 @@ Variable fonts are especially useful when you want to: - keep a single family while exploring many design-space instances If you run into unexpected results, see [Troubleshooting](troubleshooting.md). + +### Practical guidance + +- Inspect available axes before exposing variation controls. +- Store axis tags and values with the chosen font family so output can be reproduced. +- Use optical size intentionally; it is not just another scale factor. +- Fall back gracefully when a configured axis is missing from a replacement font. diff --git a/articles/imagesharp.drawing/annotations.md b/articles/imagesharp.drawing/annotations.md index 977675f4c..f4a3d2286 100644 --- a/articles/imagesharp.drawing/annotations.md +++ b/articles/imagesharp.drawing/annotations.md @@ -1,21 +1,78 @@ # Add Callouts and Annotations -Annotations are just normal drawing commands layered over an existing image. Use pens for outlines and guides, transparent fills for highlights, and text layout options for labels. +Annotations are overlays that explain or identify parts of an existing image. In ImageSharp.Drawing they are built from the same primitives as any other drawing: fills, strokes, text, lines, clips, and regions. The useful part is not the style; it is the workflow for keeping annotation geometry tied to the pixels it describes. -Treat annotation geometry as part of the image coordinate system. That makes the overlay deterministic: the highlight rectangle, guide line, and label origin all describe exact positions on the final image. If you resize the image first, compute the annotation positions after resizing so the callouts still point at the right pixels. +An annotation usually has three pieces: + +- a target region in image coordinates; +- a visual marker such as a fill, outline, arrow, or leader line; +- a label laid out in a predictable rectangle. + +Compute those pieces after the image has the size and orientation that will be exported. If you resize, crop, or auto-orient after drawing annotations, the overlay will be transformed with the pixels and may no longer point at the intended feature. + +## Coordinate Workflow + +Keep annotation geometry in final image coordinates. If the target was detected in source-image coordinates, map it after any crop, resize, or orientation step before drawing. + +```csharp +using SixLabors.ImageSharp; + +static Rectangle ScaleRectangle(Rectangle source, Size sourceSize, Size destinationSize) +{ + float scaleX = (float)destinationSize.Width / sourceSize.Width; + float scaleY = (float)destinationSize.Height / sourceSize.Height; + + return new Rectangle( + (int)MathF.Round(source.X * scaleX), + (int)MathF.Round(source.Y * scaleY), + (int)MathF.Round(source.Width * scaleX), + (int)MathF.Round(source.Height * scaleY)); +} +``` + +Use the same mapping for every point that belongs to the annotation: highlight bounds, leader start, leader end, label origin, and panel bounds. That keeps the annotation coherent when the output size changes. + +## Highlight a Region + +A rectangular highlight is the simplest annotation. Fill the target with a translucent brush, then stroke the same rectangle so the boundary is clear. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("photo.jpg"); + +Rectangle regionOfInterest = new(92, 64, 220, 140); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.22F)), regionOfInterest); + canvas.Draw(Pens.Dash(Color.Gold, 5), regionOfInterest); +})); + +image.Save("highlighted.jpg"); +``` + +Use a rectangle overload when the marker is just a one-off rectangular highlight. Use a reusable path or polygon when the same geometry must be filled, stroked, clipped, measured, or passed through a geometry operation. + +## Add a Leader and Label + +Labels are normal text drawing. Use [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) so wrapping and placement are explicit. Use a text stroke when the label must remain readable over arbitrary image content. ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Image image = Image.Load("photo.jpg"); -Rectangle highlight = new(92, 64, 220, 140); -PointF labelOrigin = new(highlight.Right + 28, highlight.Top + 12); +Rectangle target = new(92, 64, 220, 140); +PointF targetEdge = new(target.Right, target.Top + (target.Height / 2F)); +PointF labelOrigin = new(target.Right + 28, target.Top + 12); Font font = SystemFonts.CreateFont("Arial", 24, FontStyle.Bold); RichTextOptions labelOptions = new(font) { @@ -25,27 +82,119 @@ RichTextOptions labelOptions = new(font) image.Mutate(ctx => ctx.Paint(canvas => { - canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.22F)), highlight); - canvas.Draw(Pens.Dash(Color.Gold, 5), highlight); + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.22F)), target); + canvas.Draw(Pens.Dash(Color.Gold, 5), target); - // The guide line connects the label to the highlighted region without changing the image pixels underneath. + // The leader line connects the label to the target in the same image coordinate system. canvas.DrawLine( Pens.Solid(Color.Gold, 3), new PointF(labelOrigin.X - 12, labelOrigin.Y + 12), - new PointF(highlight.Right, highlight.Top + (highlight.Height / 2F))); + targetEdge); - canvas.DrawText(labelOptions, "Region of interest", Brushes.Solid(Color.White), Pens.Solid(Color.Black, 1.5F)); + // The outline pen makes the text readable over mixed light and dark pixels. + canvas.DrawText( + labelOptions, + "Region of interest", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 1.5F)); })); image.Save("annotated.jpg"); ``` -Keep annotation geometry in image coordinates. If you need a local coordinate system for a panel or inset, use `CreateRegion(...)` or a saved transform. +Draw the leader before the label so the label remains crisp and unobstructed. When a label can contain user-supplied text, set `WrappingLength` and choose an origin that leaves room for multiple lines. + +## Use a Local Panel Region + +When a callout contains several items, use [`CreateRegion(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.CreateRegion*) so the panel has local coordinates. The parent canvas still uses image coordinates; the child region uses `(0, 0)` at the panel origin. + +```csharp +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +using Image image = Image.Load("photo.jpg"); + +Rectangle target = new(92, 64, 220, 140); +Rectangle panelBounds = new(348, 52, 260, 116); +Font titleFont = SystemFonts.CreateFont("Arial", 22, FontStyle.Bold); +Font bodyFont = SystemFonts.CreateFont("Arial", 16); + +image.Mutate(ctx => ctx.Paint(canvas => +{ + canvas.Draw(Pens.Solid(Color.Gold, 4), target); + canvas.DrawLine( + Pens.Solid(Color.Gold, 3), + new PointF(target.Right, target.Top + (target.Height / 2F)), + new PointF(panelBounds.Left, panelBounds.Top + 34)); + + using DrawingCanvas panel = canvas.CreateRegion(panelBounds); + + panel.Fill(Brushes.Solid(Color.Black.WithAlpha(0.72F))); + panel.Draw(Pens.Solid(Color.Gold, 2), new Rectangle(0, 0, panelBounds.Width, panelBounds.Height)); + + // Text inside the region is positioned relative to the panel, not the source image. + panel.DrawText( + new RichTextOptions(titleFont) { Origin = new(14, 12), WrappingLength = panelBounds.Width - 28 }, + "Inspection note", + Brushes.Solid(Color.White), + pen: null); + + panel.DrawText( + new RichTextOptions(bodyFont) { Origin = new(14, 48), WrappingLength = panelBounds.Width - 28 }, + "The highlighted area is drawn in parent coordinates; this panel uses local coordinates.", + Brushes.Solid(Color.WhiteSmoke), + pen: null); +})); + +image.Save("annotation-panel.jpg"); +``` + +Region canvases are useful for labels, inset panels, badges, and legends because the panel layout can be written once without repeatedly adding the parent offset. + +## Clip an Annotation to a Shape + +Use `Save(DrawingOptions, params IPath[])` when a marker should be constrained to a non-rectangular target. The clip uses `ShapeOptions.BooleanOperation`, so set `Intersection` for "draw only inside this shape" behavior. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; + +DrawingOptions insideShape = new() +{ + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } +}; + +EllipsePolygon target = new(220, 140, 180, 96); + +canvas.Save(insideShape, target); +canvas.Fill(Brushes.ForwardDiagonal(Color.Gold.WithAlpha(0.5F), Color.Transparent), new Rectangle(120, 82, 200, 116)); +canvas.Restore(); + +canvas.Draw(Pens.Solid(Color.Gold, 4), target); +``` + +The fill and outline are separate on purpose: the hatch is clipped to the target, then the outline is drawn after `Restore()` so it remains crisp. + +## Practical Guidance -Prefer translucent fills for highlighting because they preserve the source image context. Use an outline pen or text stroke when the annotation must remain readable over both light and dark image regions. +- Normalize orientation, crop, and resize before computing annotation geometry. +- Keep target geometry in image coordinates, and use regions only for local panel layout. +- Use primitive rectangle, line, and text APIs for one-off callouts. +- Use paths or polygons when annotation geometry must be reused for clipping, fill, stroke, or measurement. +- Draw translucent markers before crisp outlines and labels. +- Set text wrapping instead of assuming label text will fit on one line. ## Related Topics - [Canvas Drawing](canvas.md) +- [Primitive Drawing Helpers](primitives.md) - [Brushes and Pens](brushesandpens.md) -- [Transforms and Composition](transformsandcomposition.md) +- [Clipping, Regions, and Layers](clippingregionslayers.md) +- [Drawing Text](text.md) diff --git a/articles/imagesharp.drawing/badge.md b/articles/imagesharp.drawing/badge.md index 6cbbb9a09..ce0709830 100644 --- a/articles/imagesharp.drawing/badge.md +++ b/articles/imagesharp.drawing/badge.md @@ -1,8 +1,12 @@ # Draw a Badge or Label -Small generated badges usually combine a filled shape, an outline, and centered text. Build the shape once, then use the same path for fill and stroke so the border exactly follows the filled area. +Small generated badges usually combine a filled shape, an outline, and centered text. Define the badge bounds once, then use the same geometry for fill and stroke so the border exactly follows the filled area. -This pattern works well for status chips, Open Graph badges, generated labels, and small UI assets. Keep the badge geometry, gradient, and text layout separate: the same path controls fill and stroke, while `RichTextOptions` controls how the label sits inside the shape. +This pattern works well for status chips, Open Graph badges, generated labels, and small UI assets. Keep the badge geometry, gradient, and text layout separate: the same rectangle controls fill and stroke, while `RichTextOptions` controls how the label sits inside the shape. + +Generated badges tend to be consumed by other layout systems, so stable output dimensions matter. Decide the canvas size and badge bounds first, then fit text inside that region with wrapping and centered alignment. If labels can vary by localization, tenant name, or status text, leave more horizontal padding than the ideal English sample appears to need. + +The same geometry should usually drive the fill and the stroke. That avoids one-pixel mismatches where a border no longer follows the filled shape. For more complex badge shapes, build a custom path once and reuse it for the gradient fill, outline, clipping, and any hit-test or layout calculations. ```csharp using SixLabors.Fonts; @@ -14,7 +18,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 180, Color.Transparent.ToPixel()); -RectanglePolygon badge = new(24, 36, 372, 108); +Rectangle badge = new(24, 36, 372, 108); Font font = SystemFonts.CreateFont("Arial", 38, FontStyle.Bold); PointF gradientStart = new(24, 36); PointF gradientEnd = new(396, 144); @@ -55,3 +59,7 @@ If the label can vary, set `WrappingLength` smaller than the badge width and use - [Primitive Drawing Helpers](primitives.md) - [Brushes and Pens](brushesandpens.md) - [Drawing Text](text.md) + +## Practical Guidance + +Build badge geometry once and reuse it for fill and stroke. Keep source dimensions stable when generated badges are consumed by layout systems. Set text wrapping shorter than the badge width when labels can vary, and use centered alignment instead of manual text offsets. diff --git a/articles/imagesharp.drawing/brushesandpens.md b/articles/imagesharp.drawing/brushesandpens.md index dd7875d82..7cb43330e 100644 --- a/articles/imagesharp.drawing/brushesandpens.md +++ b/articles/imagesharp.drawing/brushesandpens.md @@ -1,9 +1,17 @@ # Brushes and Pens -Brushes fill covered pixels. Pens define the outline generated when you stroke a path, line, or shape. +Brushes and pens separate *coverage* from *style*. A shape, path, text glyph, or generated stroke decides which pixels are covered. The brush then shades those covered pixels using a solid color, gradient, repeated pattern, image tile, or other brush source. + +Pens are built on top of brushes. A pen does not directly paint a centerline; it expands the source line, path, or shape into stroke geometry using the pen width, caps, joins, miter limit, and dash pattern. That generated outline is then filled with the pen's brush. This matters when you debug output: stroke shape problems belong to `StrokeOptions`, while color, gradient, hatch, and image-fill problems belong to the brush. + +Brushes and pens are recorded as part of canvas drawing intent, so keep any referenced resources alive until the canvas has replayed. This is especially important for `ImageBrush`, which references the source image rather than taking ownership of it. ## Solid Brushes and Pens +Solid brushes and solid pens are the simplest styling objects. Use them for flat fills, outlines, guides, and most annotation work. The same brush can be used directly in `Fill(...)` or as the fill used by a pen stroke. + +The pen width is expressed in the path's local coordinate space before the active drawing transform is applied. If you save a scaled transform on the canvas, the stroke geometry is prepared with that state during replay, so a scaled drawing state can scale the visible stroke as well as the path. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -19,15 +27,16 @@ image.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(Brushes.Solid(Color.LightSkyBlue), panel); canvas.Draw(Pens.Solid(Color.Navy, 4), panel); - EllipsePolygon ellipse = new(new PointF(230, 118), new SizeF(118, 72)); - canvas.Fill(Brushes.Solid(Color.Gold), ellipse); - canvas.Draw(Pens.Solid(Color.DarkOrange, 5), ellipse); + canvas.FillEllipse(Brushes.Solid(Color.Gold), new(230, 118), new(118, 72)); + canvas.DrawEllipse(Pens.Solid(Color.DarkOrange, 5), new(230, 118), new(118, 72)); })); ``` ## Pattern Brushes and Pattern Pens -The [`Brushes`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes) and [`Pens`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens) factories include common hatch and dash styles. +Pattern brushes are small repeating color matrices. The built-in hatch helpers on [`Brushes`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes) create common foreground/background matrices such as horizontal, vertical, diagonal, and percentage patterns. Use a transparent background when the pattern should sit over existing pixels, or pass an opaque background color when the pattern should fully cover the area. + +Pattern pens combine the same stroke-generation model as other pens with a dash pattern. [`Pens.Dash(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Dash*), [`Pens.Dot(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Dot*), [`Pens.DashDot(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.DashDot*), and [`Pens.DashDotDot(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.DashDotDot*) are convenience factories for the common sequences. Pass a brush instead of a color when the stroke itself should be filled with a gradient, hatch, or image pattern. ```csharp using SixLabors.ImageSharp; @@ -91,7 +100,9 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Gradient Brushes -Gradient brushes shade fills across space. Use color stops to describe the gradient ramp. +Gradient brushes shade covered pixels from positions in canvas space. The color stops describe the ramp, and the brush geometry describes how that ramp is mapped into the drawn area. A linear gradient moves along a line between two points. A radial gradient expands from a center point and radius. Repetition mode controls what happens outside the primary gradient span: clamp to the edge colors, repeat the ramp, or reflect it. + +Because the brush is evaluated over the covered pixels, the same gradient can be reused across several shapes to make them appear lit by one continuous source. If each shape needs its own independent gradient, create a brush whose points and radius match that shape instead of sharing one global brush. ```csharp using SixLabors.ImageSharp; @@ -117,12 +128,10 @@ RadialGradientBrush radial = new( new(0F, Color.Orange), new(1F, Color.MediumVioletRed.WithAlpha(0.25F))); -EllipsePolygon radialShape = new(new PointF(306, 116), new SizeF(156, 112)); - image.Mutate(ctx => ctx.Paint(canvas => { canvas.Fill(linear, new Rectangle(24, 24, 190, 132)); - canvas.Fill(radial, radialShape); + canvas.FillEllipse(radial, new(306, 116), new(156, 112)); })); ``` @@ -169,7 +178,9 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Stroke Shape Options -`StrokeOptions` controls how outlines are generated before rasterization. +`StrokeOptions` controls the geometry produced before the pen's brush is applied. `LineCap` affects the ends of open paths and line segments. `LineJoin` affects corners where segments meet. `MiterLimit` limits how far sharp miter joins can extend before the join falls back to a bevel-style shape. `ArcDetailScale` controls the detail used when rounded joins and caps are converted into geometry. + +Use stroke options when the outline itself is wrong: squared ends, overly sharp corners, clipped-looking miters, or rounded joins that need more detail. Use a different brush when the outline shape is correct but the stroke color, gradient, pattern, or image fill is wrong. ```csharp using SixLabors.ImageSharp; @@ -243,7 +254,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 240, Color.White.ToPixel()); -EllipsePolygon clip = new(new PointF(210, 120), new SizeF(300, 150)); +EllipsePolygon clip = new(210, 120, 300, 150); LinearGradientBrush brush = new( new PointF(40, 40), new PointF(380, 200), @@ -270,3 +281,9 @@ image.Mutate(ctx => ctx.Paint(canvas => canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2), clip); })); ``` + +## Practical Guidance + +Brushes and pens answer different questions. A brush shades covered pixels. A pen describes how a stroke outline is generated and how that outline is filled. Keeping that distinction clear prevents a lot of awkward geometry code: cap, join, miter, dash, and stroke-width decisions belong on the pen, not in hand-built outline paths. + +Create reusable pens and brushes when the same style appears across many commands. That keeps examples readable and production drawing code easier to audit. Use canvas clipping state when a style should be constrained to a region; clipping is part of drawing state, not something each brush or pen needs to know about. diff --git a/articles/imagesharp.drawing/canvas.md b/articles/imagesharp.drawing/canvas.md index e58d52b71..f66a515fa 100644 --- a/articles/imagesharp.drawing/canvas.md +++ b/articles/imagesharp.drawing/canvas.md @@ -22,9 +22,13 @@ image.Mutate(ctx => ctx.Paint(canvas => The callback receives a canvas for the current frame. Use the canvas for all drawing work that should happen together. -## Deferred Drawing and Replay +## Ordered Calls and Replay -[`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) looks immediate, but most drawing commands are recorded first and replayed later. Calls such as [`Fill(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Fill*), [`Draw(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Draw*), [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*), and [`SaveLayer(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.SaveLayer*) append drawing intent to a command buffer. Calls that must happen at a specific point, such as [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) and [`RenderScene(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.RenderScene*) are stored as entries in the canvas replay timeline. +[`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) is an ordered drawing API backed by a replay timeline. The calls look familiar if you have used immediate-mode drawing APIs: [`Fill(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Fill*), [`Draw(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Draw*), [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*), [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*), and [`Restore()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Restore) are made in the order you want drawing to happen. The canvas does not, however, promise that each call immediately writes pixels to the destination. + +It is also not a retained object tree. The canvas does not keep editable shape objects that automatically redraw when their properties change. It records ordered drawing intent, seals that intent into timeline entries, and replays the timeline into the active backend. + +Most drawing calls append drawing intent to a command buffer. Calls that must happen at a specific point, such as [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) and [`RenderScene(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.RenderScene*) are stored as entries in the canvas replay timeline. [`SaveLayer(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.SaveLayer*) is also timeline-sensitive: it records an isolated group that is later composited back into the parent. The root canvas replays the timeline when it is disposed. During replay, command ranges are prepared into backend command batches, and the backend creates and renders scenes for those ranges. This is why a manually-created canvas must be disposed: disposal is the point where recorded work is actually rendered into the target. @@ -32,9 +36,9 @@ The replay timeline can contain three kinds of entry: - command ranges for normal drawing commands - apply barriers for `Apply(...)` operations -- retained scene references inserted by `RenderScene(...)` +- retained backend scene references inserted by `RenderScene(...)` -This deferred model lets ImageSharp.Drawing use one public canvas API for CPU images, WebGPU surfaces, and retained backend scenes. The canvas records drawing intent once, performs shared preparation once, and then hands a stable command batch to the active backend. +This model keeps drawing code straightforward while still allowing ImageSharp.Drawing to prepare command batches, insert replay barriers, reuse retained backend scenes, and target CPU images or WebGPU surfaces through the same public canvas API. [`Flush()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Flush) seals the commands recorded so far into a command-range timeline entry. It does not render immediately by itself. Most code does not need it; replay barriers such as [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) already seal earlier commands before they run. @@ -76,6 +80,8 @@ Use [`Fill(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Fil [`Clear(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Clear*) can target the full canvas, a rectangle, or any [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath). It also honors the active clip state created by [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*), so clears can be scoped by both the supplied clear shape and the current canvas state. +The difference is compositing intent. `Fill(...)` draws a brush through the active `GraphicsOptions`, so source alpha and blend modes affect the destination. `Clear(...)` uses clear-style composition for the covered region, so it is the right API when the drawing command should replace or erase what was there before. Use transparent clear for cutouts, masks, and punched holes. Use an opaque brush with `Clear(...)` when the area should be reset to a known color regardless of the pixels already underneath. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -97,13 +103,13 @@ image.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); canvas.Fill(Brushes.Solid(Color.Crimson.WithAlpha(0.8F)), new Rectangle(26, 18, 268, 164)); - EllipsePolygon clip = new(new PointF(160, 100), new SizeF(214, 126)); + EllipsePolygon clip = new(160, 100, 214, 126); _ = canvas.Save(clipToEllipse, clip); canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.85F))); // Transparent clear removes content inside the supplied path and active clip. - EllipsePolygon cutout = new(new PointF(164, 98), new SizeF(74, 48)); + EllipsePolygon cutout = new(164, 98, 74, 48); canvas.Clear(Brushes.Solid(Color.Transparent), cutout); canvas.Restore(); @@ -119,6 +125,8 @@ The overload [`Save(DrawingOptions, params IPath[])`](xref:SixLabors.ImageSharp. The active state reference is captured when each command is recorded. Later [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*) or [`Restore()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Restore) calls do not replace the state for commands already in the command buffer, but mutating a referenced [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) instance can still affect commands that captured that same instance. +In normal application code, create the options you want before saving them and avoid mutating the same instance while it is active. That keeps the recorded timeline easy to reason about: save a state, record commands under that state, then restore it. If different groups need different transforms, clips, or blending, use separate `DrawingOptions` instances. + The state captured for drawing includes: - [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions), including graphics options, shape options, and transform @@ -151,7 +159,7 @@ DrawingOptions clipInside = new() image.Mutate(ctx => ctx.Paint(canvas => { - EllipsePolygon clipPath = new(new PointF(180, 110), new SizeF(260, 140)); + EllipsePolygon clipPath = new(180, 110, 260, 140); _ = canvas.Save(clipInside, clipPath); canvas.Fill(Brushes.Solid(Color.MidnightBlue), new Rectangle(0, 0, 360, 220)); @@ -228,7 +236,7 @@ image.Mutate(ctx => ctx.Paint(canvas => _ = canvas.SaveLayer(layerOptions, new Rectangle(70, 46, 220, 128)); // The layer bounds isolate composition; these coordinates are still parent-canvas coordinates. - canvas.Fill(Brushes.Solid(Color.OrangeRed), new EllipsePolygon(new PointF(180, 110), new SizeF(170, 96))); + canvas.FillEllipse(Brushes.Solid(Color.OrangeRed), new(180, 110), new(170, 96)); canvas.Draw(Pens.Solid(Color.White, 8), new Rectangle(96, 74, 168, 72)); canvas.Restore(); @@ -246,7 +254,9 @@ Use bounded layers deliberately. A smaller layer bounds can reduce the isolated The source rectangle is sampled from the source image and scaled into the destination rectangle. The current transform and clip state apply to the destination drawing. Source rectangles that extend outside the source image are clipped to the available pixels. -Because canvas drawing is deferred, the source image must remain alive until the canvas has replayed the command. With `Paint(...)`, that means keeping the source image alive for the duration of the `Mutate(...)` call. With a manually-created canvas, keep it alive until the canvas is disposed. +Because canvas drawing is replayed later, the source image must remain alive until the canvas has replayed the command. With `Paint(...)`, that means keeping the source image alive for the duration of the `Mutate(...)` call. With a manually-created canvas, keep it alive until the canvas is disposed. + +Treat source and destination rectangles as two different coordinate systems. The source rectangle selects pixels from the input image. The destination rectangle places those selected pixels on the canvas. That separation lets you crop, zoom, or letterbox an image without changing the source file. ```csharp using SixLabors.ImageSharp; @@ -265,7 +275,7 @@ DrawingOptions clipInside = new() } }; -EllipsePolygon clip = new(new PointF(210, 130), new SizeF(300, 170)); +EllipsePolygon clip = new(210, 130, 300, 170); Rectangle sourceRect = new(20, 12, 240, 180); RectangleF destination = new(60, 45, 300, 170); @@ -323,10 +333,12 @@ Use the pen's `StrokeOptions` for stroke shape: ## Retained Scene Replay -Use `CreateScene()` when the same recorded drawing should be replayed into more than one canvas target. It seals and prepares the recorded drawing commands into a retained backend scene. `RenderScene(...)` inserts that retained scene into the receiving canvas timeline at the point where it is called. +Use `CreateScene()` when the same recorded drawing should be replayed into more than one canvas target. It seals and prepares the recorded drawing commands into a retained backend scene. `RenderScene(...)` inserts that retained backend scene into the receiving canvas timeline at the point where it is called. The scene is backend-owned state, so keep it alive until every canvas that records it has been disposed. A canvas that receives `RenderScene(...)` still replays on disposal like any other canvas. +Retained backend scenes are useful when preparation is the repeated cost: logos, icons, map overlays, decorative vector art, or other drawing that is reused across many targets. They are not editable scene graphs. If the geometry, brushes, text, or image resources need to change, record a new scene. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -337,7 +349,7 @@ using SixLabors.ImageSharp.PixelFormats; using Image source = new(160, 120, Color.Transparent.ToPixel()); using DrawingCanvas sourceCanvas = source.Frames.RootFrame.CreateCanvas(source.Configuration, new()); -sourceCanvas.Fill(Brushes.Solid(Color.Gold), new EllipsePolygon(new PointF(80, 60), new SizeF(116, 72))); +sourceCanvas.FillEllipse(Brushes.Solid(Color.Gold), new(80, 60), new(116, 72)); sourceCanvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(12, 12, 136, 96)); using DrawingBackendScene scene = sourceCanvas.CreateScene(); @@ -353,7 +365,7 @@ secondCanvas.RenderScene(scene); secondCanvas.Dispose(); ``` -`RenderScene(...)` preserves timeline order. Commands recorded before it replay before the retained scene; commands recorded after it replay after the retained scene. +`RenderScene(...)` preserves timeline order. Commands recorded before it replay before the retained backend scene; commands recorded after it replay after the retained backend scene. ## Apply Image Processing to a Region @@ -375,7 +387,7 @@ image.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(Brushes.Solid(Color.LightGray)); canvas.Draw(Pens.Solid(Color.Black, 4), new Rectangle(24, 24, 312, 172)); - EllipsePolygon blurPath = new(new PointF(180, 110), new SizeF(220, 120)); + EllipsePolygon blurPath = new(180, 110, 220, 120); // The blur is clipped to the supplied path region. canvas.Apply(blurPath, region => region.GaussianBlur(8)); @@ -383,3 +395,11 @@ image.Mutate(ctx => ctx.Paint(canvas => ``` Because `Apply(...)` reads pixels at its replay point, commands before the barrier affect the processed image, and commands after the barrier do not. + +## Practical Guidance + +Use `Paint(...)` for ordinary ImageSharp processing pipelines. It gives you a canvas at the right point in `Mutate(...)` or `Clone(...)` and owns the replay lifetime for you. Use `CreateCanvas(...)` when you already have an image frame or backend target and need explicit lifetime control. In that case disposal is part of correctness: it is the point where the recorded work is replayed. + +Because canvas drawing is replayed later, anything referenced by recorded commands must stay alive until replay has completed. That includes source images for `DrawImage(...)`, image brushes, fonts, paths, and retained backend scenes. This is the important difference from strictly immediate pixel-writing APIs: the call records drawing intent, but the referenced objects may still be needed later. + +Scope state narrowly. `Save(...)` and `Restore()` are the right model for transforms, clipping, and graphics options that affect a limited part of the drawing. Use `SaveLayer(...)` when several commands should first render into an isolated group and then composite back as one result. Use `Apply(...)` when ImageSharp processors need to observe the timeline at a specific point, and keep those regions tight so CPU work and GPU readback stay bounded. diff --git a/articles/imagesharp.drawing/clipimagetoshape.md b/articles/imagesharp.drawing/clipimagetoshape.md index b36673e5c..deb97e54d 100644 --- a/articles/imagesharp.drawing/clipimagetoshape.md +++ b/articles/imagesharp.drawing/clipimagetoshape.md @@ -4,6 +4,12 @@ Use `Save(DrawingOptions, params IPath[])` with `BooleanOperation.Intersection` The important idea is that clipping is canvas state. Once saved, the clip applies to every later command until `Restore()` is called. Draw the clipped image while that state is active, then restore before drawing borders, labels, shadows, or other elements that should sit outside the mask. +Think about the image placement and the mask separately. The clip path decides where pixels are allowed to appear. The `DrawImage(...)` destination rectangle decides how the source image is cropped and scaled into the canvas. Matching the destination rectangle to the shape bounds gives predictable avatar-style crops; using a larger rectangle intentionally zooms or pans the source behind the mask. + +Clipping is also stateful, so restore as soon as the clipped drawing is complete. If you forget to restore, later borders, shadows, and labels will be clipped too, which often looks like missing drawing rather than a clipping bug. + +The clip path and destination rectangle do not need to be identical, but they should be chosen deliberately. Equal bounds give a simple fit. A larger destination rectangle zooms the source behind the mask. An offset destination rectangle pans the source without moving the mask. That separation is what lets one avatar shape support several crop choices. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -51,3 +57,7 @@ Use a destination rectangle that matches the visible shape bounds when you want - [Clipping, Regions, and Layers](clippingregionslayers.md) - [Images, Masks, and Processing](imagesandprocessing.md) - [Troubleshooting](troubleshooting.md) + +## Practical Guidance + +Save clipped state only around the commands that should be constrained, then restore before drawing borders, labels, or shadows that should sit outside the clip. Keep the source image alive until canvas replay has finished. Match the destination rectangle to the visible shape unless an intentional zoom or bleed is required. diff --git a/articles/imagesharp.drawing/clippingregionslayers.md b/articles/imagesharp.drawing/clippingregionslayers.md index 2dda2c93f..a97212c25 100644 --- a/articles/imagesharp.drawing/clippingregionslayers.md +++ b/articles/imagesharp.drawing/clippingregionslayers.md @@ -2,14 +2,20 @@ Canvas state controls where later commands can draw and how grouped commands are composed. The three main tools are `Save(...)` with clip paths, `CreateRegion(...)`, and `SaveLayer(...)`. -Use a clip when later commands should be constrained by vector geometry. Use a region when you want a rectangular child layout with local coordinates. Use a layer when several commands should be rendered together first, then blended or composited back as one result. +These APIs solve different problems and should not be treated as interchangeable: + +- Use a clip when later commands should be constrained by vector geometry while staying in the current coordinate system. +- Use a region when you want a rectangular child canvas where `(0, 0)` means the region origin. +- Use a layer when several commands should render together into an isolated target and then composite back as one result. ## Clip Later Commands -`Save(DrawingOptions, params IPath[])` pushes a new state with the supplied options and clip paths. The clip paths are combined with each command by `ShapeOptions.BooleanOperation`. +`Save(DrawingOptions, params IPath[])` pushes a new state with the supplied options and clip paths. The clip paths are combined with each later command by `ShapeOptions.BooleanOperation`; they are not applied retroactively to commands already recorded. The default boolean operation is `Difference`, which subtracts the clip path. For ordinary "draw inside this shape" clipping, set `BooleanOperation.Intersection`. +Think of clipping as a state scope. Save the clipped state immediately before the work that needs it, then restore as soon as that work is complete. That keeps borders, labels, shadows, and diagnostics from being clipped accidentally. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -19,7 +25,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 260, Color.White.ToPixel()); -EllipsePolygon spotlight = new(new PointF(210, 130), new SizeF(300, 160)); +EllipsePolygon spotlight = new(210, 130, 300, 160); DrawingOptions clipInside = new() { ShapeOptions = new() @@ -48,6 +54,8 @@ Use `Restore()` to pop the latest state, or `RestoreTo(saveCount)` when nested s `CreateRegion(...)` creates a child canvas with local coordinates inside a rectangular area. It is useful for controls, panels, tiles, thumbnails, and other sub-layouts where `(0, 0)` should mean the region origin. +A region is a coordinate convenience, not an independent render target. The child canvas shares the parent replay timeline, and the root canvas still owns final replay. Disposing the child region closes that local drawing scope; it does not render the whole image immediately. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; @@ -68,13 +76,15 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` -Nested regions can also have their own saved state. The root canvas still owns final replay, so disposing a child region does not render the whole image immediately. +Nested regions can also have their own saved state. Use them when nested layout is clearer than constantly adding offsets to parent-canvas coordinates. ## Layers `SaveLayer(...)` starts an isolated compositing scope. Commands drawn inside the layer render into that layer, then `Restore()` composites the layer back to the parent with the supplied `GraphicsOptions`. -Layer bounds limit the isolated target and final composition area. They do not shift the coordinate system. +Layer bounds limit the isolated target and final composition area. They do not shift the coordinate system. Commands inside a bounded layer still use the same local coordinates as the parent canvas, so the layer bounds should describe the affected area, not a new origin. + +Use a layer when group behavior matters. Group opacity, group blending, and grouped masking are different from applying the same `GraphicsOptions` to each command independently. Without a layer, two semi-transparent shapes can blend with each other and the background one command at a time; with a layer, they first form one isolated result and then that result blends back once. ```csharp using SixLabors.ImageSharp; @@ -92,7 +102,7 @@ image.Mutate(ctx => ctx.Paint(canvas => _ = canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.55F }, new Rectangle(70, 46, 220, 128)); // Layer bounds constrain compositing; these coordinates are still parent coordinates. - canvas.Fill(Brushes.Solid(Color.OrangeRed), new EllipsePolygon(new PointF(180, 110), new SizeF(170, 96))); + canvas.FillEllipse(Brushes.Solid(Color.OrangeRed), new(180, 110), new(170, 96)); canvas.Draw(Pens.Solid(Color.White, 8), new Rectangle(96, 74, 168, 72)); canvas.Restore(); @@ -101,3 +111,11 @@ image.Mutate(ctx => ctx.Paint(canvas => ``` Use layers when a group of commands should blend back as one result. Without a layer, each command blends into the parent independently. + +## Practical Guidance + +- Use clips when geometry should constrain later commands in the current coordinate space. +- Use regions when a child layout should have its own local origin. +- Use layers when several commands should blend back as one grouped result. +- Remember that layer bounds constrain composition but do not move the coordinate system. +- Restore saved state as soon as the scoped work is complete. diff --git a/articles/imagesharp.drawing/gettingstarted.md b/articles/imagesharp.drawing/gettingstarted.md index 52ce057d7..03322fe33 100644 --- a/articles/imagesharp.drawing/gettingstarted.md +++ b/articles/imagesharp.drawing/gettingstarted.md @@ -1,19 +1,20 @@ # Getting Started ->[!NOTE] ->This guide assumes intermediate C# and .NET knowledge. If you are new to .NET, start with the language and runtime basics first, then come back to the image and drawing APIs. +ImageSharp.Drawing adds high-performance vector drawing, brush and pen styling, image composition, and text rendering to ImageSharp. It is designed for generated graphics where the image pipeline and drawing pipeline need to work together: badges, charts, thumbnails, watermarks, annotations, documents, server-side render output, and GPU-backed drawing targets. -ImageSharp.Drawing adds vector drawing, brush and pen styling, and text rendering to ImageSharp. The main workflow is: +The main workflow is: 1. Create or load an `Image`. 2. Call `Mutate(...)`. 3. Use [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) to receive a [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas). 4. Draw onto the canvas with brushes, pens, paths, shapes, images, or text. -The same canvas can mix all of those operations. This model scales from small badges to poster-style artwork, route maps, typography sheets, image masking, and WebGPU scenes. +The same canvas can mix all of those operations. The important idea is that drawing is recorded through [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) in the order you call it, then replayed into the current frame. That replay model lets the library share the same public drawing code across CPU images, retained backend scenes, and WebGPU targets. ## Draw a Shape +Start with geometry, then choose how it is painted. Built-in shapes such as [`StarPolygon`](xref:SixLabors.ImageSharp.Drawing.StarPolygon), [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon), and [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon) are reusable geometry objects. A brush fills the area covered by the shape, and a pen generates and fills the stroke outline. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -35,11 +36,13 @@ image.Mutate(ctx => ctx.Paint(canvas => image.Save("star.png"); ``` -[`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) creates a canvas for each frame being processed. Drawing is recorded through that canvas and applied when the paint operation runs. +[`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) creates a canvas for each frame being processed. Drawing is recorded through that canvas, and the canvas is disposed by the paint processor after your callback returns. That disposal step replays the recorded timeline into the frame. ## Combine Drawing Operations -Most real compositions combine background fills, path drawing, text, image drawing, clipping, and image processors. Keep the source images and brushes alive until the `Paint(...)` call has completed because the canvas records commands first and replays them later. +Most real compositions combine background fills, path drawing, text, image drawing, clipping, and image processors. Keep those concerns separate in the code: geometry decides where drawing can happen, brushes and pens decide how pixels are produced, text options decide layout, and canvas state decides which later commands are clipped, transformed, blended, or processed. + +Keep source images, image brushes, fonts, and reusable paths alive until the `Paint(...)` call has completed because the canvas records commands first and replays them later. The `Apply(...)` call in this example is also a replay barrier: it processes the pixels produced by earlier commands and does not include drawing that happens later. ```csharp using SixLabors.Fonts; @@ -60,7 +63,7 @@ RichTextOptions titleOptions = new(font) HorizontalAlignment = HorizontalAlignment.Center }; -EllipsePolygon focus = new(new PointF(320, 195), new SizeF(360, 190)); +EllipsePolygon focus = new(320, 195, 360, 190); RectangleF photoArea = new(80, 92, 480, 230); DrawingOptions clipToFocus = new() { @@ -90,7 +93,9 @@ image.Save("composition.png"); ## Use Drawing Options -[`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls the shared drawing state used by the canvas. The most common settings are graphics options for blending and antialiasing, shape options for fill behavior, and transforms for vector output. +[`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls the shared drawing state used by the canvas. It is not a brush, pen, or shape; it is the context used to interpret later commands. `GraphicsOptions` controls edge coverage and pixel composition, `ShapeOptions` controls fill and clip behavior, and `Transform` moves vector output from local coordinates into final canvas coordinates. + +Pass options to `Paint(...)` when the whole callback should use that state. Use `Save(options)` and `Restore()` when only part of the drawing should use it. ```csharp using System.Numerics; @@ -114,19 +119,20 @@ DrawingOptions options = new() Transform = new(Matrix3x2.CreateRotation(-0.18F, new(160, 100))) }; -EllipsePolygon shape = new(new PointF(160, 100), new SizeF(210, 96)); Brush brush = Brushes.Horizontal(Color.DeepSkyBlue, Color.Navy); image.Mutate(ctx => ctx.Paint(options, canvas => { - canvas.Fill(brush, shape); - canvas.Draw(Pens.Solid(Color.Black, 3), shape); + canvas.FillEllipse(brush, new(160, 100), new(210, 96)); + canvas.DrawEllipse(Pens.Solid(Color.Black, 3), new(160, 100), new(210, 96)); })); ``` ## Draw Text -Text drawing uses SixLabors.Fonts for font discovery, shaping, measurement, and layout. Use [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) when you draw directly to a canvas. +Text drawing uses SixLabors.Fonts for font discovery, shaping, measurement, and layout. Use [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) when you draw directly to a canvas. The options are the text layout contract: font, origin, wrapping, alignment, fallback, culture, and rich runs should be the same when measuring and drawing. + +Prefer layout options over manual width subtraction. Wrapping and alignment let the text engine account for line height, glyph metrics, shaping, and fallback fonts, which manual coordinate guesses cannot do reliably. ```csharp using SixLabors.Fonts; @@ -159,3 +165,10 @@ For deeper text guidance, see the [Fonts](../fonts/index.md) docs. - [Paths and Shapes](pathsandshapes.md) - [Brushes and Pens](brushesandpens.md) - [Drawing Text](text.md) + +## Practical Guidance + +- Keep reusable geometry, pens, brushes, fonts, and source images alive until `Paint(...)` completes. +- Create drawing options for the state you want to scope, then use `Save(...)` and `Restore()` around that scope. +- Use `Apply(...)` after the commands whose pixels should be processed. +- Move from primitive helpers to reusable paths when the same geometry drives more than one command. diff --git a/articles/imagesharp.drawing/imagesandprocessing.md b/articles/imagesharp.drawing/imagesandprocessing.md index dbe367040..6d68c50e4 100644 --- a/articles/imagesharp.drawing/imagesandprocessing.md +++ b/articles/imagesharp.drawing/imagesandprocessing.md @@ -1,12 +1,14 @@ # Images, Masks, and Processing -ImageSharp.Drawing can draw images through the canvas, use images as brushes, and run ImageSharp processors inside drawing regions. +ImageSharp.Drawing can draw images through the canvas, use images as brushes, and run ImageSharp processors inside drawing regions. These features look similar because they all produce pixels from images, but they model different intent. -Use `DrawImage(...)` when you want to place a rectangular source image into a rectangular destination. Use an image brush when the image should behave like a fill for arbitrary geometry. Use `Apply(...)` when you need normal ImageSharp processors to affect only the pixels covered by a rectangle or path. +Use `DrawImage(...)` when you want to place a rectangular source image into a rectangular destination. Use an image brush when the image should behave like a fill for arbitrary geometry. Use `Apply(...)` when you need normal ImageSharp processors to affect the pixels visible at a specific point in the canvas timeline. ## Draw an Image -`DrawImage(...)` copies a source rectangle from an image into a destination rectangle on the canvas. The destination is affected by the current transform and clip state. +`DrawImage(...)` samples a source rectangle from an image and places it into a destination rectangle on the canvas. The source rectangle is expressed in source-image coordinates. The destination rectangle is expressed in canvas coordinates and is affected by the current transform and clip state. + +The optional resampler is used when the selected source pixels have to be scaled into the destination. Bicubic is the default, so pass a sampler only when your output needs a different tradeoff such as sharper edges, smoother downsampling, or a specific product policy. ```csharp using SixLabors.ImageSharp; @@ -26,7 +28,7 @@ DrawingOptions clipInside = new() } }; -EllipsePolygon clip = new(new PointF(240, 150), new SizeF(340, 190)); +EllipsePolygon clip = new(240, 150, 340, 190); image.Mutate(ctx => ctx.Paint(canvas => { @@ -46,7 +48,9 @@ Choose the source rectangle in source-image coordinates and the destination rect ## Use an Image as a Brush -Use `ImageBrush` when an image should fill any path as a texture. This is different from `DrawImage(...)`: the brush samples image pixels while the supplied path controls coverage. +Use `ImageBrush` when an image should fill any path as a texture. This is different from `DrawImage(...)`: the brush supplies sampled image pixels, while the supplied path controls coverage. That makes image brushes useful for clipped portraits, textured text, patterned fills, masks, thumbnails inside arbitrary shapes, and repeated decorative elements. + +An image brush references its source image. Keep the source alive until canvas replay has completed. If the same image is reused by several brushes or commands, own that lifetime outside the `Paint(...)` callback rather than disposing it in the middle of drawing. ```csharp using SixLabors.ImageSharp; @@ -76,6 +80,8 @@ An image brush is best when the same texture should fill a shape, text path, or `Apply(...)` runs normal ImageSharp processors inside a rectangle, path, or path builder. It is a replay barrier: commands before it affect the pixels being processed, and commands after it do not. +That makes `Apply(...)` a timeline tool, not just a clipping tool. Put it immediately after the pixels that should be processed. Draw crisp outlines, labels, or foreground objects after the barrier so they are not blurred, pixelated, color-adjusted, or otherwise processed with the background. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -87,7 +93,7 @@ using Image source = Image.Load("photo.jpg"); using Image image = new(520, 320, Color.White.ToPixel()); RectangleF destination = new(40, 38, 440, 244); -EllipsePolygon redaction = new(new PointF(300, 168), new SizeF(150, 96)); +EllipsePolygon redaction = new(300, 168, 150, 96); image.Mutate(ctx => ctx.Paint(canvas => { @@ -102,3 +108,11 @@ image.Mutate(ctx => ctx.Paint(canvas => On GPU-backed canvases, `Apply(...)` requires the affected pixels to be read back, processed by the CPU pipeline, and written back before presentation. Keep regions as small as the effect allows. The placement of `Apply(...)` matters. Commands recorded before it contribute pixels to the processor input; commands recorded after it are drawn over the processed result. This makes it possible to blur or pixelate an image region, then draw a crisp outline or label on top. + +## Practical Guidance + +Use `DrawImage(...)` when an image should be sampled from a source rectangle and placed into a destination rectangle. Use an image brush when the image should behave like a fill pattern inside arbitrary geometry. Those two APIs can produce similar-looking results, but they model different intent: placement versus shading. + +The source image must remain alive until the canvas has replayed the command. In `Paint(...)`, that means through the end of the mutation pipeline. With manually-created canvases, it means until the canvas is disposed. Source rectangles are expressed in source-image coordinates; destination rectangles are expressed in canvas coordinates, so cropping and placement can be reasoned about independently. + +`Apply(...)` is a timeline decision. Put it exactly where the processor should observe the image: commands before it contribute pixels to the processor input, and commands after it draw over the processed result. Keep processor regions tight, especially on GPU-backed canvases where readback may be required. diff --git a/articles/imagesharp.drawing/index.md b/articles/imagesharp.drawing/index.md index be2cb5c12..00c428eb0 100644 --- a/articles/imagesharp.drawing/index.md +++ b/articles/imagesharp.drawing/index.md @@ -1,13 +1,11 @@ # Introduction ### What is ImageSharp.Drawing? -ImageSharp.Drawing is a library built on top of ImageSharp to provide 2D drawing extensions. +ImageSharp.Drawing is the high-performance 2D drawing layer for ImageSharp. It adds vector geometry, strokes, fills, text rendering, image composition, clipping, layers, and optional WebGPU-backed rendering while keeping the same cross-platform, managed-code deployment model as ImageSharp. -ImageSharp.Drawing is designed from the ground up to be high-performance, flexible, and extensible. It provides vector geometry, brush and pen styling, canvas drawing, image compositing, and text rendering building blocks for custom images. +The core model is deliberately small: geometry describes coverage, brushes and pens describe how pixels are produced, drawing options describe state, and [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) records ordered drawing work into a replay timeline. That makes the same drawing code useful for one-off image generation, templated graphics, server-side rendering, retained backend scenes, and GPU-backed output. -The core model is deliberately small: geometry describes coverage, brushes and pens describe how pixels are produced, drawing options describe state, and [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) records commands that are replayed into an ImageSharp processing pipeline. That makes the same drawing code useful for one-off image generation, templated graphics, server-side rendering, retained canvas scenes, and GPU-backed output. - -Read the articles as a progression. Start with the canvas workflow, learn the geometry and styling types, then move into text, image composition, transforms, and WebGPU when the job needs them. +Read the articles as a progression. Start with the canvas workflow because replay, state, and lifetime explain the rest of the API. Then learn geometry, brushes, pens, clipping, text, image composition, transforms, and WebGPU as separate pieces that combine into one drawing pipeline. ### Start Here @@ -20,7 +18,11 @@ Read the articles as a progression. Start with the canvas workflow, learn the ge - [Images, Masks, and Processing](imagesandprocessing.md) covers [`DrawImage(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawImage*), image brushes, clipping masks, and [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*). - [Transforms and Composition](transformsandcomposition.md) covers transforms, blending, alpha composition, and antialiasing. - [Drawing Text](text.md) covers [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), measuring, and text along paths. -- [WebGPU](webgpu.md) covers GPU-backed windows, external surfaces, and offscreen render targets. +- [WebGPU](webgpu.md) introduces GPU-backed drawing targets and links to the focused WebGPU pages. +- [WebGPU Environment and Support](webgpuenvironment.md) covers startup configuration, availability probes, compute-pipeline checks, and native error logging. +- [WebGPU Window Rendering](webgpuwindow.md) covers `WebGPUWindow`, frame loops, window state, framebuffer sizing, and presentation. +- [WebGPU External Surfaces](webgpuexternalsurface.md) covers `WebGPUExternalSurface`, native surface hosts, host-owned resize, and frame acquisition. +- [WebGPU Offscreen Render Targets](webgpurendertarget.md) covers `WebGPURenderTarget`, offscreen canvases, texture formats, and readback. - [Migrating from System.Drawing](migratingfromsystemdrawing.md) maps common GDI+ drawing concepts to ImageSharp.Drawing. - [Migrating from SkiaSharp](migratingfromskiasharp.md) maps common SkiaSharp drawing concepts to ImageSharp.Drawing. - [Recipes](recipes.md) provides copy-pasteable solutions for common drawing tasks. @@ -104,3 +106,10 @@ dotnet publish -p:SixLaborsLicenseKey="$SIXLABORS_LICENSE_KEY" ``` Build as normal after the file or property is configured. If the license is missing or invalid, the build fails with a clear error. You do not need to reference the licensing package directly; it is carried by Six Labors libraries. + +### How to Use These Docs + +- Start with the canvas model, because replay, state, and lifetime explain the rest of the API. +- Use paths and brushes pages when geometry and styling decisions are still unclear. +- Use text and image-processing pages when drawing must combine rich text, source images, clipping, and effects. +- Use WebGPU pages only when the output target genuinely benefits from GPU-backed rendering. diff --git a/articles/imagesharp.drawing/migratingfromskiasharp.md b/articles/imagesharp.drawing/migratingfromskiasharp.md index e40b2651f..17f7c461a 100644 --- a/articles/imagesharp.drawing/migratingfromskiasharp.md +++ b/articles/imagesharp.drawing/migratingfromskiasharp.md @@ -1,8 +1,10 @@ # Migrating from SkiaSharp -If you are coming from SkiaSharp, the biggest adjustment is the rendering model. SkiaSharp code is usually centered on an `SKCanvas` supplied by the destination you are drawing to: a bitmap, raster surface, GPU surface, document, or picture recorder. ImageSharp.Drawing works inside the ImageSharp processing pipeline and records drawing commands through [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) before replaying them to the active backend. +If you are coming from SkiaSharp, the biggest adjustment is the rendering model. SkiaSharp code is usually centered on an `SKCanvas` supplied by the destination you are drawing to: a bitmap, raster surface, GPU surface, document, or picture recorder. ImageSharp.Drawing works inside the ImageSharp processing pipeline and records ordered drawing work through [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) before replaying it to the active backend. -That difference is useful. The same drawing code can target normal CPU-backed images, retained scenes, and WebGPU-backed surfaces while keeping the same shape, brush, pen, text, and image composition model. +That difference is useful. The same drawing code can target normal CPU-backed images, retained backend scenes, and WebGPU-backed surfaces while keeping the same shape, brush, pen, text, and image composition model. + +Start by matching behavior, not by chasing the shortest code. Keep the same canvas size, rectangles, colors, alpha, stroke widths, transform order, clipping, font files, and image sampling choices while translating the drawing model. Once the output matches, ImageSharp.Drawing usually lets you simplify because geometry, styling, text layout, and image processing are expressed as separate concepts. ## Core Type Mapping @@ -13,8 +15,8 @@ That difference is useful. The same drawing code can target normal CPU-backed im | `SKPaint` fill | [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush), usually [`Brushes.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Brushes.Solid*), gradient brushes, image brushes, or pattern brushes | | `SKPaint` stroke | [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), usually [`Pens.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Solid*) or a custom `Pen` with stroke options | | `SKColor` | `Color`, or a concrete pixel type such as `Rgba32` when working directly with pixels | -| `SKRect` / `SKRoundRect` | `Rectangle`, `RectangleF`, and shape types such as [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon) | -| `SKPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shapes such as [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon) | +| `SKRect` / `SKRoundRect` | `Rectangle` for rectangle fill, stroke, and clear helpers; `RectangleF` for APIs that explicitly accept floating-point bounds such as image destination rectangles; shape types when geometry must be reused | +| `SKPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shapes when geometry must be reused | | `SKMatrix` | `Matrix4x4` transforms, commonly constructed from `Matrix3x2` | | `SKImageFilter` / `SKMaskFilter` | `Apply(...)` with ImageSharp processors for region-scoped effects | | `SKTextBlob` / text drawing | [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), [`TextBlock`](xref:SixLabors.Fonts.TextBlock), Fonts shaping, and [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) | @@ -23,12 +25,12 @@ That difference is useful. The same drawing code can target normal CPU-backed im In SkiaSharp, you draw through the `SKCanvas` provided by the current destination. A canvas backed by a raster bitmap or raster surface writes to pixels visible to the CPU. A GPU-surface canvas targets GPU work that is flushed or submitted later. A document or picture-recorder canvas records drawing commands instead of exposing writable pixels. +ImageSharp.Drawing gives you one ordered canvas API for these destination styles. Inside `Paint(...)`, the canvas records drawing work at that point in the ImageSharp pipeline and replays it into the active backend when the processor completes. + For simple bitmap code, that often looks like this: SkiaSharp: -SkiaSharp positions text by baseline. Offset by ascent when you want to match a top-left drawing origin. - ```csharp using SkiaSharp; @@ -109,17 +111,21 @@ using SixLabors.ImageSharp.Processing; image.Mutate(context => context.Paint(canvas => { - canvas.Fill(Brushes.Solid(Color.ParseHex("#2f80ed")), new RectangleF(48, 42, 280, 126)); - canvas.Draw(Pens.Solid(Color.ParseHex("#1b3f72"), 4), new RectangleF(48, 42, 280, 126)); + canvas.Fill(Brushes.Solid(Color.ParseHex("#2f80ed")), new Rectangle(48, 42, 280, 126)); + canvas.Draw(Pens.Solid(Color.ParseHex("#1b3f72"), 4), new Rectangle(48, 42, 280, 126)); })); ``` -This is usually the cleanest migration path: create brushes and pens where SkiaSharp code previously configured fill and stroke paints. +This is usually the cleanest migration path: create brushes and pens where SkiaSharp code previously configured fill and stroke paints. Avoid looking for a single `SKPaint` replacement. In ImageSharp.Drawing, fill style belongs to the brush, stroke geometry belongs to the pen, graphics state belongs to `DrawingOptions`, and text layout belongs to `RichTextOptions`. ## Paths and Shapes SkiaSharp path code usually builds an `SKPath`, then fills or strokes it. ImageSharp.Drawing uses [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) for incremental construction and [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath) for the finished geometry. +Preserve whether each figure is open or closed. Closed figures define fillable areas and produce closed stroke joins; open figures are usually stroked outlines where cap behavior is visible at the ends. If the original Skia path relies on winding for holes, keep the same winding model or explicitly choose an `IntersectionRule` that matches the original fill type. + +For direct migrations of simple `SKCanvas` calls, use the canvas helpers first. Rectangles use `Fill(brush, Rectangle)` and `Draw(pen, Rectangle)` overloads; ellipses, arcs, pies, lines, and Beziers have named helpers. Move to explicit shape objects when the same geometry is reused for fill, stroke, clipping, measurement, or composition. + SkiaSharp: ```csharp @@ -173,7 +179,7 @@ image.Mutate(context => context.Paint(canvas => })); ``` -For common geometry, prefer the built-in shapes instead of manually building paths: +For common one-off geometry, prefer the canvas helper that matches the original `SKCanvas` call: SkiaSharp: @@ -200,13 +206,18 @@ using SixLabors.ImageSharp.Processing; image.Mutate(context => context.Paint(canvas => { - canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), new EllipsePolygon(180, 120, 220, 96)); + // ImageSharp.Drawing ellipse helpers take center and size, not top-left bounds. + canvas.FillEllipse(Brushes.Solid(Color.MediumSeaGreen), new(180, 120), new(220, 96)); })); ``` +Use an explicit shape when the geometry is data, not just a drawing call. For example, an `EllipsePolygon` can be filled, stroked, clipped against, transformed, measured, or reused in several commands. + ## Transforms and Canvas State -SkiaSharp commonly uses `Save()`, `Restore()`, `Translate()`, `Scale()`, and `RotateDegrees()` on the canvas. ImageSharp.Drawing exposes the same idea through canvas state and `Matrix4x4` transforms. +SkiaSharp commonly uses `Save()`, `Restore()`, `Translate()`, `Scale()`, and `RotateDegrees()` on the canvas. ImageSharp.Drawing exposes the same scoped-state idea through `Save(...)`, `Restore()`, and `Matrix4x4` transforms. + +Translate the transform in the same order that Skia applied it. The saved state affects subsequent geometry, strokes, text, clips, and image placement until it is restored. For ordinary 2D affine transforms, construct the ImageSharp.Drawing transform from `Matrix3x2`; the resulting `Matrix4x4` keeps the public canvas model consistent across CPU, retained scene, and WebGPU targets. SkiaSharp: @@ -254,19 +265,21 @@ image.Mutate(context => context.Paint(canvas => }; _ = canvas.Save(options); - canvas.Fill(Brushes.Solid(Color.HotPink), new RectangleF(-70, -24, 140, 48)); - canvas.Draw(Pens.Solid(Color.White, 3), new RectangleF(-70, -24, 140, 48)); + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(-70, -24, 140, 48)); + canvas.Draw(Pens.Solid(Color.White, 3), new Rectangle(-70, -24, 140, 48)); canvas.Restore(); })); ``` -ImageSharp.Drawing uses `Matrix4x4` because the same transform model works across CPU rendering, retained scenes, and WebGPU output. For normal 2D drawing, construct it from `Matrix3x2` so the affine values stay familiar. +ImageSharp.Drawing uses `Matrix4x4` because the same transform model works across CPU rendering, retained backend scenes, and WebGPU output. For normal 2D drawing, construct it from `Matrix3x2` so the affine values stay familiar. ## Image Composition SkiaSharp image composition often uses `DrawImage(...)` or `DrawBitmap(...)`. In ImageSharp.Drawing, use [`DrawImage(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawImage*) inside [`Paint(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.PaintExtensions) when the operation belongs with the rest of the drawing commands. +Keep the source and destination rectangles explicit while migrating. The source rectangle is in source-image coordinates; the destination rectangle is in canvas coordinates after the current transform. If the old Skia code used a specific sampling option, choose the matching ImageSharp resampler. Otherwise, the drawing API default is appropriate for normal resized placement. + SkiaSharp: ```csharp @@ -303,11 +316,11 @@ using Image output = new(640, 360, Color.White.ToPixel()); output.Mutate(context => context.Paint(canvas => { canvas.DrawImage(source, source.Bounds, new RectangleF(32, 32, 320, 220)); - canvas.Draw(Pens.Solid(Color.White, 4), new RectangleF(32, 32, 320, 220)); + canvas.Draw(Pens.Solid(Color.White, 4), new Rectangle(32, 32, 320, 220)); })); ``` -Keep source images alive until the canvas has replayed. Inside `Paint(...)`, replay is owned by the processing operation. If you create and manage a canvas yourself, dispose or flush it before disposing source images used by drawing commands. +Keep source images alive until the canvas has replayed. Inside `Paint(...)`, replay is owned by the processing operation. If you create and manage a canvas yourself, dispose it before disposing source images used by drawing commands. ## Region Effects @@ -372,6 +385,8 @@ On GPU-backed canvases, `Apply(...)` may require readback into the CPU ImageShar SkiaSharp text drawing can start simple, but richer layout usually involves `SKTextBlob`, font managers, shaping, and manual measurement. ImageSharp.Drawing uses SixLabors.Fonts directly, so advanced text layout is part of the normal drawing API. +The most common positioning difference is baseline versus layout origin. Skia's simple `DrawText(...)` overloads position text by baseline. ImageSharp.Drawing positions text through `RichTextOptions`, where `Origin` is interpreted by the chosen alignment and wrapping settings. When you need a top-left equivalent for Skia baseline code, account for font metrics during migration, then prefer `RichTextOptions` alignment once the output is confirmed. + SkiaSharp: ```csharp @@ -430,3 +445,11 @@ For most SkiaSharp migrations: 8. Move text code to [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), [`TextBlock`](xref:SixLabors.Fonts.TextBlock), and the Fonts layout APIs when measurement or wrapping matters. You do not need to migrate everything at once. ImageSharp.Drawing is usually easiest to adopt by moving one rendering workflow at a time: generate the same output image, replace the paint/path/text concepts with the closest Drawing equivalents, then simplify once the new model is in place. + +## Practical Guidance + +- Keep examples behavior-equivalent while migrating; change API shape first, then simplify. +- Move bitmap processing to core ImageSharp and canvas drawing to ImageSharp.Drawing. +- Replace mutable paint objects with explicit brushes, pens, and drawing options. +- Use saved canvas state for transforms, clipping, and scoped graphics options. +- Verify text output with real fonts and wrapping because SkiaSharp and Fonts use different layout models. diff --git a/articles/imagesharp.drawing/migratingfromsystemdrawing.md b/articles/imagesharp.drawing/migratingfromsystemdrawing.md index 2ee1dae4a..17b278d90 100644 --- a/articles/imagesharp.drawing/migratingfromsystemdrawing.md +++ b/articles/imagesharp.drawing/migratingfromsystemdrawing.md @@ -4,6 +4,8 @@ If you are coming from `System.Drawing`, the biggest adjustment is moving from a The drawing concepts still map cleanly. `Graphics` becomes [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas), `Brush` and `Pen` become ImageSharp.Drawing brushes and pens, `GraphicsPath` becomes [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) or [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and text moves to the Fonts-powered [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) APIs. +Treat migration as a behavior-matching exercise first. Keep the same image size, geometry, colors, alpha, transform order, clipping behavior, and font choice while translating the API shape. Once the output is equivalent, simplify the ImageSharp.Drawing code to use higher-level shapes, text layout, and image processing where they make the intent clearer. + For core image loading, saving, pixel formats, and raw pixel access, see the ImageSharp [Migrating from System.Drawing](../imagesharp/migratingfromsystemdrawing.md) guide. This page focuses on drawing code. ## Core Type Mapping @@ -15,8 +17,8 @@ For core image loading, saving, pixel formats, and raw pixel access, see the Ima | `System.Drawing.Color` | `SixLabors.ImageSharp.Color`, or a concrete pixel type such as `Rgba32` | | `SolidBrush` / `TextureBrush` | `Brushes.Solid(...)`, image brushes, pattern brushes, gradient brushes | | `Pen` | [`SixLabors.ImageSharp.Drawing.Processing.Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), usually through [`Pens.Solid(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Pens.Solid*) | -| `Rectangle` / `RectangleF` | `Rectangle` / `RectangleF` | -| `GraphicsPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shape types | +| `Rectangle` / `RectangleF` | `Rectangle` for rectangle fill, stroke, and clear helpers; `RectangleF` for APIs that explicitly accept floating-point bounds such as image destination rectangles | +| `GraphicsPath` | [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Path`](xref:SixLabors.ImageSharp.Drawing.Path), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), and built-in shape types when geometry must be reused | | `Matrix` | `Matrix4x4`, commonly constructed from `Matrix3x2` | | `Graphics.DrawImage(...)` | `DrawingCanvas.DrawImage(...)` | | `Graphics.DrawString(...)` | [`DrawingCanvas.DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*) with [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) | @@ -56,11 +58,13 @@ image.Mutate(context => context.Paint(canvas => })); ``` -Use `Mutate(...)` when you want to update an image in place. Use `Clone(...)` when the old code created a separate output bitmap while keeping the source unchanged. +Use `Mutate(...)` when you want to update an image in place. Use `Clone(...)` when the old code created a separate output bitmap while keeping the source unchanged. The `Paint(...)` processor owns the canvas lifetime for this common case: commands recorded inside the callback are replayed into the image at the correct point in the ImageSharp processing pipeline. ## Brushes and Pens -`System.Drawing` separates filled shapes and stroked outlines through `Brush` and `Pen`. ImageSharp.Drawing keeps the same mental model, but uses its own brush and pen types. +`System.Drawing` separates filled shapes and stroked outlines through `Brush` and `Pen`. ImageSharp.Drawing keeps the same drawing vocabulary, but the objects belong to the ImageSharp.Drawing pipeline rather than the GDI+ object model. + +A brush supplies color, gradient, pattern, or image samples for covered pixels. A pen describes how to turn a source line, path, or shape into stroke geometry: width, dash pattern, joins, caps, and miter behavior all affect that generated outline. The generated outline is then filled by the pen brush. That distinction matters when migrating dashed strokes, image-filled outlines, or paths where cap and join behavior changes the visible shape. System.Drawing: @@ -84,14 +88,16 @@ using SixLabors.ImageSharp.Processing; image.Mutate(context => context.Paint(canvas => { - canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32(47, 128, 237, 255))), new RectangleF(48, 42, 280, 126)); - canvas.Draw(Pens.Solid(Color.FromPixel(new Rgba32(27, 63, 114, 255)), 4), new RectangleF(48, 42, 280, 126)); + canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32(47, 128, 237, 255))), new Rectangle(48, 42, 280, 126)); + canvas.Draw(Pens.Solid(Color.FromPixel(new Rgba32(27, 63, 114, 255)), 4), new Rectangle(48, 42, 280, 126)); })); ``` ## Paths and Shapes -`GraphicsPath` maps to [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) when you are constructing custom geometry. For simple rectangles, ellipses, arcs, and lines, prefer the built-in Drawing helpers and shape types. +`GraphicsPath` maps to [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder) when you are constructing custom geometry. Build the path in the same coordinate space as the original `GraphicsPath`, then fill or stroke it with ImageSharp.Drawing brushes and pens. + +Keep open and closed figures deliberate. A closed figure represents an area boundary, so fill rules, joins, and holes are part of the shape contract. An open figure is usually a stroke path, where caps and joins define the visible ends and corners. For direct migrations of simple rectangles, ellipses, arcs, pies, lines, and Beziers, prefer the canvas helpers. Rectangles use `Fill(brush, Rectangle)` and `Draw(pen, Rectangle)` overloads; ellipses, arcs, pies, lines, and Beziers have named helpers. Use shape objects such as `EllipsePolygon`, `RectanglePolygon`, or custom paths when the geometry is reused for fill, stroke, clipping, measurement, or composition. System.Drawing: @@ -135,7 +141,7 @@ image.Mutate(context => context.Paint(canvas => })); ``` -For common geometry, use shape types directly: +For common one-off geometry, use the canvas helpers that match the `Graphics` method you are replacing: System.Drawing: @@ -157,15 +163,19 @@ using SixLabors.ImageSharp.Processing; image.Mutate(context => context.Paint(canvas => { - // EllipsePolygon takes center and size; this matches the System.Drawing bounds above. - canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), new EllipsePolygon(180, 120, 220, 96)); + // ImageSharp.Drawing ellipse helpers take center and size, not top-left bounds. + canvas.FillEllipse(Brushes.Solid(Color.MediumSeaGreen), new(180, 120), new(220, 96)); })); ``` +Use an explicit polygon when the ellipse is part of the drawing model rather than a one-off command. For example, clipping needs an `IPath`, so `new EllipsePolygon(...)` is the right shape for the clipping example below. + ## Transforms and Canvas State `System.Drawing.Graphics` stores transform state on the `Graphics` object. ImageSharp.Drawing stores transform state in [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions), which can be saved onto the canvas state stack. +Translate transform code by preserving operation order. The transformed coordinate system affects subsequent fills, strokes, text, clips, and image placement until the saved state is restored. ImageSharp.Drawing uses `Matrix4x4` for canvas state so the same model can represent 2D affine and projective transforms across CPU and GPU backends; for normal migration work, build the value from `Matrix3x2` so the six affine numbers stay familiar. + System.Drawing: ```csharp @@ -201,8 +211,8 @@ image.Mutate(context => context.Paint(canvas => }; _ = canvas.Save(options); - canvas.Fill(Brushes.Solid(Color.HotPink), new RectangleF(-70, -24, 140, 48)); - canvas.Draw(Pens.Solid(Color.White, 3), new RectangleF(-70, -24, 140, 48)); + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(-70, -24, 140, 48)); + canvas.Draw(Pens.Solid(Color.White, 3), new Rectangle(-70, -24, 140, 48)); canvas.Restore(); })); @@ -214,6 +224,8 @@ ImageSharp.Drawing uses `Matrix4x4` for canvas transforms so the same drawing st If your `System.Drawing` code uses `Graphics.DrawImage(...)`, use `DrawImage(...)` inside `Paint(...)` when the image placement belongs with the rest of the drawing commands. +Keep source and destination rectangles explicit. The source rectangle selects pixels from the input image; the destination rectangle defines where those pixels land on the canvas. If you do not pass a resampler, ImageSharp.Drawing uses the drawing API default, which is the right choice for ordinary image placement. Choose a specific resampler only when the migration requires a known sampling policy. + System.Drawing: ```csharp @@ -244,16 +256,18 @@ using Image output = new(640, 360, Color.White.ToPixel()); output.Mutate(context => context.Paint(canvas => { canvas.DrawImage(source, source.Bounds, new RectangleF(32, 32, 320, 220)); - canvas.Draw(Pens.Solid(Color.White, 4), new RectangleF(32, 32, 320, 220)); + canvas.Draw(Pens.Solid(Color.White, 4), new Rectangle(32, 32, 320, 220)); })); ``` -Keep source images alive until the canvas has replayed. Inside `Paint(...)`, replay is owned by the processing operation. If you create and manage a canvas yourself, dispose or flush it before disposing source images used by drawing commands. +Keep source images alive until the canvas has replayed. Inside `Paint(...)`, replay is owned by the processing operation. If you create and manage a canvas yourself, dispose it before disposing source images used by drawing commands. ## Clipping `Graphics.SetClip(...)` maps to saving canvas state with clip paths. Restore the state when the clipped drawing is complete. +For equivalent `SetClip(...)` behavior, use `BooleanOperation.Intersection`. ImageSharp.Drawing clip paths are combined through [`ShapeOptions.BooleanOperation`](xref:SixLabors.ImageSharp.Drawing.Processing.ShapeOptions.BooleanOperation), and the default operation is not the same as intersecting the current drawing area with the supplied clip. + System.Drawing: ```csharp @@ -279,7 +293,16 @@ using SixLabors.ImageSharp.Processing; image.Mutate(context => context.Paint(canvas => { - _ = canvas.Save(new DrawingOptions(), new EllipsePolygon(190, 120, 260, 160)); + DrawingOptions clipInside = new() + { + ShapeOptions = new() + { + BooleanOperation = BooleanOperation.Intersection + } + }; + + // SetClip-style behavior keeps only the intersection with the ellipse. + _ = canvas.Save(clipInside, new EllipsePolygon(190, 120, 260, 160)); canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(20, 20, 360, 200)); canvas.Restore(); @@ -290,6 +313,8 @@ image.Mutate(context => context.Paint(canvas => `Graphics.DrawString(...)` handles simple text drawing. ImageSharp.Drawing uses SixLabors.Fonts through `DrawText(...)`, so wrapping, alignment, shaping, fallback, and rich text options are part of the normal text pipeline. +Use the same font file and layout rectangle when checking output parity. `RichTextOptions.Origin` is the anchor used by the layout options, `WrappingLength` defines the available line width, `TextAlignment` aligns lines within that wrapping width, and `HorizontalAlignment` / `VerticalAlignment` place the laid-out block relative to the origin. This keeps text positioning declarative instead of relying on manual string measurement. + System.Drawing: ```csharp @@ -353,3 +378,11 @@ For most `System.Drawing` drawing migrations: 8. Replace `DrawString(...)` with [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*), [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), and the Fonts layout APIs when wrapping or shaping matters. You do not have to migrate all drawing code at once. Start with one rendering workflow, match the output, then simplify the code once the ImageSharp.Drawing model is in place. + +## Practical Guidance + +- Keep source and destination geometry equivalent while translating examples. +- Replace `Graphics` state with explicit canvas `Save(...)` and `Restore()` scopes. +- Use ImageSharp.Drawing brushes and pens instead of carrying `System.Drawing` object lifetimes across. +- Move text layout decisions into `RichTextOptions` and Fonts APIs rather than manually positioning strings. +- Validate output on non-Windows environments if the migration goal is cross-platform rendering. diff --git a/articles/imagesharp.drawing/pathsandshapes.md b/articles/imagesharp.drawing/pathsandshapes.md index 108fc2ea6..0bb0162ac 100644 --- a/articles/imagesharp.drawing/pathsandshapes.md +++ b/articles/imagesharp.drawing/pathsandshapes.md @@ -1,6 +1,6 @@ # Paths and Shapes -ImageSharp.Drawing separates geometry from painting. Shapes and paths describe where drawing happens; brushes and pens describe how pixels are shaded. +ImageSharp.Drawing separates geometry from painting. Shapes and paths describe where drawing happens; brushes and pens describe how pixels are shaded. Keeping that split clear makes drawing code easier to reuse: the same path can be filled, stroked, clipped, measured, transformed, used as a text baseline, or combined with other paths without duplicating the styling code. The core geometry types are: @@ -16,7 +16,9 @@ The core geometry types are: ## Built-In Shapes -Built-in shape types are closed paths. They can be filled directly and stroked with a pen. +Built-in shape types are closed paths with a clear geometric meaning. Use them when the shape is part of the drawing model, not just a one-off primitive call. For example, an ellipse object can be reused for fill, stroke, clipping, hit testing, and layout bounds, while a primitive `DrawEllipse(...)` call only records that one drawing command. + +The shape constructors use the coordinate model of the shape itself. Rectangle-like shapes use a position and size. Ellipses, regular polygons, stars, and pies are normally expressed from a center point plus radii or size. If a translated example looks offset, check whether the source API used top-left bounds while the ImageSharp.Drawing shape expects a center. ```csharp using SixLabors.ImageSharp; @@ -27,9 +29,9 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 260, Color.White.ToPixel()); -EllipsePolygon ellipse = new(new PointF(120, 110), new SizeF(160, 96)); +EllipsePolygon ellipse = new(120, 110, 160, 96); StarPolygon star = new(x: 292, y: 128, prongs: 7, innerRadii: 34, outerRadii: 72); -PiePolygon pie = new(new PointF(120, 202), new SizeF(120, 86), startAngle: -30, sweepAngle: 245); +PiePolygon pie = new(120, 202, radiusX: 120, radiusY: 86, startAngle: -30, sweepAngle: 245); image.Mutate(ctx => ctx.Paint(canvas => { @@ -46,7 +48,7 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Open and Closed Paths -Open paths are useful for strokes, polylines, and curved baselines. Closed paths enclose an area and are the normal input for fills. +Open paths are useful for strokes, polylines, and curved baselines. Closed paths enclose an area and are the normal input for fills. The distinction affects both fill behavior and stroke joins: a closed figure has a final join between the last and first segment, while an open figure has start and end caps. [`Path`](xref:SixLabors.ImageSharp.Drawing.Path) is open by default. [`Polygon`](xref:SixLabors.ImageSharp.Drawing.Polygon) is closed. [`PathBuilder.CloseFigure()`](xref:SixLabors.ImageSharp.Drawing.PathBuilder.CloseFigure) closes the current figure before starting the next one. @@ -169,7 +171,7 @@ Polygon outer = new( new PointF(60, 204) ]); -EllipsePolygon hole = new(new PointF(210, 120), new SizeF(178, 96)); +EllipsePolygon hole = new(210, 120, 178, 96); ComplexPolygon complex = new(outer, hole); DrawingOptions options = new() @@ -229,7 +231,7 @@ using SixLabors.ImageSharp.Processing; using Image image = new(420, 240, Color.White.ToPixel()); -EllipsePolygon subject = new(new PointF(190, 120), new SizeF(260, 154)); +EllipsePolygon subject = new(190, 120, 260, 154); StarPolygon cutout = new(x: 226, y: 120, prongs: 6, innerRadii: 38, outerRadii: 82); ShapeOptions clipOptions = new() @@ -298,8 +300,8 @@ using SixLabors.ImageSharp.Processing; using Image image = new(360, 220, Color.White.ToPixel()); -EllipsePolygon outer = new(new PointF(180, 110), new SizeF(260, 150)); -EllipsePolygon inner = new(new PointF(180, 110), new SizeF(126, 76)); +EllipsePolygon outer = new(180, 110, 260, 150); +EllipsePolygon inner = new(180, 110, 126, 76); PathCollection shape = new(outer, inner); DrawingOptions options = new() @@ -318,3 +320,11 @@ image.Mutate(ctx => ctx.Paint(options, canvas => ``` For lower-level polygon boolean operations, see [PolygonClipper](../polygonclipper/index.md). + +## Practical Guidance + +Use primitive helpers when geometry exists only for one command. Move to path and polygon objects when geometry becomes part of the model: the same shape is filled, stroked, clipped, transformed, measured, or shared between commands. That makes the relationship between layout and painting explicit. + +Build closed paths deliberately when the shape represents an area. Filling an open path can work because the path is closed for fill processing, but a deliberately closed figure communicates intent and gives stroke joins closed-contour behavior. Use `ComplexPolygon` when multiple contours should be interpreted together as one region, especially when holes are involved. + +The fill rule is part of the geometry contract. `NonZero` is the default and matches normal SVG and web canvas expectations, where winding direction is meaningful. Use `EvenOdd` when contour direction should not matter and nested contours should alternate inside/outside status. diff --git a/articles/imagesharp.drawing/primitives.md b/articles/imagesharp.drawing/primitives.md index 349137a6b..c02966303 100644 --- a/articles/imagesharp.drawing/primitives.md +++ b/articles/imagesharp.drawing/primitives.md @@ -1,14 +1,16 @@ # Primitive Drawing Helpers -Primitive helpers are convenience methods on [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) for common geometry. Use them when the shape is simple and you do not need to keep an [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath) instance around. +Primitive helpers are convenience methods on [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) for common one-off geometry. They let you draw rectangles, ellipses, lines, Beziers, arcs, and pies without first creating a reusable [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath) object. The helpers still follow the same rules as path drawing: fills use brushes, strokes use pens, [`DrawingOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingOptions) controls antialiasing and transforms, and active canvas state applies to the recorded command. -Primitive calls record a drawing command immediately. They are a good fit for marks, guides, simple badges, outlines, and other geometry that is only used once. If the same geometry must be filled, stroked, clipped, transformed, measured, or shared between commands, create a path or polygon object instead. +Primitive calls append drawing intent to the canvas as soon as you call them. They are a good fit for marks, guides, simple badges, outlines, and other geometry that is only used once. If the same geometry must be filled, stroked, clipped, transformed, measured, passed to text layout, or shared between commands, create a path or polygon object instead so the geometry becomes explicit. ## Rectangles, Ellipses, Lines, and Beziers -Rectangle helpers use a top-left coordinate plus width and height. Ellipse helpers use a center point plus size, matching [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon). Lines and Beziers use explicit points in canvas coordinates, so they are easy to combine with image-space measurements. +Rectangle drawing is handled by rectangle-specific overloads: `Fill(brush, Rectangle)`, `Draw(pen, Rectangle)`, and `Clear(brush, Rectangle)`. There are no `FillRectangle(...)`, `DrawRectangle(...)`, or `ClearRectangle(...)` methods on `DrawingCanvas`. Ellipse helpers use `FillEllipse(...)` and `DrawEllipse(...)` with a center point plus size, matching [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon). Lines and Beziers use explicit points in canvas coordinates, so they are easy to combine with image-space measurements. + +Those coordinate conventions matter when translating from other libraries. Rectangle APIs usually describe a box from its top-left corner; ellipse, arc, and pie helpers describe an ellipse frame from its center. If the values look visually shifted, check whether the source API used top-left ellipse bounds while the Drawing helper expects a center. ```csharp using SixLabors.ImageSharp; @@ -47,7 +49,7 @@ Use the rectangle and ellipse helpers when the geometry exists only for that com Arc and pie helpers take a center point, a size, a rotation angle, a start angle, and a sweep angle. Positive and negative sweeps are both valid, which makes clockwise and counter-clockwise segments easy to express. -Arc helpers draw or fill the curved segment of an ellipse. Pie helpers close the segment back to the center, creating a wedge. Use [`PiePolygon`](xref:SixLabors.ImageSharp.Drawing.PiePolygon) when the wedge is part of reusable geometry. +Arc helpers describe the curved segment of an ellipse. Pie helpers close the segment back to the center, creating a wedge. Use arcs for gauges, rings, callouts, and curved marks. Use pies for chart slices, radial badges, and wedge-shaped fills. Use [`PiePolygon`](xref:SixLabors.ImageSharp.Drawing.PiePolygon) when the wedge is part of reusable geometry. ```csharp using SixLabors.ImageSharp; @@ -92,3 +94,10 @@ Use [`PathBuilder`](xref:SixLabors.ImageSharp.Drawing.PathBuilder), [`Polygon`]( - measure bounds, length, or area before drawing. The primitive helpers are best for direct one-off drawing. Paths are better when the geometry is part of the model. + +## Practical Guidance + +- Use primitive helpers for direct marks, guides, and simple one-off geometry. +- Switch to paths or polygons when the same geometry is filled, stroked, clipped, measured, or transformed. +- Remember that rectangle helpers use top-left coordinates while ellipse, arc, and pie helpers use center and size. +- Use built-in shape types when the geometry becomes part of your application model. diff --git a/articles/imagesharp.drawing/recipes.md b/articles/imagesharp.drawing/recipes.md index e7f84e922..141209d98 100644 --- a/articles/imagesharp.drawing/recipes.md +++ b/articles/imagesharp.drawing/recipes.md @@ -4,6 +4,10 @@ These pages are the quick-start side of the ImageSharp.Drawing docs. They focus Each recipe is intentionally complete enough to show the shape of a real workflow: create or load an image, set up reusable drawing objects outside the canvas callback, record the drawing commands, then save the result. After using a recipe, follow the related conceptual pages to understand the canvas state, lifetime, and composition behavior behind it. +Drawing recipes are easiest to adapt when you keep three things separate: geometry, styling, and canvas state. Geometry decides where drawing can happen. Brushes, pens, and text options decide what is drawn. Canvas state decides which later commands are transformed, clipped, layered, or processed. + +When adapting a recipe, change one of those layers at a time. If the layout is wrong, inspect the geometry and coordinate system before changing brushes. If the colors or texture are wrong, inspect the brush or pen before changing the shape. If later drawing is unexpectedly clipped, blurred, transformed, or transparent, inspect the canvas state scope around `Save(...)`, `Restore()`, `SaveLayer(...)`, and `Apply(...)`. + ## Common Tasks - [Add a Text Watermark](watermark.md) for anchored, semi-transparent text over an image. @@ -18,6 +22,16 @@ Each recipe is intentionally complete enough to show the shape of a real workflo - Create reusable geometry before the callback when more than one command needs the same shape. - Use `Save(...)` and `Restore()` when a clip, transform, or drawing option should affect only part of the recipe. - Put `Apply(...)` after the drawing commands that should be processed and before any crisp outlines or labels. +- Choose final output dimensions before positioning text, watermarks, badges, or annotations. +- Prefer `RichTextOptions` alignment and wrapping over manual string measurements. + +## Practical Guidance + +Drawing recipes become easier to maintain when the drawing model stays explicit. Keep source images, image brushes, fonts, paths, and retained backend scenes alive until canvas replay has completed. With `Paint(...)`, that means until the processing callback has finished; with manually-created canvases, it means until the root canvas is disposed. + +Use state scopes to make composition readable. `Save(...)` is for clipping, transforms, and options that affect later commands. `SaveLayer(...)` is for a group that should blend back as one result. `Apply(...)` is a timeline barrier for ImageSharp processors, so place it after the pixels that should be processed and before crisp outlines or labels. Effects such as blur need expanded processing regions because the result spreads outside the original shape. + +For text-heavy recipes, prefer layout options over guessed coordinates. Wrapping, horizontal alignment, vertical alignment, and text alignment keep examples robust when labels change, localize, or use fallback fonts. ## Related Topics diff --git a/articles/imagesharp.drawing/softshadow.md b/articles/imagesharp.drawing/softshadow.md index c2f18d4e3..185d31a12 100644 --- a/articles/imagesharp.drawing/softshadow.md +++ b/articles/imagesharp.drawing/softshadow.md @@ -4,6 +4,12 @@ Draw the shadow shape first, then apply a blur to the shadow region before drawi The blur region should be larger than the original shadow shape. Gaussian blur spreads pixels outward, so using the exact shape bounds clips the soft edge. A good rule of thumb is to expand the processing rectangle by at least the blur radius on every side. +The order is what makes the effect work. First draw the shadow pixels. Then use `Apply(...)` to blur only the region that contains the shadow. Then draw the crisp foreground panel. If the panel were drawn before the blur, it would be blurred with the shadow. If the blur region were too small, the soft edge would be cut off. + +Use `Apply(...)` for this pattern when a normal ImageSharp processor should affect an already-recorded part of the drawing. Use `SaveLayer(...)` for a different problem: grouping several commands so the group composites back to the parent as one result. + +A shadow is usually easier to reason about as three separate decisions: the shadow geometry, the blur processing area, and the foreground geometry. The shadow geometry is often the foreground shape offset by a few pixels. The blur processing area should cover the shadow plus the blur spread. The foreground geometry should be drawn after the blur so it remains sharp. + ```csharp using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -32,7 +38,7 @@ image.Mutate(ctx => ctx.Paint(canvas => image.Save("shadow.png"); ``` -Keep the blur region tight. On CPU canvases this reduces the amount of image data processed, and on GPU-backed canvases it reduces readback and upload work. +Keep the blur region tight. On CPU canvases this reduces the amount of image data processed, and on GPU-backed canvases it reduces readback and upload work. If you need a shadow around a complex path, use the path bounds expanded by the blur radius as the processing region, then draw the foreground path after the barrier. Use `SaveLayer(...)` instead when the foreground and shadow need to be composed as one group over existing content. Use `Apply(...)` when you want a normal ImageSharp processor, such as blur, pixelation, or color adjustment, to affect only part of the drawing timeline. @@ -41,3 +47,7 @@ Use `SaveLayer(...)` instead when the foreground and shadow need to be composed - [Canvas Drawing](canvas.md) - [Images, Masks, and Processing](imagesandprocessing.md) - [Transforms and Composition](transformsandcomposition.md) + +## Practical Guidance + +Expand blur regions so feathered pixels are not clipped, but keep them as small as the effect allows. Draw crisp foreground content after the blur barrier. Use layers instead when the shadow and foreground must compose as one group. diff --git a/articles/imagesharp.drawing/text.md b/articles/imagesharp.drawing/text.md index a4b07db05..1ef886124 100644 --- a/articles/imagesharp.drawing/text.md +++ b/articles/imagesharp.drawing/text.md @@ -8,6 +8,8 @@ At the simple end, text is one call. At the advanced end, the same model can dra ## Draw Simple Text +Simple text drawing still uses the full Fonts shaping pipeline. The text is shaped, positioned from `RichTextOptions.Origin`, and then painted through the same brush and pen model as other canvas drawing. Pass a brush to fill glyphs, a pen to outline glyphs, or both when the text needs a filled face and a stroked edge. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -30,7 +32,7 @@ image.Mutate(ctx => ctx.Paint(canvas => })); ``` -Pass a brush to fill glyphs, a pen to outline glyphs, or both. +Even in simple examples, treat `RichTextOptions` as part of the drawing contract. If you later measure the same string, use the same font, wrapping, alignment, culture, fallback, and feature settings so the measured layout matches the rendered pixels. ## Draw Rich Text @@ -128,7 +130,7 @@ RichTextOptions options = new(font) TextBlock block = new("Prepared text can be measured and drawn with the same shaping.", options); TextMetrics metrics = block.Measure(wrappingLength: 520); -RectanglePolygon layoutBox = new(60, 48, 520, metrics.Advance.Height + 24); +Rectangle layoutBox = new(60, 48, 520, (int)MathF.Ceiling(metrics.Advance.Height + 24)); image.Mutate(ctx => ctx.Paint(canvas => { @@ -150,6 +152,8 @@ The line-local enumerator is the right fit for text that flows through different [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions) inherits the core Fonts text options and adds ImageSharp.Drawing-specific rich text behavior. +Wrapping and alignment happen before pixels are drawn. `WrappingLength` determines where line breaking can happen. `TextAlignment` aligns lines within the paragraph. `HorizontalAlignment` and `VerticalAlignment` position the laid-out paragraph relative to `Origin`. Keeping those roles separate avoids the common mistake of manually subtracting measured widths and then fighting wrapped or fallback text. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -219,7 +223,9 @@ image.Mutate(ctx => ctx.Paint(canvas => ## Draw Text Along a Path -Text can also follow an [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath). +Text can also follow an [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath). In this mode the path acts as the text baseline, so path direction matters: reversing the path reverses the flow direction. Use open paths for natural baselines. Closed shapes can work, but they should be chosen deliberately because the baseline continues around the contour. + +Path text is still shaped text. The font, runs, fallback, culture, and decoration options come from the text options; the path only changes where the shaped glyphs are placed. ```csharp using SixLabors.Fonts; @@ -257,6 +263,8 @@ image.Mutate(ctx => ctx.Paint(canvas => Use `TextBuilder.GeneratePaths(...)` when the glyph outlines themselves should become drawing geometry. The returned paths can be filled, stroked, used as clips, or combined with image drawing. +Generating paths changes the problem from text layout to geometry. Once glyph outlines become paths, they can be clipped, filled with image brushes, stroked with pens, transformed, or combined with other paths. Use this when text is part of a graphic effect or mask. Use `DrawText(...)` when you simply want text rendered as text. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -298,3 +306,11 @@ image.Mutate(ctx => ctx.Paint(canvas => canvas.Draw(Pens.Solid(Color.White, 2), letters); })); ``` + +## Practical Guidance + +Use `RichTextOptions` as the drawing contract for canvas text. If text is measured before it is drawn, the measurement and drawing passes should use the same font, origin model, wrapping length, alignment, culture, fallback, feature tags, and text runs. Otherwise the final pixels can differ from the measured result even when the string is identical. + +Prefer the layout options over manual coordinate math. Centering text in a region is a layout problem: set the origin to the region anchor, specify wrapping, and use horizontal, vertical, and text alignment so the layout engine accounts for line height, wrapping, shaping, and fallback metrics. Manual width subtraction is fragile as soon as the string localizes, wraps, or uses a fallback face. + +Style ranges and placeholders use grapheme-indexed `[start, end)` ranges. This matters for emoji, combining marks, complex scripts, and any text where one visible unit is not one UTF-16 `char`. Use `TextBlock` when the same shaped text needs to be measured, inspected, hit-tested, or rendered repeatedly. Use generated text paths when text becomes geometry for fills, clips, strokes, or masks. diff --git a/articles/imagesharp.drawing/transformsandcomposition.md b/articles/imagesharp.drawing/transformsandcomposition.md index 42b49c402..4a9e5d2ef 100644 --- a/articles/imagesharp.drawing/transformsandcomposition.md +++ b/articles/imagesharp.drawing/transformsandcomposition.md @@ -4,9 +4,13 @@ The safest way to think about transforms and composition is as scoped canvas state. Save the options that should affect a group, draw the affected commands, then restore the previous state before drawing labels, guides, or other unaffected output. +`DrawingOptions` is not a styling object like a brush or pen. It describes how later drawing commands are interpreted by the canvas: where their geometry lands, how their coverage is rasterized, how their pixels combine with existing pixels, and how fill or clip geometry is interpreted. That is why the same options object can affect fills, strokes, text, images, clips, layers, and image-processing barriers. + ## Transform Drawing -`DrawingOptions.Transform` is applied to vector output before rasterization. For strokes, the path is stroked in local geometry space and the generated outline is transformed for drawing. +`DrawingOptions.Transform` is applied to vector output before rasterization. Paths, shapes, text glyph geometry, generated stroke outlines, and clip paths are prepared with the active transform before the backend receives the command. The source geometry still starts in the local coordinate system you wrote in the code; the transform is part of the saved canvas state that converts that local geometry into final drawing space. + +For strokes, the pen first generates an outline from the source path in local geometry space, then the active transform is applied to that generated outline. That means a scaled drawing state affects the visible stroke as well as the path it follows. If you need a shape to move or rotate while keeping a screen-constant outline width, draw the fill inside transformed state, restore, then draw the outline separately in parent coordinates. ## Why Matrix4x4? @@ -20,7 +24,7 @@ Matrix4x4 transform = new(Matrix3x2.CreateRotation(angle, center)); When more than one 2D operation is needed, compose the `Matrix3x2` expression first and wrap the final result in `Matrix4x4`. Keeping the 2D operations together makes order explicit and avoids hand-written matrix values for ordinary scale, rotate, skew, and translate cases. -Use the full `Matrix4x4` form when you need transforms that cannot be expressed by `Matrix3x2`, such as perspective-style projection. The canvas, path, text, brush, image, and WebGPU paths all carry the same transform type, so code can move between CPU drawing, retained scenes, and GPU rendering without changing the public drawing model. +Use the full `Matrix4x4` form when you need transforms that cannot be expressed by `Matrix3x2`, such as perspective-style projection. The canvas, path, text, brush, image, and WebGPU paths all carry the same transform type, so code can move between CPU drawing, retained backend scenes, and GPU rendering without changing the public drawing model. ```csharp using System.Numerics; @@ -37,7 +41,7 @@ DrawingOptions rotated = new() Transform = new(Matrix3x2.CreateRotation(0.32F, new Vector2(210, 130))) }; -RectanglePolygon panel = new(92, 70, 236, 120); +Rectangle panel = new(92, 70, 236, 120); image.Mutate(ctx => ctx.Paint(canvas => { @@ -56,7 +60,14 @@ Transforms also apply to clipped drawing. When you save transformed options with ## Blend and Composite -`GraphicsOptions` controls antialiasing, color blending, alpha composition, and blend percentage. +`GraphicsOptions` answers four separate questions for each command: + +- `Antialias` controls coverage at geometry edges. When enabled, edge pixels can receive fractional coverage for smoother vector output. When disabled, coverage is thresholded to fully covered or not covered using `AntialiasThreshold`. +- `BlendPercentage` scales the strength of the drawing operation. `1F` applies the command at full strength, `0F` makes it invisible, and values in between behave like operation opacity. +- `ColorBlendingMode` controls how source and destination color channels are combined where the command draws. `Normal` uses ordinary alpha blending. Modes such as `Multiply`, `Screen`, `Overlay`, `Darken`, and `Lighten` are useful for tinting, shadows, highlights, and visual effects. +- `AlphaCompositionMode` controls how source and destination alpha are combined using Porter-Duff composition rules. The default `SrcOver` draws the new source over existing pixels. Modes such as `Src`, `Clear`, `DestIn`, and `DestOut` are useful for replacement, erasing, masks, and cutouts. + +Those settings are per-command canvas state when you use `Save(...)`. If two shapes are drawn under a saved `GraphicsOptions`, each one blends independently with the destination. Use `SaveLayer(...)` when several commands should first render together into an isolated layer and then blend back as one group. ```csharp using SixLabors.ImageSharp; @@ -85,16 +96,20 @@ image.Mutate(ctx => ctx.Paint(canvas => // The saved GraphicsOptions affect commands recorded until Restore. canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(100, 32, 110, 176)); - canvas.Fill(Brushes.Solid(Color.Red.WithAlpha(0.5F)), new EllipsePolygon(194, 120, 124, 92)); + canvas.FillEllipse(Brushes.Solid(Color.Red.WithAlpha(0.5F)), new(194, 120), new(124, 92)); canvas.Restore(); })); ``` -Use `SaveLayer(...)` when the blend should apply to a group as a single composited result. Use plain `Save(...)` when each command should blend independently. +The example uses `Multiply` to darken overlapping colors while leaving alpha composition as `SrcOver`, so the new shapes still draw over the existing background. `BlendPercentage` reduces the strength of the whole operation without changing the source color values in the code. + +Use `SaveLayer(...)` when the blend should apply to a group as a single composited result. Use plain `Save(...)` when each command should blend independently. This distinction is important for group opacity: two semi-transparent shapes drawn independently will overlap each other; the same shapes drawn inside a layer can be composited once as a single group. ## Antialiasing -Turn antialiasing off when exact integer coverage matters, such as low-resolution masks or pixel-art-style output. Leave it on for normal vector graphics. +Antialiasing is about edge coverage, not color choice. With antialiasing enabled, partially covered edge pixels receive partial coverage so diagonal and curved edges look smooth. With antialiasing disabled, those fractional coverage values are compared with `AntialiasThreshold`; pixels above the threshold are kept, and pixels below it are discarded. + +Turn antialiasing off when exact binary coverage matters, such as low-resolution masks, hit-test masks, generated sprite masks, or pixel-art-style output. Leave it on for normal vector graphics, text, badges, diagrams, and annotations. Lowering `AntialiasThreshold` can preserve thin features when antialiasing is disabled, while raising it makes binary output more conservative. ```csharp using SixLabors.ImageSharp; @@ -117,3 +132,11 @@ image.Mutate(ctx => ctx.Paint(aliased, canvas => canvas.Fill(Brushes.Solid(Color.White), new Rectangle(10, 10, 44, 28)); })); ``` + +## Practical Guidance + +- Treat transforms and graphics options as scoped canvas state. +- Compose ordinary 2D transforms with `Matrix3x2`, then wrap the final value in `Matrix4x4`. +- Draw diagnostic bounds outside transformed state when you need parent-coordinate references. +- Use `SaveLayer(...)` for group opacity or group blending; use `Save(...)` for per-command state. +- Leave antialiasing enabled for normal vector graphics and disable it only for exact pixel coverage. diff --git a/articles/imagesharp.drawing/troubleshooting.md b/articles/imagesharp.drawing/troubleshooting.md index 7f41029a1..5a22a5d61 100644 --- a/articles/imagesharp.drawing/troubleshooting.md +++ b/articles/imagesharp.drawing/troubleshooting.md @@ -1,21 +1,21 @@ # Troubleshooting -This page collects common issues you can hit when moving from simple drawing samples to full ImageSharp.Drawing pipelines. Most problems come from three areas: deferred canvas replay, clipping and fill-rule choices, or text layout state. +This page collects common issues you can hit when moving from simple drawing samples to full ImageSharp.Drawing pipelines. Most problems come from three areas: canvas replay lifetime, clipping and fill-rule choices, or text layout state. -If the issue is WebGPU-specific, start with the WebGPU section below and then check the dedicated [WebGPU](webgpu.md) page. +If the issue is WebGPU-specific, start with the WebGPU section below and then check the dedicated [WebGPU](webgpu.md), [environment](webgpuenvironment.md), [window](webgpuwindow.md), [external surface](webgpuexternalsurface.md), and [render target](webgpurendertarget.md) pages. ## Nothing Appears on the Image If you are drawing through `image.Mutate(ctx => ctx.Paint(...))`, the processing pipeline owns the canvas lifetime and replays the recorded drawing commands for you. -If you create a canvas manually, make sure the canvas is disposed or flushed before you inspect the destination image. Canvas drawing is recorded and replayed in order, so pending commands are not visible until the canvas replays them. +If you create a canvas manually, make sure the canvas is disposed before you inspect the destination image. Canvas drawing is recorded and replayed in order, so pending commands are not visible until the root canvas replays them. [`Flush()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Flush) only seals queued commands into the replay timeline; it does not render them by itself. ```csharp using Image image = new(400, 240, Color.White.ToPixel()); using (DrawingCanvas canvas = image.CreateCanvas()) { - canvas.Fill(Color.CornflowerBlue, new Rectangle(40, 40, 180, 100)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(40, 40, 180, 100)); // Disposing the canvas replays the recorded drawing commands onto the image. } @@ -44,7 +44,7 @@ SizeF clipSize = new(260, 160); EllipsePolygon clip = new(clipCenter, clipSize); canvas.Save(options, clip); -canvas.Fill(Color.HotPink, new Rectangle(0, 0, 400, 240)); +canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(0, 0, 400, 240)); canvas.Restore(); ``` @@ -66,7 +66,9 @@ DrawingOptions options = new() } }; -canvas.Fill(options, Color.MediumSeaGreen, complexPolygon); +_ = canvas.Save(options); +canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), complexPolygon); +canvas.Restore(); ``` ## Text Is Not Centered Where Expected @@ -86,7 +88,7 @@ This matters for emoji, combining marks, flags, and other user-perceived charact Canvas operations are ordered, but image processors operate at replay barriers. A processor such as blur, opacity, or a mask operation includes drawing that was recorded before the `Apply(...)` call. ```csharp -canvas.Fill(Color.Black, shadowShape); +canvas.Fill(Brushes.Solid(Color.Black), shadowShape); // Apply seals the shadow geometry before the blur processor is applied. canvas.Apply(x => x.GaussianBlur(8)); @@ -96,18 +98,18 @@ This is most useful when you mix vector drawing with ImageSharp processors in th ## Images, Brushes, or Masks Stop Working After Disposal -Drawing commands can be replayed later than the point where the command is recorded. Keep any source `Image` used by `DrawImage`, masks, or `ImageBrush` alive until the canvas has been disposed or flushed. +Drawing commands can be replayed later than the point where the command is recorded. Keep any source `Image` used by `DrawImage`, masks, or `ImageBrush` alive until the root canvas has been disposed. The canvas does not own images passed into it. Dispose those images after the drawing scope that uses them has completed. ## WebGPU Produces a Blank Frame -Probe WebGPU support before creating GPU-backed drawing resources. WebGPU depends on the runtime environment, adapter, device, texture format, and browser or native surface. +Probe WebGPU support before creating GPU-backed drawing resources. WebGPU depends on the runtime environment, adapter, device, texture format, and native surface or offscreen target. For window or surface rendering, acquire a frame, draw into its canvas, and dispose the frame. Disposing the frame completes the drawing scope and presents it to the surface. ```csharp -if (!surface.TryAcquireFrame(out WebGPUSurfaceFrame frame)) +if (!surface.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) { return; } @@ -117,8 +119,8 @@ using (frame) DrawingCanvas canvas = frame.CreateCanvas(); // Drawing commands are presented when the frame is disposed. - canvas.Clear(Color.White); - canvas.Fill(Color.SteelBlue, new Rectangle(40, 40, 180, 120)); + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.SteelBlue), new Rectangle(40, 40, 180, 120)); } ``` @@ -126,7 +128,7 @@ Resize the [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processin ## A Good Debugging Order -1. Confirm the canvas scope is disposed or flushed before checking the output. +1. Confirm the root canvas scope is disposed before checking the output. 2. Check source image lifetimes when using image brushes, masks, or `DrawImage`. 3. Check `ShapeOptions.BooleanOperation` when clipping. 4. Check `ShapeOptions.IntersectionRule` and contour winding for complex polygons. @@ -141,3 +143,14 @@ Resize the [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processin - [Paths and Shapes](pathsandshapes.md) - [Drawing Text](text.md) - [WebGPU](webgpu.md) +- [WebGPU Environment and Support](webgpuenvironment.md) +- [WebGPU Window Rendering](webgpuwindow.md) +- [WebGPU External Surfaces](webgpuexternalsurface.md) +- [WebGPU Offscreen Render Targets](webgpurendertarget.md) + +## Practical Guidance + +- Start with lifetime and replay issues before debugging visual details. +- Reduce complex examples to one shape, one clip, or one text block to isolate state. +- Check canvas ordering around `Apply(...)` whenever processors see unexpected pixels. +- Check font availability and grapheme ranges before changing text drawing code. diff --git a/articles/imagesharp.drawing/watermark.md b/articles/imagesharp.drawing/watermark.md index 5d5f0cec3..ec27e1bd9 100644 --- a/articles/imagesharp.drawing/watermark.md +++ b/articles/imagesharp.drawing/watermark.md @@ -4,6 +4,12 @@ Use `DrawText(...)` with alignment options when a watermark should stay anchored Anchor the watermark by choosing an origin near the desired edge, then set horizontal and vertical alignment relative to that origin. This keeps the code stable when the watermark text changes length or the image size changes. +Watermark placement should normally happen after the image has reached its final export size and orientation. If you resize after drawing the watermark, the text will be resampled with the image and may become soft. If you draw before `AutoOrient()`, the anchor can land in the wrong visual corner. + +Readability is usually the hard part. A watermark that looks fine on one photo can disappear over another. Combining a semitransparent fill with a subtle contrasting stroke gives the text a chance to remain readable over both light and dark regions without making it dominate the image. + +Think of watermark styling as an accessibility problem, not only a branding problem. The fill alpha controls how strongly the watermark competes with the photo. The outline pen protects glyph edges against local contrast changes. The font size and wrapping length should be chosen for the final export dimensions, not for the original camera image size. + ```csharp using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -35,7 +41,7 @@ image.Mutate(ctx => ctx.Paint(canvas => image.Save("watermarked.jpg"); ``` -Use a subtle fill alpha and a darker outline when the watermark must remain readable over mixed image content. +Use a subtle fill alpha and a darker outline when the watermark must remain readable over mixed image content. If the watermark can contain user-supplied text, set `WrappingLength` and use alignment rather than assuming a fixed string width. For repeated export workflows, create the font and text options once per image size, then draw inside the `Paint(...)` callback. Use wrapping when the watermark can contain user or tenant names that may be longer than expected. @@ -43,3 +49,7 @@ For repeated export workflows, create the font and text options once per image s - [Drawing Text](text.md) - [Images, Masks, and Processing](imagesandprocessing.md) + +## Practical Guidance + +Use alignment options to anchor watermarks instead of manual text-size guesses. Normalize orientation and resize before positioning watermarks for export, then recreate text options when the image size, font size, wrapping, or origin changes. Use a fill alpha and outline that remain readable on both light and dark image regions. diff --git a/articles/imagesharp.drawing/webgpu.md b/articles/imagesharp.drawing/webgpu.md index bba112642..b1bc8307f 100644 --- a/articles/imagesharp.drawing/webgpu.md +++ b/articles/imagesharp.drawing/webgpu.md @@ -1,47 +1,35 @@ # WebGPU -ImageSharp.Drawing.WebGPU provides a GPU-backed drawing target for the same [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) API used by the CPU image pipeline. +ImageSharp.Drawing.WebGPU provides GPU-backed drawing targets for the same [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) API used by the CPU image pipeline. -Use the WebGPU package when you want ImageSharp.Drawing to render into a native WebGPU surface or an offscreen GPU texture. Use the regular ImageSharp.Drawing package when you want to draw directly into an `Image` on the CPU. +Use the WebGPU package when the destination is naturally GPU-owned: an interactive window, a native surface owned by another UI toolkit, or an offscreen GPU texture. Use regular ImageSharp.Drawing when the destination is an `Image` that you will process, inspect, encode, or save on the CPU. ## What WebGPU Is -WebGPU is a modern, explicit GPU API. It gives an application access to a graphics adapter, a device, command queues, textures, buffers, shaders, and presentation surfaces. It is conceptually similar to modern native graphics APIs such as Vulkan, Metal, and Direct3D 12, but it exposes a portable WebGPU programming model. +WebGPU is a modern, explicit GPU API standardized for portable GPU acceleration. It gives applications access to adapters, devices, queues, textures, buffers, shaders, and presentation surfaces. It is conceptually similar to Vulkan, Metal, and Direct3D 12, but it exposes a portable WebGPU programming model. For the broader standard, implementation status, learning resources, and community material, see [webgpu.org](https://webgpu.org/). -In ImageSharp.Drawing, WebGPU is not a browser feature. It is a native rendering backend used by .NET applications through the `SixLabors.ImageSharp.Drawing.WebGPU` package. The package creates or attaches to native WebGPU surfaces, records [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) commands, lowers those commands into GPU work, and renders them into a WebGPU texture. +In ImageSharp.Drawing, WebGPU is a native .NET rendering backend, not a browser-only feature. The `SixLabors.ImageSharp.Drawing.WebGPU` package creates or attaches to native WebGPU targets, records [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) work, lowers that work into GPU scenes, and renders into WebGPU textures. -The most important difference from normal ImageSharp drawing is the destination: +The important shift is the destination: -- normal ImageSharp.Drawing draws into CPU image memory -- ImageSharp.Drawing.WebGPU draws into GPU textures and surfaces +- CPU ImageSharp.Drawing draws into CPU image memory. +- ImageSharp.Drawing.WebGPU draws into GPU textures and presentation surfaces. -Use WebGPU when the destination is interactive, GPU-owned, or repeatedly redrawn. Use the CPU path when you need simple image generation, server-side processing, format encoding, or direct pixel access after every operation. +That affects application design. A CPU image pipeline is best when you need direct pixels after each step. A WebGPU pipeline is best when output should stay on the GPU, be redrawn repeatedly, or be presented directly to a surface. -## How ImageSharp.Drawing Uses WebGPU +## Public Type Map -The WebGPU backend keeps the public drawing model the same. You still draw with [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas), [`Brush`](xref:SixLabors.ImageSharp.Drawing.Processing.Brush), [`Pen`](xref:SixLabors.ImageSharp.Drawing.Processing.Pen), [`IPath`](xref:SixLabors.ImageSharp.Drawing.IPath), [`RichTextOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.RichTextOptions), layers, clips, and retained scenes. +The WebGPU API is organized around ownership: -The difference is what happens when the canvas flushes or is disposed: +| Type | Owns | Use it when | +|---|---|---| +| [`WebGPUEnvironment`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment) | Process-level environment configuration and support probes | You need to check availability or configure the adapter preference before creating WebGPU objects | +| [`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) | A native window, surface, frame loop, and presentation cycle | ImageSharp.Drawing should own the application window | +| [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) | A WebGPU surface attached to caller-owned native handles | Another toolkit or host application owns the window or drawable | +| [`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) | An offscreen GPU texture | You need GPU drawing without a visible window, or readback into ImageSharp | +| [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) | One acquired presentable frame | You are drawing one visible frame and must dispose it to render and present | -1. The canvas prepares the recorded drawing commands. -2. The WebGPU backend creates a retained GPU scene from those commands. -3. The backend creates render-scoped WebGPU resources. -4. GPU compute/render work rasterizes the scene into the target texture. -5. Window and external-surface frames are presented when the frame is disposed. - -That means WebGPU drawing is still deferred like the rest of the canvas API. The canvas callback is where you record work. Canvas or frame disposal is where the recorded work is submitted to the target. - -## Public WebGPU Types - -The public WebGPU API is target-first. - -- [`WebGPUEnvironment`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment) probes support and configures the library-managed WebGPU environment before first use. -- [`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) owns a native window, WebGPU surface, device resources, and render loop. -- [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) attaches to a native drawable owned by another toolkit or host application. -- [`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) owns an offscreen GPU texture and can read it back into an ImageSharp image. -- [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) represents one acquired presentable frame. Dispose it to render and present the frame. - -Most application code should start by choosing the target type. You do not normally create devices, queues, or command encoders yourself. +Start by choosing the output target. You normally do not create WebGPU devices, queues, command encoders, or shader modules yourself. ## Installation @@ -75,319 +63,76 @@ paket add SixLabors.ImageSharp.Drawing.WebGPU --version VERSION_NUMBER *** -## Check WebGPU Support - -Use [`WebGPUEnvironment`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment) when an application needs to check support before constructing a WebGPU window, external surface, or render target. - -```csharp -using SixLabors.ImageSharp.Drawing.Processing.Backends; - -WebGPUEnvironment.Options = new() -{ - PowerPreference = WebGPUPowerPreference.HighPerformance -}; - -WebGPUEnvironmentError availability = WebGPUEnvironment.ProbeAvailability(); -if (availability != WebGPUEnvironmentError.Success) -{ - return; -} - -WebGPUEnvironmentError compute = WebGPUEnvironment.ProbeComputePipelineSupport(); -if (compute != WebGPUEnvironmentError.Success) -{ - return; -} -``` - -Assign [`WebGPUEnvironment.Options`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment.Options) before any other WebGPU object is created. The library-managed WebGPU environment is initialized on first use. - -`ProbeAvailability()` checks whether the package can initialize the WebGPU API, create an instance, acquire an adapter, acquire a device, and get the default queue. `ProbeComputePipelineSupport()` checks whether the acquired device can create a trivial compute pipeline. The compute-pipeline probe is useful because the drawing backend depends on compute work for the staged raster pipeline. - -The result is a [`WebGPUEnvironmentError`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironmentError). `Success` is the only successful value. Other values tell you which step failed, such as API initialization, adapter acquisition, device acquisition, queue acquisition, or compute-pipeline creation. - -```csharp -using SixLabors.ImageSharp.Drawing.Processing.Backends; +## How Rendering Works -static bool TryUseWebGPU() -{ - WebGPUEnvironmentError availability = WebGPUEnvironment.ProbeAvailability(); - if (availability != WebGPUEnvironmentError.Success) - { - Console.WriteLine($"WebGPU unavailable: {availability}"); - return false; - } +The WebGPU backend keeps the public drawing model the same. You still draw with [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas), brushes, pens, paths, text, images, clips, layers, and retained backend scenes. - WebGPUEnvironmentError compute = WebGPUEnvironment.ProbeComputePipelineSupport(); - if (compute != WebGPUEnvironmentError.Success) - { - Console.WriteLine($"WebGPU compute unavailable: {compute}"); - return false; - } +The replay target changes: - return true; -} -``` - -Configure `WebGPUEnvironment.UncapturedError` if you want to log native WebGPU validation or device errors. The callback may be invoked from a native WebGPU callback thread, so keep it short and non-blocking. +1. Canvas commands are recorded in order. +2. The canvas seals command ranges into the replay timeline. +3. The WebGPU backend prepares retained GPU scene data for those ranges. +4. Rendering creates frame-scoped resources and dispatches GPU work into the target texture. +5. Surface frames are presented when the frame is disposed. -```csharp -using SixLabors.ImageSharp.Drawing.Processing.Backends; +Disposal is part of correctness. A manually-created WebGPU canvas must be disposed to replay its recorded work. A [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) must be disposed to render and present the frame. -WebGPUEnvironment.UncapturedError = (errorType, message) => -{ - Console.Error.WriteLine($"{errorType}: {message}"); -}; -``` - -## Texture Formats +## Choosing a Target -WebGPU targets have a concrete texture format. The supported formats are: +Use [`WebGPUWindow`](webgpuwindow.md) when ImageSharp.Drawing should own the window and event loop. -- `Rgba8Unorm`, mapped to `Rgba32` -- `Bgra8Unorm`, mapped to `Bgra32` -- `Rgba8Snorm`, mapped to `NormalizedByte4` -- `Rgba16Float`, mapped to `HalfVector4` +Use [`WebGPUExternalSurface`](webgpuexternalsurface.md) when another application, UI framework, game engine, or native toolkit owns the window and exposes native handles. -Use the default `Rgba8Unorm` unless you have a reason to match another host surface format or readback pixel type. +Use [`WebGPURenderTarget`](webgpurendertarget.md) when you want offscreen GPU drawing, render-to-texture workflows, tests, benchmarks, or readback into an `Image`. -```csharp -using SixLabors.ImageSharp.Drawing.Processing.Backends; +Use [`WebGPUEnvironment`](webgpuenvironment.md) before any of those when you need predictable startup behavior, diagnostics, or fallback to CPU rendering. -WebGPUWindowOptions options = new() -{ - Format = WebGPUTextureFormat.Rgba8Unorm -}; -``` +## Shared Concepts -For readback, the ImageSharp pixel type must match the render target format. For example, `Rgba8Unorm` reads back naturally as `Image`, and `Bgra8Unorm` reads back naturally as `Image`. +Texture format controls the GPU target and, for readback, the matching ImageSharp pixel type: -## Present Modes +| WebGPU format | Natural ImageSharp pixel type | +|---|---| +| `Rgba8Unorm` | `Rgba32` | +| `Bgra8Unorm` | `Bgra32` | +| `Rgba8Snorm` | `NormalizedByte4` | +| `Rgba16Float` | `HalfVector4` | -Window and external-surface targets present completed frames to a display. `WebGPUPresentMode` controls how frames wait for that display. +Present mode controls how visible frames wait for the display: -- `Fifo` is the safest default. It is v-synced and avoids tearing. +- `Fifo` is v-synced and is the safest default. - `Immediate` presents as soon as possible and can tear. -- `Mailbox` keeps newer frames over older queued frames when supported by the backend and platform. - -Use `Fifo` for most applications. Use `Immediate` or `Mailbox` only when latency matters more than presentation stability and you have tested the target platform. - -## Draw to a Window - -[`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) owns the platform window, WebGPU device resources, and frame acquisition. The render callback receives a [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame), and the frame exposes the [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) for that render. - -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; - -WebGPUWindowOptions options = new() -{ - Title = "ImageSharp.Drawing WebGPU", - Size = new(960, 540), - PresentMode = WebGPUPresentMode.Fifo -}; - -using WebGPUWindow window = new(options); - -window.Run((WebGPUSurfaceFrame frame) => -{ - DrawingCanvas canvas = frame.Canvas; - RectanglePolygon panel = new(64, 72, 320, 180); - EllipsePolygon marker = new(new PointF(224, 162), new SizeF(120, 82)); - - // Run supplies a frame canvas and presents it after the callback completes. - canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(Brushes.Solid(Color.CornflowerBlue), panel); - canvas.Fill(Brushes.Solid(Color.Gold), marker); - canvas.Draw(Pens.Solid(Color.Black, 3), panel); -}); -``` - -`Run(Action)` is the simplest model. The window acquires a frame, gives you the frame and its canvas, disposes the frame after the callback, and presents the result. - -Use the [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) overload when you need frame lifetime control or the elapsed render time. - -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; - -using WebGPUWindow window = new(); - -window.Run((frame, elapsed) => -{ - DrawingCanvas canvas = frame.Canvas; - float radius = 40 + (MathF.Sin((float)elapsed.TotalSeconds) * 12); - EllipsePolygon pulse = new(new PointF(120, 120), radius); - - // Disposing the frame after this callback presents the rendered canvas. - canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(Brushes.Solid(Color.MediumSeaGreen), pulse); -}); -``` - -[`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) also exposes window events and properties such as title, size, framebuffer size, render scale, position, visibility, focus, state, border, frame rate limits, and present mode. `FramebufferSize` is the size that matters for the WebGPU surface. `ClientSize` is the window coordinate size. - -## Manual Frame Acquisition - -Use `TryAcquireFrame(...)` when you own the loop and want to decide when events, updates, and rendering happen. +- `Mailbox` keeps newer frames over older queued frames when supported. -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; - -using WebGPUWindow window = new(); - -while (!window.IsClosing) -{ - window.DoEvents(); - - if (!window.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) - { - continue; - } - - using (frame) - { - DrawingCanvas canvas = frame.Canvas; - canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new RectanglePolygon(40, 40, 180, 120)); - } -} -``` - -`TryAcquireFrame(...)` can return `false` when the surface cannot provide a drawable frame right now. That can happen for transient surface states such as timeout, outdated surface, lost surface, zero-sized framebuffer, or device recovery. Treat `false` as "skip this render attempt and try again later." - -## Draw to an Existing Surface - -Use [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when another toolkit owns the window or native drawable. Create a [`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) for the platform handle, notify the surface when the drawable framebuffer changes size, and acquire one [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) for each render. - -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; - -void RunWin32Surface(nint hwnd, nint hinstance) -{ - WebGPUSurfaceHost host = WebGPUSurfaceHost.Win32(hwnd, hinstance); - using WebGPUExternalSurface surface = new(host, new(1280, 720)); - - void Resize(Size framebufferSize) => surface.Resize(framebufferSize); - - void Render() - { - if (!surface.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) - { - return; - } - - using (frame) - { - RectanglePolygon content = new(48, 48, 320, 160); - - // The external UI loop owns when Render is called; the frame owns presentation. - frame.Canvas.Clear(Brushes.Solid(Color.White)); - frame.Canvas.Fill(Brushes.Solid(Color.Orange), content); - } - } -} -``` - -[`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) includes factory methods for GLFW, SDL, Win32, X11, Cocoa, UIKit, Wayland, WinRT, Android, Vivante, and EGL hosts. - -The host application remains responsible for: - -- creating and owning the native window or drawable -- providing the correct native handles to [`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) -- calling `Resize(...)` when the drawable framebuffer size changes -- calling `TryAcquireFrame(...)` from its render loop -- disposing each acquired [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) -- keeping native handles valid for the lifetime of the external surface - -Use [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when ImageSharp.Drawing should render into an existing UI framework or native application instead of creating its own window. - -## Draw Offscreen - -[`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) renders into an offscreen GPU texture. Create a canvas, draw into it, dispose the canvas to flush the drawing work, then read the result back when CPU image access is needed. - -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.PixelFormats; - -using WebGPURenderTarget target = new(640, 360); - -using (DrawingCanvas canvas = target.CreateCanvas()) -{ - RectanglePolygon background = new(0, 0, target.Width, target.Height); - EllipsePolygon highlight = new(new PointF(320, 180), new SizeF(260, 140)); - - // Disposing the canvas flushes the recorded drawing commands to the GPU target. - canvas.Fill(Brushes.Solid(Color.White), background); - canvas.Fill(Brushes.Solid(Color.LightSkyBlue), highlight); - canvas.Draw(Pens.Solid(Color.DarkSlateBlue, 4), highlight); -} - -using Image image = target.ReadbackImage(); -image.Save("webgpu-output.png"); -``` - -Offscreen render targets are useful for GPU-generated images, render-to-texture workflows, tests, benchmarks, and any workflow that wants GPU drawing without a visible window. - -Readback copies GPU texture data into CPU memory. It is useful when you need an `Image`, but it is also a synchronization point. Avoid reading back every frame in an interactive render loop unless you actually need CPU pixels. - -## Choosing a Target - -Use [`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) when ImageSharp.Drawing should own the application window and render loop. - -Use [`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) when an existing application, UI framework, or native toolkit owns the window and event loop. - -Use [`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) when you want GPU rendering without a visible window, or when the output needs to be read back into an ImageSharp image. +CPU/GPU synchronization is the expensive boundary. Reading a render target back into an `Image` copies GPU texture data into CPU memory. Running normal ImageSharp processors through `Apply(...)` on a GPU-backed canvas can require GPU readback, CPU processing, and upload before drawing continues. ## When Not to Use WebGPU -WebGPU is not automatically the best target for every drawing workload. Prefer normal ImageSharp.Drawing when: +WebGPU is not automatically better for every drawing workload. Prefer the normal CPU path when: -- you are generating static images on the server +- you are generating static images on a server - you need direct CPU pixel access after most operations -- you are encoding the result immediately to PNG, JPEG, WebP, or another image format -- your deployment environment has no reliable GPU, native WebGPU runtime, or compute-pipeline support -- the drawing workload is small enough that GPU setup and readback costs dominate +- you immediately encode the result to PNG, JPEG, WebP, or another image format +- the deployment environment has unreliable GPU or native WebGPU support +- the drawing workload is small enough that GPU setup or readback costs dominate Prefer WebGPU when: - the target is already a GPU surface - the scene is interactive or redrawn repeatedly -- you can keep the result on the GPU -- you want a native window or external host surface +- the output can stay on the GPU +- you need a native window or host surface - the drawing workload benefits from GPU-side batching and rasterization -## Frame Lifetime Rules - -The important lifetime rules are: - -- Dispose a [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) created from [`WebGPURenderTarget.CreateCanvas()`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget.CreateCanvas*) to submit its recorded work. -- Dispose a [`WebGPUSurfaceFrame`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceFrame) to submit and present the frame. -- Keep retained scenes alive until every canvas or frame that recorded them has been disposed. -- Keep source images used by image brushes alive until the WebGPU canvas has replayed. -- Call `Resize(...)` on external surfaces before acquiring the next frame after a framebuffer resize. - -The window `Run(...)` helpers handle frame disposal for you. Manual loops and external surfaces require you to dispose the frame yourself. - -## Troubleshooting - -If WebGPU cannot start, call `ProbeAvailability()` and log the returned `WebGPUEnvironmentError`. +## Related Topics -If support probing succeeds but drawing fails, also call `ProbeComputePipelineSupport()` and configure `WebGPUEnvironment.UncapturedError` before creating WebGPU targets. +- [WebGPU Environment and Support](webgpuenvironment.md) +- [WebGPU Window Rendering](webgpuwindow.md) +- [WebGPU External Surfaces](webgpuexternalsurface.md) +- [WebGPU Offscreen Render Targets](webgpurendertarget.md) +- [Canvas Drawing](canvas.md) +- [Transforms and Composition](transformsandcomposition.md) -If a window or external surface stops rendering after resize or display changes, make sure the framebuffer size is positive and, for external surfaces, call `Resize(...)` with the new framebuffer size before acquiring frames. +## Practical Guidance -If readback fails or produces an unexpected pixel type, check that the render target format and requested `Image` type match. +Choose the target first, because the target owns the lifetime rules. Probe support before constructing production WebGPU targets. Dispose canvases and frames promptly so recorded drawing is submitted. Keep source images, image brushes, fonts, paths, and retained backend scenes alive until every canvas or frame that references them has been disposed. diff --git a/articles/imagesharp.drawing/webgpuenvironment.md b/articles/imagesharp.drawing/webgpuenvironment.md new file mode 100644 index 000000000..fa1e27027 --- /dev/null +++ b/articles/imagesharp.drawing/webgpuenvironment.md @@ -0,0 +1,96 @@ +# WebGPU Environment and Support + +[`WebGPUEnvironment`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment) configures and probes the library-managed WebGPU environment. Use it before constructing windows, external surfaces, or render targets when startup must be predictable. + +The environment represents the process-level WebGPU runtime used by the public target types. It is responsible for acquiring the adapter, device, and queue used by ImageSharp.Drawing.WebGPU. Because the environment initializes on first use, set options and error callbacks before creating any WebGPU object. + +## Configure Before First Use + +[`WebGPUEnvironment.Options`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment.Options) is read during first initialization. Changing it later does not reconfigure an existing device. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUEnvironment.Options = new() +{ + PowerPreference = WebGPUPowerPreference.HighPerformance +}; +``` + +`HighPerformance` is the usual choice for drawing workloads. If an application should prefer a lower-power adapter, configure that once during startup before probing or creating targets. + +## Probe Availability + +Call [`ProbeAvailability()`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment.ProbeAvailability) to check whether the library can initialize the WebGPU API, create an instance, acquire an adapter, acquire a device, and get the default queue. + +Call [`ProbeComputePipelineSupport()`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment.ProbeComputePipelineSupport) when the drawing backend must prove it can create compute pipelines. This is a stronger check than basic device acquisition. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +static bool TryUseWebGPU() +{ + WebGPUEnvironmentError availability = WebGPUEnvironment.ProbeAvailability(); + if (availability != WebGPUEnvironmentError.Success) + { + Console.WriteLine($"WebGPU unavailable: {availability}"); + return false; + } + + WebGPUEnvironmentError compute = WebGPUEnvironment.ProbeComputePipelineSupport(); + if (compute != WebGPUEnvironmentError.Success) + { + Console.WriteLine($"WebGPU compute unavailable: {compute}"); + return false; + } + + return true; +} +``` + +`Success` is the only successful result. Other values are stable failure categories such as API initialization failure, adapter timeout, device request failure, queue acquisition failure, or compute-pipeline probe failure. Branch on the enum value rather than parsing diagnostic strings. + +## Log Native WebGPU Errors + +Configure [`UncapturedError`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUEnvironment.UncapturedError) to receive native WebGPU validation, device, or internal errors reported outside a specific managed call. + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUEnvironment.UncapturedError = (errorType, message) => +{ + Console.Error.WriteLine($"{errorType}: {message}"); +}; +``` + +The callback can be raised from a native WebGPU callback thread. Keep handlers short and non-blocking. Use it for logging, not for complex UI updates or recovery work. + +## Fallback Strategy + +Applications that can render on either CPU or GPU should decide early: + +```csharp +bool useGpu = TryUseWebGPU(); + +if (useGpu) +{ + // Construct WebGPUWindow, WebGPUExternalSurface, or WebGPURenderTarget. +} +else +{ + // Fall back to Image and the normal ImageSharp.Drawing path. +} +``` + +Do not create a WebGPU target first and then probe after failure. Probing first gives better diagnostics and avoids partially initialized rendering paths. + +## Related Topics + +- [WebGPU](webgpu.md) +- [WebGPU Window Rendering](webgpuwindow.md) +- [WebGPU External Surfaces](webgpuexternalsurface.md) +- [WebGPU Offscreen Render Targets](webgpurendertarget.md) + +## Practical Guidance + +Configure `WebGPUEnvironment.Options` and `UncapturedError` during application startup. Use `ProbeAvailability()` for basic device readiness and `ProbeComputePipelineSupport()` when WebGPU drawing is required. Treat a non-success result as a normal deployment condition and provide a CPU fallback when the application can still produce useful output. diff --git a/articles/imagesharp.drawing/webgpuexternalsurface.md b/articles/imagesharp.drawing/webgpuexternalsurface.md new file mode 100644 index 000000000..d5667b6e5 --- /dev/null +++ b/articles/imagesharp.drawing/webgpuexternalsurface.md @@ -0,0 +1,122 @@ +# WebGPU External Surfaces + +[`WebGPUExternalSurface`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface) attaches ImageSharp.Drawing.WebGPU to a native drawable owned by another application or UI toolkit. + +Use it when you already have a window, view, swapchain host, or platform drawable and ImageSharp.Drawing should render into that existing surface. Unlike [`WebGPUWindow`](webgpuwindow.md), this type does not own the native window or event loop. + +## Ownership Model + +The host application owns: + +- the native window or drawable +- the native handles passed to [`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) +- resize notifications +- event processing +- render scheduling +- the lifetime of the underlying UI object + +`WebGPUExternalSurface` owns the WebGPU surface resources attached to those handles. It can acquire frames, create frame canvases, and present rendered output, but it never releases the native handles you supplied. + +## Create a Surface Host + +Create a [`WebGPUSurfaceHost`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUSurfaceHost) with the factory method matching the host platform or toolkit: + +- `Glfw(...)` +- `Sdl(...)` +- `Win32(...)` +- `X11(...)` +- `Cocoa(...)` +- `UIKit(...)` +- `Wayland(...)` +- `WinRT(...)` +- `Android(...)` +- `Vivante(...)` +- `EGL(...)` + +For Win32: + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUSurfaceHost host = WebGPUSurfaceHost.Win32(hwnd, hinstance); +``` + +Pass valid native handles for the lifetime of the external surface. The exact handles depend on the platform. For example, X11 needs a display pointer and window id, Wayland needs display and surface pointers, and UIKit needs the window plus framebuffer objects. + +## Create the External Surface + +The framebuffer size is the drawable size in pixels, not necessarily the logical UI size. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUExternalSurfaceOptions options = new() +{ + PresentMode = WebGPUPresentMode.Fifo, + Format = WebGPUTextureFormat.Rgba8Unorm +}; + +using WebGPUExternalSurface surface = new(host, new Size(1280, 720), options); +``` + +Use a custom `Configuration` overload when the drawing backend should be bound to a specific ImageSharp configuration. + +## Handle Resizes + +The host application must notify the external surface when the drawable framebuffer changes size: + +```csharp +void OnFramebufferResized(int width, int height) +{ + surface.Resize(new Size(width, height)); +} +``` + +Zero-sized framebuffers are ignored. This matters for minimized windows, hidden views, and platform states where the drawable temporarily has no size. + +## Render a Frame + +Call [`TryAcquireFrame(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUExternalSurface.TryAcquireFrame*) from the host render loop. Dispose each acquired frame to submit and present. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +void Render() +{ + if (!surface.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) + { + return; + } + + using (frame) + { + DrawingCanvas canvas = frame.Canvas; + + // The host owns when Render is called; the frame owns presentation. + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.Orange), new Rectangle(48, 48, 320, 160)); + } +} +``` + +Use the overload that accepts `DrawingOptions` when the whole frame should start with a specific transform, graphics options, or shape options. + +## External Surface Failure Modes + +`TryAcquireFrame(...)` can return `false` when a drawable frame is not available. Common causes include a zero-sized framebuffer, an outdated or lost surface, timeout, or device recovery. + +The correct response is usually to skip that render attempt, keep processing host events, and try again on the next render tick. Recreate the external surface only when the host application has replaced the native drawable or invalidated the handles. + +## Related Topics + +- [WebGPU](webgpu.md) +- [WebGPU Environment and Support](webgpuenvironment.md) +- [WebGPU Window Rendering](webgpuwindow.md) +- [WebGPU Offscreen Render Targets](webgpurendertarget.md) + +## Practical Guidance + +Use `WebGPUExternalSurface` when ImageSharp.Drawing is a renderer inside somebody else's windowing model. Keep native handles valid, forward framebuffer resize events, acquire at most one frame per render, dispose every acquired frame, and treat a failed frame acquisition as a normal transient condition. diff --git a/articles/imagesharp.drawing/webgpurendertarget.md b/articles/imagesharp.drawing/webgpurendertarget.md new file mode 100644 index 000000000..4ade5f1fe --- /dev/null +++ b/articles/imagesharp.drawing/webgpurendertarget.md @@ -0,0 +1,104 @@ +# WebGPU Offscreen Render Targets + +[`WebGPURenderTarget`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget) owns an offscreen WebGPU texture. Use it when you want GPU drawing without a visible window, or when a GPU-rendered result must later be read back into an ImageSharp image. + +Offscreen render targets are useful for render-to-texture workflows, GPU-generated assets, tests, benchmarks, previews, and pipelines that draw on the GPU before handing the final result back to CPU code. + +## Create a Render Target + +The simplest constructor uses the default `Rgba8Unorm` format: + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPURenderTarget target = new(640, 360); +``` + +Specify a format when the target must match another GPU workflow or readback pixel type: + +```csharp +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPURenderTarget target = new(WebGPUTextureFormat.Bgra8Unorm, 640, 360); +``` + +The target exposes `Width`, `Height`, `Bounds`, and `Format`. The bounds are always rooted at `(0, 0)` in target pixel coordinates. + +## Draw Offscreen + +Create a canvas, draw into it, and dispose the canvas to replay and submit the recorded work to the offscreen texture. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPURenderTarget target = new(640, 360); + +using (DrawingCanvas canvas = target.CreateCanvas()) +{ + // Canvas disposal replays the recorded drawing work into the GPU texture. + canvas.Fill(Brushes.Solid(Color.White)); + canvas.FillEllipse(Brushes.Solid(Color.LightSkyBlue), new(320, 180), new(260, 140)); + canvas.DrawEllipse(Pens.Solid(Color.DarkSlateBlue, 4), new(320, 180), new(260, 140)); +} +``` + +Use `CreateCanvas(DrawingOptions)` when the whole render pass should start with non-default drawing state. + +## Read Back to ImageSharp + +[`ReadbackImage()`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget.ReadbackImage*) creates a new CPU image from the current GPU texture contents. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; + +using Image image = target.ReadbackImage(); +image.Save("webgpu-output.png"); +``` + +The typed readback pixel type must match the target format: + +| Target format | Typed readback | +|---|---| +| `Rgba8Unorm` | `ReadbackImage()` | +| `Bgra8Unorm` | `ReadbackImage()` | +| `Rgba8Snorm` | `ReadbackImage()` | +| `Rgba16Float` | `ReadbackImage()` | + +The non-generic `ReadbackImage()` chooses the natural ImageSharp pixel type from the render target format. + +## Read Back Into an Existing Buffer + +Use [`ReadbackInto(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPURenderTarget.ReadbackInto*) when you already own the destination pixel buffer: + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; + +using Image destination = new(target.Width, target.Height); +target.ReadbackInto(destination.Frames.RootFrame.PixelBuffer.GetRegion()); +``` + +If the destination region is smaller than the render target, the matching top-left portion is read back. This lets callers read into bounded regions without forcing an intermediate full-size image. + +## Readback Cost + +Readback is a synchronization point. The GPU work must be visible to the CPU, and the texture data must be copied into CPU memory. That cost is fine for final export, tests, snapshots, or occasional thumbnails. It is usually the wrong thing to do every frame in an interactive GPU render loop. + +If the next stage is also GPU-owned, keep the result in a WebGPU render target or surface instead of reading it back to ImageSharp immediately. + +## Related Topics + +- [WebGPU](webgpu.md) +- [WebGPU Environment and Support](webgpuenvironment.md) +- [WebGPU Window Rendering](webgpuwindow.md) +- [WebGPU External Surfaces](webgpuexternalsurface.md) + +## Practical Guidance + +Use `WebGPURenderTarget` when the output should be GPU-rendered but not immediately presented to a window. Dispose the canvas before readback. Pick the texture format from the final consumer, and avoid readback in tight frame loops unless CPU pixels are genuinely required. diff --git a/articles/imagesharp.drawing/webgpuwindow.md b/articles/imagesharp.drawing/webgpuwindow.md new file mode 100644 index 000000000..5590e5abd --- /dev/null +++ b/articles/imagesharp.drawing/webgpuwindow.md @@ -0,0 +1,143 @@ +# WebGPU Window Rendering + +[`WebGPUWindow`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow) is the highest-level WebGPU target. It owns the native window, WebGPU surface, device resources, frame acquisition, and presentation cycle. + +Use it when ImageSharp.Drawing should own the application window. If another UI framework, game engine, or host application owns the window, use [`WebGPUExternalSurface`](webgpuexternalsurface.md) instead. + +## Window Ownership + +A `WebGPUWindow` wraps a real native window and exposes a [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) for each acquired frame. You draw into that frame canvas, and the frame is presented when the frame scope is disposed. + +The window owns: + +- the platform window +- the WebGPU presentation surface +- the frame acquisition loop +- resize-driven surface reconfiguration +- the drawing backend bound to the window target + +That ownership makes it a good fit for demos, tools, visualizers, preview windows, and applications where ImageSharp.Drawing is the main renderer. + +## Create a Window + +[`WebGPUWindowOptions`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindowOptions) controls the initial title, size, position, visibility, scheduling hints, state, border, present mode, and texture format. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +WebGPUWindowOptions options = new() +{ + Title = "ImageSharp.Drawing WebGPU", + Size = new(960, 540), + PresentMode = WebGPUPresentMode.Fifo, + Format = WebGPUTextureFormat.Rgba8Unorm +}; + +using WebGPUWindow window = new(options); +``` + +`Size` is the initial client-area size in window coordinates. `FramebufferSize` is the pixel size of the drawable WebGPU surface. On high-DPI displays those can differ; use `RenderScale`, `PointToFramebuffer(...)`, or framebuffer resize events when mapping input to pixels. + +## Render With Run + +The simplest model is [`Run(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow.Run*). The window owns the loop, acquires one frame per render callback, and disposes the frame after your callback returns. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPUWindow window = new(new WebGPUWindowOptions { Title = "WebGPU Demo" }); + +window.Run((WebGPUSurfaceFrame frame) => +{ + DrawingCanvas canvas = frame.Canvas; + Rectangle panel = new(64, 72, 320, 180); + + // The frame is presented after Run disposes it at the end of the callback. + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), panel); + canvas.FillEllipse(Brushes.Solid(Color.Gold), new(224, 162), new(120, 82)); + canvas.Draw(Pens.Solid(Color.Black, 3), panel); +}); +``` + +Use the elapsed-time overload when animation needs frame timing: + +```csharp +window.Run((frame, elapsed) => +{ + DrawingCanvas canvas = frame.Canvas; + float radius = 40 + (MathF.Sin((float)elapsed.TotalSeconds) * 12); + + // The radius is converted to an ellipse size because FillEllipse takes width and height. + canvas.Clear(Brushes.Solid(Color.White)); + canvas.FillEllipse(Brushes.Solid(Color.MediumSeaGreen), new(120, 120), new(radius * 2, radius * 2)); +}); +``` + +## Drive the Loop Manually + +Use [`TryAcquireFrame(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.Backends.WebGPUWindow.TryAcquireFrame*) when the application owns the loop. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +using WebGPUWindow window = new(); + +while (!window.IsClosing) +{ + window.DoEvents(); + + if (!window.TryAcquireFrame(out WebGPUSurfaceFrame? frame)) + { + continue; + } + + using (frame) + { + DrawingCanvas canvas = frame.Canvas; + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(40, 40, 180, 120)); + } +} +``` + +`TryAcquireFrame(...)` can return `false` when no drawable frame is available right now. That can happen for transient surface states such as timeout, outdated surface, lost surface, zero-sized framebuffer, or device recovery. Treat `false` as "skip this render attempt and try again later." + +## Window Events and State + +`WebGPUWindow` exposes events for update, resize, framebuffer resize, closing, focus, move, state change, and file drop. Use `FramebufferResized` when WebGPU pixel dimensions matter. Use `Resized` when UI layout in client coordinates matters. + +Useful properties include: + +- `Title`, `ClientSize`, `FramebufferSize`, and `RenderScale` +- `Position`, `IsVisible`, `WindowState`, and `WindowBorder` +- `FramesPerSecond`, `UpdatesPerSecond`, and `IsEventDriven` +- `PresentMode` and `Format` +- `PointToClient(...)`, `PointToScreen(...)`, and `PointToFramebuffer(...)` + +Changing `PresentMode` reconfigures the surface. Changing `Format` is an initial creation choice; choose the target format in `WebGPUWindowOptions`. + +## Frame Lifetime + +A frame owns one acquired presentable texture. Drawing commands are recorded through `frame.Canvas`. Disposing the frame disposes that canvas, replays the drawing timeline, submits GPU work, presents the surface texture, and releases per-frame resources. + +If `Run(...)` owns the loop, it disposes the frame for you. If you call `TryAcquireFrame(...)`, dispose every frame you acquire. + +## Related Topics + +- [WebGPU](webgpu.md) +- [WebGPU Environment and Support](webgpuenvironment.md) +- [WebGPU External Surfaces](webgpuexternalsurface.md) +- [WebGPU Offscreen Render Targets](webgpurendertarget.md) + +## Practical Guidance + +Use `WebGPUWindow` when ImageSharp.Drawing owns the render loop. Start with `Run(...)`; move to `TryAcquireFrame(...)` only when you need explicit event, update, and render scheduling. Keep frame drawing short, dispose frames promptly, and use framebuffer coordinates when mapping input or layout to actual GPU pixels. diff --git a/articles/imagesharp.web/configuration.md b/articles/imagesharp.web/configuration.md index d09936e3a..f3d6e8c0e 100644 --- a/articles/imagesharp.web/configuration.md +++ b/articles/imagesharp.web/configuration.md @@ -205,3 +205,11 @@ These callbacks are often the right tool when you need small workflow adjustment - [Processing Commands](processingcommands.md) - [Securing Requests](security.md) - [Extensibility](extensibility.md) + +## Practical Guidance + +Most ImageSharp.Web configuration changes affect one stage of the request pipeline: command parsing, source resolution, image processing, encoding, caching, or response behavior. Change the stage that owns the behavior you need, and keep the others boring. For example, a provider should resolve source images; it should not also reinterpret resize commands. A parser should shape the command collection; it should not open streams. + +Be careful when replacing callbacks. The default `OnParseCommandsAsync` inserts `autoorient=true` when the request does not specify orientation behavior, so replacing it without preserving the existing delegate also changes default output. That can be correct for passthrough scenarios, but it should be a deliberate compatibility decision. + +For public URLs, presets are often a better product surface than arbitrary query strings. They limit the transformation vocabulary, simplify HMAC signing, reduce cache explosion, and make generated variants easier to reason about. When you do allow free-form commands, keep the middleware ImageSharp configuration aligned with your encoder and ICC expectations so a URL means the same thing across deployments. diff --git a/articles/imagesharp.web/extensibility.md b/articles/imagesharp.web/extensibility.md index abde17691..2bb520116 100644 --- a/articles/imagesharp.web/extensibility.md +++ b/articles/imagesharp.web/extensibility.md @@ -12,6 +12,8 @@ ImageSharp.Web is designed as a set of replaceable layers rather than one monoli - Use [`IImageCache`](xref:SixLabors.ImageSharp.Web.Caching.IImageCache) and [`IImageCacheResolver`](xref:SixLabors.ImageSharp.Web.Resolvers.IImageCacheResolver) when processed output should be stored in a new backend. - Use [`ICacheKey`](xref:SixLabors.ImageSharp.Web.Caching.ICacheKey) or [`ICacheHash`](xref:SixLabors.ImageSharp.Web.Caching.ICacheHash) when only cache naming needs to change. +Choose the narrowest extension point that owns the behavior. A parser should not open source images. A provider should not parse resize commands. A processor should not decide where cached files live. Keeping those boundaries clean makes security, caching, HMAC validation, and diagnostics much easier to reason about. + ## Add a Custom Processor Custom processors are the usual way to introduce a new query-string command. Implement [`IImageWebProcessor`](xref:SixLabors.ImageSharp.Web.Processors.IImageWebProcessor), parse your command values from the [`CommandCollection`](xref:SixLabors.ImageSharp.Web.Commands.CommandCollection), and mutate the [`FormattedImage`](xref:SixLabors.ImageSharp.Web.FormattedImage): @@ -59,12 +61,16 @@ builder.Services.AddImageSharp() Processor order is driven by the order of the recognized command keys in the request, so custom processors participate in the same ordering model as the built-in ones. +Processors should be deterministic for the same source image and command collection. If a processor depends on external data, include that data in the command surface or cache key strategy; otherwise cached output can become stale or inconsistent. + ## Custom Command Converters The built-in converters already cover integral types, floating-point values, booleans, strings, arrays, lists, colors, and enums. If your processor wants a custom command type, implement [`ICommandConverter`](xref:SixLabors.ImageSharp.Web.Commands.Converters.ICommandConverter`1), register it with [`AddConverter()`](xref:SixLabors.ImageSharp.Web.ImageSharpBuilderExtensions.AddConverter*), then parse it inside the processor with [`CommandParser.ParseValue()`](xref:SixLabors.ImageSharp.Web.Commands.CommandParser.ParseValue*). This is the right place to centralize parsing rules for custom value syntaxes instead of repeating string parsing inside each processor. +Converters should parse request values into stable typed values. Keep validation messages clear, because parse failures normally surface as client-facing bad requests. + ## Custom Providers and Caches Implement a custom provider when your source image is not on disk, in Azure Blob Storage, or in S3. A provider owns request matching and returns a resolver that can: @@ -79,6 +85,8 @@ Implement a custom cache when processed images should live somewhere other than If you only need different cache naming rather than a whole new backend, replace [`ICacheKey`](xref:SixLabors.ImageSharp.Web.Caching.ICacheKey) or [`ICacheHash`](xref:SixLabors.ImageSharp.Web.Caching.ICacheHash) instead of writing a new cache. +Providers and caches sit on hot request paths. Keep stream ownership explicit, avoid buffering entire images unless the backend requires it, and make cache metadata decisions consistently so conditional requests and stale entries behave predictably. + ## Replace the Request Syntax Implement [`IRequestParser`](xref:SixLabors.ImageSharp.Web.Commands.IRequestParser) when commands should come from somewhere other than the raw query string, for example: @@ -96,6 +104,14 @@ If you add custom processors and want equally natural Razor markup, derive from That lets your Razor layer stay strongly typed instead of falling back to raw query-string fragments. +## Production Checklist + +- Decide whether the extension changes request parsing, processing, source resolution, cache storage, or URL generation before choosing an API. +- Keep custom command names stable; changing them invalidates URLs and cache keys. +- Include any output-affecting external state in commands, presets, or cache-key inputs. +- Preserve HMAC and preset restrictions when replacing request parsing. +- Log enough context to diagnose provider misses, parser failures, cache misses, and processor validation errors. + ## Related Topics - [Configuration and Pipeline](configuration.md) diff --git a/articles/imagesharp.web/gettingstarted.md b/articles/imagesharp.web/gettingstarted.md index 3433c9a12..36a695ebe 100644 --- a/articles/imagesharp.web/gettingstarted.md +++ b/articles/imagesharp.web/gettingstarted.md @@ -101,3 +101,10 @@ Keep source storage and cache storage conceptually separate. The provider root i - [Image Providers](imageproviders.md) - [Image Caches](imagecaches.md) - [Securing Requests](security.md) + +## Practical Guidance + +- Put `UseImageSharp()` before middleware that would otherwise serve source image files directly. +- Keep source storage separate from derived cache storage. +- Configure provider and cache roots explicitly when the app has no web root or runs in a container. +- Read the security page before exposing free-form transformation URLs publicly. diff --git a/articles/imagesharp.web/imagecaches.md b/articles/imagesharp.web/imagecaches.md index 76979780c..1cf33e0fc 100644 --- a/articles/imagesharp.web/imagecaches.md +++ b/articles/imagesharp.web/imagecaches.md @@ -2,6 +2,8 @@ ImageSharp.Web caches processed output so that identical requests do not repeatedly decode, process, and re-encode the source image. The cache stores both the encoded bytes and metadata about the source and response so the middleware can detect stale entries and serve correct headers. +Think of the cache as derived output, not source-of-truth storage. It should be safe to clear and rebuild, but it must be configured carefully enough that all application instances agree on keys, freshness, and storage location. + ## How the Cache Works For each processed request, the middleware: @@ -43,6 +45,8 @@ ImageSharp.Web tracks two different lifetimes: If the source provider supplies a source `Cache-Control` max-age, that value overrides `BrowserMaxAge` for the response. +Set browser lifetime based on how long clients may keep a response without revalidation. Set backend cache lifetime based on how long your server-side derived output should be trusted before checking the source again. Those are related, but they are not the same operational decision. + ## Cache Keys and Hashes By default, ImageSharp.Web uses: @@ -144,6 +148,14 @@ builder.Services.AddImageSharp() Cached objects use the hashed request key as the object key, and the response metadata needed by the middleware is stored with the object. +## Practical Guidance + +Treat the cache as derived output. It should be safe to clear and rebuild, but it must be separate from source storage so cleanup jobs cannot delete originals. In single-instance deployments a physical cache may be enough; in multi-instance deployments, use shared storage when all instances should reuse the same processed variants. + +Cache lifetime has two audiences. Browser lifetime controls how long clients may reuse a response without coming back. Backend cache lifetime controls how long the server trusts a generated variant before checking source freshness. Those values should match source update frequency, CDN behavior, and the cost of regeneration. + +If you customize cache keys, include every output-affecting request detail. Host, tenant, preset expansion, command values, and source path can all matter depending on the application. Monitor cache growth when public URLs expose many dimensions or quality values, because variant counts can grow faster than source image counts. + ## Related Topics - [Getting Started](gettingstarted.md) diff --git a/articles/imagesharp.web/imageproviders.md b/articles/imagesharp.web/imageproviders.md index 13c66ec27..0db6d9270 100644 --- a/articles/imagesharp.web/imageproviders.md +++ b/articles/imagesharp.web/imageproviders.md @@ -126,3 +126,10 @@ If your source already fits an `IFileProvider`-style model, [`FileProviderImageP - [Image Caches](imagecaches.md) - [Extensibility](extensibility.md) - [Troubleshooting](troubleshooting.md) + +## Practical Guidance + +- Put providers in the order you want requests to be matched. +- Keep original source storage separate from processed cache storage. +- Return accurate source metadata so stale cache detection works. +- Implement a custom provider only when existing filesystem, Azure, or S3 providers do not match the source model. diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 5537a9f19..6e3cdca97 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -115,3 +115,10 @@ When enabled, ImageSharp.Web adds implicit `global using` directives for: - `SixLabors.ImageSharp.Web` You can turn this off by removing the property or setting it to `false`. + +## How to Use These Docs + +- Start with getting started and processing commands to understand the default request pipeline. +- Read configuration, providers, and caches before deploying beyond a single local filesystem setup. +- Read security before exposing arbitrary command URLs to clients. +- Use extensibility only after choosing which pipeline stage actually owns the behavior you need. diff --git a/articles/imagesharp.web/processingcommands.md b/articles/imagesharp.web/processingcommands.md index 04680e377..bc7c4daec 100644 --- a/articles/imagesharp.web/processingcommands.md +++ b/articles/imagesharp.web/processingcommands.md @@ -109,3 +109,11 @@ If `bgcolor` is omitted and the output format cannot represent alpha, transparen - [Securing Requests](security.md) - [Tag Helpers](taghelpers.md) - [Extensibility](extensibility.md) + +## Practical Guidance + +Command order is part of the processing contract. ImageSharp.Web runs processors in the order their first recognized command appears, so URL generation should be treated like pipeline construction. If a custom watermark should happen after resize, generate the resize command first and the watermark command later. + +For normal web delivery, leave auto-orientation enabled unless preserving raw source orientation is intentional. For transparent images converted to opaque formats, specify a background color explicitly so output is predictable across encoders and future defaults. + +Before exposing command URLs publicly, decide whether clients should have a free-form transformation API. HMAC protects generated URLs, while presets reduce the command surface itself. Many applications want both: presets for a small public vocabulary and signing to prevent tampering. diff --git a/articles/imagesharp.web/security.md b/articles/imagesharp.web/security.md index e91cd1ff6..f63490cb3 100644 --- a/articles/imagesharp.web/security.md +++ b/articles/imagesharp.web/security.md @@ -20,6 +20,8 @@ Once a non-empty secret key is configured, ImageSharp command URLs must also inc Use one stable secret across all app instances that must validate the same URLs. Rotating the secret invalidates previously generated signed URLs. +Do not put the HMAC secret in source control or client-side code. Treat it like any other server-side signing secret and load it from configuration, environment variables, or a secret store. + ## How the Default Token Is Computed By default, ImageSharp.Web computes HMAC-SHA256 over a lower-invariant relative URL built from: @@ -30,6 +32,8 @@ By default, ImageSharp.Web computes HMAC-SHA256 over a lower-invariant relative That behavior is important because the middleware strips unknown commands before validation. The easiest way to stay in sync with the server is to let ImageSharp.Web compute the token for you instead of re-implementing the canonicalization rules yourself. +If a proxy, CDN, or URL generator rewrites paths or query strings, make sure it preserves the canonical URL shape used to compute the token. A harmless-looking path-base or casing change can invalidate signatures. + ## Generate Signed URLs on the Server [`RequestAuthorizationUtilities`](xref:SixLabors.ImageSharp.Web.RequestAuthorizationUtilities) is the simplest server-side API for generating a valid token: @@ -101,6 +105,14 @@ That makes requests look like this: Only the named preset is expanded into commands. Other free-form query-string keys are ignored by that parser. You can combine presets with HMAC signing if you want both a small command surface and signed URLs. +## Practical Guidance + +HMAC signing proves that a URL was generated by code that knows the server-side secret. It does not, by itself, decide whether the command surface is a good product API. For public endpoints, decide first which transformations clients should be allowed to request. If the answer is a small set of known variants, presets are usually clearer and safer than exposing free-form `width`, `height`, `quality`, and `format` combinations. + +Keep HMAC keys server-side and load them from configuration or a secret store. Rotating the key invalidates existing signed URLs, so treat rotation as a deployment event and plan cache/CDN behavior around it. Use HTTPS so signed URLs and source paths are not exposed or altered in transit. + +Generate signatures with ImageSharp.Web utilities instead of reimplementing canonicalization. The token is computed over the sanitized command collection and relative URL shape; reverse proxies, CDNs, path-base changes, and query-string rewriting can all affect validation. Test the production URL path, including proxy rewriting, before assuming a signed URL generated locally will validate after deployment. + ## Related Topics - [Configuration and Pipeline](configuration.md) diff --git a/articles/imagesharp.web/taghelpers.md b/articles/imagesharp.web/taghelpers.md index c2813935f..603c0343f 100644 --- a/articles/imagesharp.web/taghelpers.md +++ b/articles/imagesharp.web/taghelpers.md @@ -87,3 +87,10 @@ In the first case, `HmacTokenTagHelper` signs your handwritten command URL. In t - [Processing Commands](processingcommands.md) - [Securing Requests](security.md) - [Extensibility](extensibility.md) + +## Practical Guidance + +- Use tag helpers when Razor should generate command URLs instead of hand-built query strings. +- Let the HMAC tag helper sign generated URLs when request signing is enabled. +- Keep custom tag helper commands aligned with custom processors. +- Inspect the emitted `src` during troubleshooting so command order and token generation are visible. diff --git a/articles/imagesharp.web/troubleshooting.md b/articles/imagesharp.web/troubleshooting.md index db7fee9d1..c3c999683 100644 --- a/articles/imagesharp.web/troubleshooting.md +++ b/articles/imagesharp.web/troubleshooting.md @@ -111,3 +111,10 @@ When an ImageSharp.Web request misbehaves, this order is usually productive: - [Configuration and Pipeline](configuration.md) - [Securing Requests](security.md) - [Extensibility](extensibility.md) + +## Practical Guidance + +- Check middleware order before provider or cache details. +- Confirm the parsed command collection before debugging processor behavior. +- Validate HMAC with the same canonicalization path used by the middleware. +- Separate source misses from cache misses when diagnosing 404s or stale output. diff --git a/articles/imagesharp/bmp.md b/articles/imagesharp/bmp.md index a9d7b3725..fbcac34b5 100644 --- a/articles/imagesharp/bmp.md +++ b/articles/imagesharp/bmp.md @@ -8,6 +8,12 @@ ImageSharp exposes BMP-specific APIs through [`BmpEncoder`](xref:SixLabors.Image BMP is best thought of as a straightforward bitmap container rather than a delivery format optimized for file size. +BMP files are useful when another system expects simple Windows bitmap data, but they are rarely the best choice for public delivery. Depending on bit depth, the file may store direct color values or palette indexes. Lower bit depths require palette generation and therefore can change colors in the same way other indexed formats do. + +The format's simplicity is also its main tradeoff. BMP output can be easy for older tools to consume, but it usually produces much larger files than PNG, WebP, or QOI for ordinary application assets. Use it because the receiving workflow expects BMP, not because it is generally efficient. + +Transparency support is limited and workflow-dependent. In ImageSharp, `SupportTransparency` applies to 32-bit BMP output. If alpha preservation is the main requirement, PNG or WebP is usually a better default. + A few practical implications: - ImageSharp can write BMP output at 1, 2, 4, 8, 16, 24, or 32 bits per pixel. @@ -40,6 +46,8 @@ The most commonly used `BmpEncoder` options are: - `SupportTransparency` enables BMP alpha support for 32-bit output. - `Quantizer` and `PixelSamplingStrategy` matter when you target indexed BMP output such as 1, 4, or 8 bits per pixel. +When targeting 1, 2, 4, or 8 bits per pixel, the encoder must map source colors into a limited palette. That can be useful for compatibility with old systems, but it should be treated as a color-reduction step. For 24-bit or 32-bit BMP output, the file is larger but avoids indexed palette tradeoffs. + ## Read BMP Metadata Use `GetBmpMetadata()` to inspect BMP-specific metadata: @@ -92,3 +100,10 @@ BMP is usually a poor fit when: - You want a modern web-oriented format. For most application and web output, [PNG](png.md), [WebP](webp.md), or [QOI](qoi.md) are usually better starting points. + +## Practical Guidance + +- Use BMP mainly for compatibility with software that specifically expects it. +- Expect large files compared with modern compressed formats. +- Choose bit depth deliberately when interoperating with older Windows-oriented tooling. +- Prefer PNG or WebP when the same image is intended for storage, delivery, or web use. diff --git a/articles/imagesharp/colorandeffects.md b/articles/imagesharp/colorandeffects.md index 9885a6eb1..f785bf9c8 100644 --- a/articles/imagesharp/colorandeffects.md +++ b/articles/imagesharp/colorandeffects.md @@ -19,6 +19,8 @@ image.Mutate(x => x.Grayscale()); ImageSharp also supports [`GrayscaleMode`](xref:SixLabors.ImageSharp.Processing.GrayscaleMode) when you need a specific conversion mode. +Grayscale conversion is not just averaging red, green, and blue. Different modes weight channels differently, and those choices affect perceived brightness. Use a specific `GrayscaleMode` when output must match a known visual or analytical expectation. + ## Apply a Sepia Tone Use `Sepia()` for a classic warm-tone effect: @@ -49,6 +51,8 @@ image.Mutate(x => x Values greater than `1` increase the effect. Values less than `1` reduce it. +Brightness and contrast are simple global operations. They are useful for quick output tuning, but they do not replace tone mapping, exposure recovery, or color-managed workflows. Apply them after geometry changes when the effect is meant for the final exported image. + ## Shift Hue and Saturation Use `Hue()` and `Saturate()` when you want to push color balance or intensity: @@ -64,6 +68,8 @@ image.Mutate(x => x .Saturate(1.25F)); ``` +Hue shifts rotate color relationships, while saturation changes color intensity. Both can create out-of-gamut or unnatural-looking results if pushed too far. Use small values for photographic correction and stronger values for intentional stylized output. + ## Adjust Opacity Use `Opacity()` to reduce alpha values across the image: @@ -79,6 +85,8 @@ image.Mutate(x => x.Opacity(0.5F)); This is most useful when working with images that already include transparency. +Opacity changes alpha values; it does not composite the image onto a background. If the final format cannot store alpha, use `BackgroundColor()` or another compositing step before saving. + ## Use ColorMatrix for Custom Filters [`ColorMatrix`](xref:SixLabors.ImageSharp.ColorMatrix) is the low-level type for custom channel transforms. It is a 5x4 matrix over the color and alpha channels, and [`Filter()`](xref:SixLabors.ImageSharp.Processing.FilterExtensions.Filter*) applies that matrix to the image. @@ -147,3 +155,7 @@ As with other processors, order matters when combining effects. - [Processing Images](processing.md) - [Rotate, Flip, and Auto-Orient](orientation.md) - [Crop, Pad, and Canvas](cropandcanvas.md) + +## Practical Guidance + +Apply color effects after geometry changes when the effect is output-specific. Keep source images in a suitable working color space before judging color adjustments, and use explicit encoder settings afterward so compression does not hide the result you tuned. Test effects on representative images, because a setting that flatters one sample can damage skin tones, brand colors, gradients, or shadows elsewhere. diff --git a/articles/imagesharp/configuration.md b/articles/imagesharp/configuration.md index 913602704..69ccd9b93 100644 --- a/articles/imagesharp/configuration.md +++ b/articles/imagesharp/configuration.md @@ -99,3 +99,10 @@ Use a custom or cloned configuration when: - [Memory Management](memorymanagement.md) - [Interop and Raw Memory](interop.md) - [Troubleshooting](troubleshooting.md) + +## Practical Guidance + +- Use the default configuration unless you have a specific format, allocator, parallelism, or stream behavior to change. +- Clone configuration for targeted overrides instead of mutating global defaults. +- Restrict formats at trust boundaries when your workload only supports a known subset. +- Profile before changing allocator, buffer, or parallelism settings. diff --git a/articles/imagesharp/cropandcanvas.md b/articles/imagesharp/cropandcanvas.md index 78c8ec9da..0350f8b57 100644 --- a/articles/imagesharp/cropandcanvas.md +++ b/articles/imagesharp/cropandcanvas.md @@ -23,6 +23,8 @@ This removes everything outside the requested bounds. The crop rectangle is expressed in the image's current coordinate space. If the source may contain EXIF orientation, call `AutoOrient()` before choosing crop coordinates that should match what a person sees. +Cropping changes the image size and shifts the remaining pixels so the cropped rectangle becomes the new image. Any coordinates you calculated before the crop no longer refer to the same positions afterward. In workflows that add overlays, annotations, or drawing after cropping, calculate those later positions against the post-crop image. + ## Crop by Width and Height If the crop should start at the top-left corner, you can pass just width and height: @@ -36,6 +38,8 @@ using Image image = Image.Load("input.jpg"); image.Mutate(x => x.Crop(800, 600)); ``` +This overload is intentionally simple: it keeps the top-left region of the current image. Use the rectangle overload when the crop needs to be centered, anchored, or based on detected content. + ## Pad to a Larger Canvas Use `Pad()` when you want to enlarge the canvas without scaling the image: @@ -68,6 +72,8 @@ image.Mutate(x => x.BackgroundColor(Color.White)); This is a common step before saving a transparent source image to a format that does not support transparency. +The background color becomes real pixel data. If you flatten before resizing, the background participates in interpolation at transparent edges. If you resize first and flatten later, transparent edge pixels are resized with alpha preserved and then composited onto the chosen background. For logos and cutouts, the difference can be visible around antialiased edges. + ## Crop Automatically Based on Content Use `EntropyCrop()` when you want ImageSharp to trim low-information borders automatically: @@ -108,3 +114,7 @@ Cropping first can reduce the amount of pixel data that later processors need to - [Processing Images](processing.md) - [Resizing Images](resize.md) - [Rotate, Flip, and Auto-Orient](orientation.md) + +## Practical Guidance + +Normalize orientation before choosing crop rectangles that should match what users see. Keep source-region decisions separate from final canvas-size decisions: crop decides what pixels survive, resize decides how large they become, and padding decides how much output room surrounds them. Use automatic cropping for cleanup, but prefer explicit rectangles, anchors, or resize options when the output dimensions are part of a layout contract. diff --git a/articles/imagesharp/cur.md b/articles/imagesharp/cur.md index e700a40d1..ca931b419 100644 --- a/articles/imagesharp/cur.md +++ b/articles/imagesharp/cur.md @@ -8,6 +8,10 @@ ImageSharp exposes CUR-specific APIs through [`CurEncoder`](xref:SixLabors.Image CUR is best thought of as a cursor container rather than a normal image file format. +CUR shares much of the icon-container shape with ICO, but cursor files add hotspot coordinates. The hotspot is the active point of the cursor: for an arrow it is normally the tip; for a crosshair it may be the center. If the hotspot is wrong, the image can look correct while clicking and hit testing feel wrong. + +Like ICO, a CUR file can contain multiple embedded images for different sizes. Consumers can choose a frame based on display scale or cursor size. Hotspot metadata is per frame, so every embedded cursor image needs correct hotspot coordinates. + A few practical implications: - Existing CUR files can contain one or more cursor images. @@ -43,6 +47,8 @@ The most useful CUR-specific values live on [`CurFrameMetadata`](xref:SixLabors. [`CurMetadata`](xref:SixLabors.ImageSharp.Formats.Cur.CurMetadata) mirrors the root frame's compression, bit depth, and color-table information at the image level. +Treat hotspot coordinates as part of the user interaction contract. They should be chosen from the cursor design, not copied blindly from another size unless the coordinate scales correctly. + ## Read CUR Metadata Use `Image.Identify()` when you want cursor metadata without a full decode: @@ -76,3 +82,10 @@ CUR is usually a poor fit when: - You want broad compatibility outside Windows cursor workflows. For Windows icon assets without cursor hotspots, see [ICO](ico.md). + +## Practical Guidance + +- Treat hotspot coordinates as part of the cursor asset, not incidental metadata. +- Validate all embedded frames when generating multi-size cursor files. +- Use CUR only when the output is meant to behave as a cursor. +- Use ICO, PNG, or another ordinary image format when hotspot metadata is not required. diff --git a/articles/imagesharp/exr.md b/articles/imagesharp/exr.md index 554551cb9..c8fee9a04 100644 --- a/articles/imagesharp/exr.md +++ b/articles/imagesharp/exr.md @@ -8,6 +8,12 @@ ImageSharp supports OpenEXR read and write workflows and exposes EXR-specific me OpenEXR is best thought of as a high-precision interchange format rather than a delivery format. +OpenEXR is designed for scene-referred and high-dynamic-range image data. Values are often intended to survive rendering, compositing, lighting, or color-grading steps before they are tone mapped for display. That makes EXR very different from web formats, where pixels are usually already display-referred. + +The pixel type matters. Half-float storage is common because it gives much more range than 8-bit formats while keeping file size lower than full 32-bit float data. Full float output is useful when the pipeline needs the extra precision. Unsigned integer storage exists for workflows that require it, but it is not the common "HDR image" choice. + +Compression should be chosen with the consuming pipeline in mind. ZIP and ZIPS are both lossless options, but they organize compression differently. Renderer, compositor, and asset-pipeline expectations are often more important than theoretical compression ratios. + A few practical implications: - OpenEXR is common in VFX, rendering, compositing, and HDR-oriented workflows. @@ -41,6 +47,8 @@ The most commonly used `ExrEncoder` options are: - `PixelType` controls whether channels are written as `Half`, `Float`, or `UnsignedInt`. - `Compression` controls the current EXR encoder compression mode. Use `None`, `Zip`, or `Zips`. +Do not use OpenEXR only because an image is "high quality." Use it when the numeric range and precision are needed by the pipeline. If the next step is ordinary display, web delivery, or a thumbnail, tone mapping or conversion to a delivery format is usually the next deliberate step. + ## Read OpenEXR Metadata Use `GetExrMetadata()` to inspect EXR-specific metadata: @@ -78,3 +86,10 @@ OpenEXR is usually a poor fit when: - You want the broadest ecosystem compatibility for day-to-day assets. For everyday application and web output, [PNG](png.md), [JPEG](jpeg.md), [WebP](webp.md), and [TIFF](tiff.md) are usually easier starting points. + +## Practical Guidance + +- Use OpenEXR when high dynamic range, floating-point data, or rendering-pipeline interchange is the real requirement. +- Keep tone mapping and display conversion separate from storing scene-referred data. +- Test compression and channel layout with the downstream renderer or compositing tool. +- Prefer TIFF, PNG, or WebP when the workflow does not need EXR-specific precision or metadata. diff --git a/articles/imagesharp/formatconversion.md b/articles/imagesharp/formatconversion.md index fc8498799..ee82dadc1 100644 --- a/articles/imagesharp/formatconversion.md +++ b/articles/imagesharp/formatconversion.md @@ -1,9 +1,11 @@ # Convert Between Formats -Format conversion is one of the most common reasons people adopt ImageSharp in the first place. The nice part is that you usually do not have to think in terms of format-to-format adapters; you load into ImageSharp's common image model, make any changes you need, and then save with the target encoder. +Format conversion is one of the most common reasons people adopt ImageSharp in the first place. The nice part is that you usually do not have to think in terms of format-to-format adapters; you load into ImageSharp's common image model, make any changes you need, and then save to the destination path, stream, or encoder. That decode-and-re-encode flow is not a blind one. Once an image is loaded, the processing pipeline works with format-agnostic pixel data, while the metadata layer still carries enough information for the destination format to choose the best representation it supports. +For common conversions, saving to a destination path or format is intentionally useful. ImageSharp combines the decoded image, bridged metadata, pixel information, and registered encoder defaults to produce strong automated output. Use explicit encoders when your application has a specific output policy to express, not because the default conversion path is something to avoid. + ## How ImageSharp Bridges Formats ImageSharp's built-in codec metadata translates through [`FormatConnectingMetadata`](xref:SixLabors.ImageSharp.Formats.FormatConnectingMetadata) and [`FormatConnectingFrameMetadata`](xref:SixLabors.ImageSharp.Formats.FormatConnectingFrameMetadata). Those bridge types carry the common image and frame semantics that can be shared across formats, including: @@ -13,18 +15,18 @@ ImageSharp's built-in codec metadata translates through [`FormatConnectingMetada - Indexed-color settings such as shared color table mode. - Animation settings such as background color, repeat count, frame duration, blend mode, and disposal mode. -That is why ImageSharp's conversion story is more comprehensive than simply decoding everything to one in-memory layout and forgetting how the source was encoded. For example, PNG metadata can derive palette, grayscale, RGB, or RGBA output and choose 1, 2, 4, 8, or 16-bit encoding from bridged pixel information, while GIF metadata can carry indexed color-table mode and repeat-count behavior forward when the target format supports them. +That is why ImageSharp's conversion story is more comprehensive than simply decoding everything to one in-memory layout and forgetting how the source was encoded. For example, PNG metadata can derive palette, grayscale, RGB, or RGBA output and choose 1, 2, 4, 8, or 16-bit encoding from bridged pixel information, while GIF metadata can carry indexed color-table mode and repeat-count behavior forward when the target format supports them. These bridges are what make the automatic conversion APIs useful for real application workflows rather than only toy examples. ## Use Identify to Plan the Conversion -Before converting, [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify*) can tell you how the source is encoded. [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) exposes [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo), including: +You do not need to preflight every conversion. Use [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify*) when routing depends on how the source is encoded, or when you want to choose a different destination format before paying the cost of a full decode. [`ImageInfo.PixelType`](xref:SixLabors.ImageSharp.ImageInfo.PixelType) exposes [`PixelTypeInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo), including: - [`BitsPerPixel`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.BitsPerPixel) - [`ColorType`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.ColorType) - [`AlphaRepresentation`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.AlphaRepresentation) - [`ComponentInfo`](xref:SixLabors.ImageSharp.PixelFormats.PixelTypeInfo.ComponentInfo) for component count and precision -This is useful when you need to decide whether to flatten transparency for JPEG, keep higher-precision data in PNG, TIFF, or OpenEXR, or preserve indexed workflows where the target format supports them. +This is useful when you need to decide whether to flatten transparency for JPEG, keep higher-precision data in PNG, TIFF, or OpenEXR, preserve indexed workflows where the target format supports them, or select between several acceptable delivery formats. ## Convert PNG to JPEG @@ -49,7 +51,7 @@ Choose the flattening color deliberately. White is common for documents and many ## Convert JPEG to WebP -Use a WebP encoder when you want to move a photographic source to a more modern delivery format: +Save with a WebP extension for the default WebP output, or pass a WebP encoder when you want to set a delivery policy such as lossy output and a specific quality value: ```csharp using SixLabors.ImageSharp; @@ -72,18 +74,17 @@ PNG is a good target when you want lossless output or transparency support: ```csharp using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; using Image image = Image.Load("input.bin"); -image.Save("output.png", new PngEncoder()); +image.Save("output.png"); ``` PNG is not automatically the best "safe" target for every input. It preserves sharp graphics and transparency well, but photographic sources can become much larger than JPEG or WebP. Use PNG when lossless output, alpha, indexed color, or broad compatibility matter more than smallest file size. ## Choose the Output Based on Pixel Info -When you want format conversion to respect the source characteristics, inspect the encoded pixel type first and then choose the encoder accordingly: +When you need to implement a routing policy, inspect the encoded pixel type first and then choose the destination accordingly: ```csharp using SixLabors.ImageSharp; @@ -118,8 +119,14 @@ else - Converting from a lossy format to a lossless format does not restore discarded detail. - Converting a transparent image to JPEG requires flattening or compositing first. -- ImageSharp uses bridged metadata and pixel-type information to pick good destination settings when the target format can represent them. -- If you care about exact output tradeoffs, use an explicit encoder rather than relying only on the file extension. +- ImageSharp uses bridged metadata, pixel-type information, and encoder defaults to pick good destination settings when the target format can represent them. +- Save-by-extension is the simplest and recommended path for ordinary conversions. Pass an explicit encoder when you want to override defaults for quality, compression, bit depth, palette behavior, metadata handling, or another application policy. - Format conversion is also a metadata decision. Decide whether orientation, color profiles, animation timing, and authoring metadata should be preserved, transformed, or stripped. For more on format behavior and encoder options, see [Image Formats](imageformats.md). For more on inspecting pixel types before a conversion, see [Read Image Info Without Decoding](identify.md) and [Pixel Formats](pixelformats.md). + +## Practical Guidance + +For everyday conversion, let ImageSharp do the normal thing: load the source, apply any processing you need, and save to the destination path or format. The conversion layer carries format-agnostic metadata and pixel information forward so encoders can choose strong defaults. This is a real feature of the library, especially for automated services that accept multiple input formats and produce a consistent output type. + +Add policy only where policy is genuinely needed. A transparent PNG converted to JPEG still needs an explicit background color because JPEG cannot represent alpha. An animated input needs a target format that can represent frame timing and disposal behavior if animation must survive. A public API, cache, or asset pipeline may want fixed quality, compression, bit depth, palette behavior, or metadata handling. Those are reasons to pass an explicit encoder, but they are refinements on top of a capable automated conversion model rather than a workaround for it. diff --git a/articles/imagesharp/gif.md b/articles/imagesharp/gif.md index 6c22e59b5..9b3fbdf0d 100644 --- a/articles/imagesharp/gif.md +++ b/articles/imagesharp/gif.md @@ -8,6 +8,12 @@ In ImageSharp, GIF encoding is built on a quantizing animated encoder, which mea GIF is fundamentally a palette format. Each frame is limited to indexed colors rather than storing full true-color pixel data, which is why quantization and palette choice matter so much. +GIF stores each pixel as an index into a color table. A frame can use a global color table shared by the animation or a local color table for that frame. That design keeps the format simple and compatible, but it means full-color source images must be reduced to a limited palette before they can be written. + +Transparency in GIF is also index based. A palette entry can be treated as transparent, but GIF does not have smooth per-pixel alpha like PNG or WebP. Soft edges, shadows, and semitransparent UI elements can therefore look jagged or require a matte color baked into the pixels. + +Animation behavior is controlled by frame metadata. Frame delay, disposal mode, transparency index, repeat count, and color table mode all affect how the animation plays. When a converted GIF looks wrong, the issue is often frame metadata rather than the pixels alone. + A few practical implications: - GIF is well known and widely compatible for simple animations. @@ -54,6 +60,10 @@ Because `GifEncoder` inherits from [`QuantizingAnimatedImageEncoder`](xref:SixLa - `PixelSamplingStrategy` - `TransparentColorMode` +A global palette can keep animation output more consistent and can reduce overhead when frames share a similar color set. Local palettes can improve quality when frames differ significantly, but they can increase file size and make palette behavior harder to reason about. Choose based on the animation, not as a fixed rule. + +`RepeatCount` controls looping. A value of `0` represents infinite looping in normal GIF usage. Frame delays are stored on frame metadata, so set them on the frames whose timing matters rather than assuming the encoder will infer the intended animation speed. + ## Quantization and Palette Control Every GIF encode in ImageSharp is a quantization step, because GIF stores indexed palette entries rather than full true-color pixels. If you do nothing, ImageSharp will still build a palette for you, but for gradients, photographic frames, UI art, or brand colors it is often worth controlling the quantizer explicitly. @@ -134,3 +144,11 @@ GIF is usually a poor fit when: - You need modern transparency behavior. For a step-by-step multi-frame workflow, see [Working with Animations](animations.md). For a more modern animated format, see [WebP](webp.md). + +## Practical Guidance + +Use GIF for compatibility. It remains useful for simple looping animations and legacy-friendly workflows, but it is palette-constrained and rarely the most efficient modern animated format. + +Control quantization deliberately because GIF quality depends heavily on palette choice. Gradients, photos, and subtle color changes can degrade quickly if the palette is poorly matched. Dithering can hide banding, but it can also add visible texture. + +When converting existing animations, inspect frame delay, disposal mode, transparency, and repeat count. Those values define the animation behavior just as much as the pixels do. Prefer animated WebP or APNG when modern compression, alpha behavior, or color quality matters more than legacy support. diff --git a/articles/imagesharp/ico.md b/articles/imagesharp/ico.md index 08f109579..143f219ab 100644 --- a/articles/imagesharp/ico.md +++ b/articles/imagesharp/ico.md @@ -8,6 +8,12 @@ ImageSharp exposes ICO-specific APIs through [`IcoEncoder`](xref:SixLabors.Image ICO is best thought of as a container for one or more icon images. +An ICO file can contain multiple frames for different icon sizes and bit depths. That lets Windows and other consumers choose the most appropriate embedded image for a particular scale or display context. A single-frame ICO can work, but multi-size icon assets are often more useful in real applications. + +Frames can be stored using BMP-style data or PNG-compressed data. PNG-compressed icon frames are common for larger or more detailed icons because they can preserve alpha and reduce file size. BMP-style frames can still matter for older compatibility workflows. + +The encoded width and height are part of the frame metadata, not just a consequence of the decoded ImageSharp frame size. When generating icons, keep frame metadata aligned with the asset sizes your target platform expects. + A few practical implications: - Existing ICO files can contain one or more embedded icon images. @@ -45,6 +51,8 @@ The most useful ICO-specific values live on [`IcoFrameMetadata`](xref:SixLabors. [`IcoMetadata`](xref:SixLabors.ImageSharp.Formats.Ico.IcoMetadata) mirrors the root frame's compression, bit depth, and color-table information at the image level. +Treat each frame as an icon candidate, not just a page in an image sequence. If you create a multi-frame ICO, inspect every frame's dimensions, compression, and bit depth before saving. + ## Read ICO Metadata Use `Image.Identify()` when you want to inspect the icon container without decoding every embedded image: @@ -78,3 +86,10 @@ ICO is usually a poor fit when: - You want a broadly portable web or application image format. For ordinary image delivery or storage, [PNG](png.md), [WebP](webp.md), and [JPEG](jpeg.md) are usually better choices. + +## Practical Guidance + +- Include the sizes your target platform expects instead of assuming one frame is enough. +- Inspect frame metadata when converting existing icons so dimensions and compression remain intentional. +- Use ICO for Windows icon assets, not general image storage. +- Keep source artwork separately; generated icon files are usually deployment artifacts. diff --git a/articles/imagesharp/identify.md b/articles/imagesharp/identify.md index b4b4c830d..94aac304b 100644 --- a/articles/imagesharp/identify.md +++ b/articles/imagesharp/identify.md @@ -99,3 +99,10 @@ Console.WriteLine(imageInfo.Height); - Identification is not a replacement for decode-time error handling. It is a cheap preflight step; malformed input can still fail later when pixels are decoded. For more detail, see [Loading, Identifying, and Saving](loadingandsaving.md), [Working with Metadata](metadata.md), [Convert Between Formats](formatconversion.md), and [Pixel Formats](pixelformats.md). + +## Practical Guidance + +- Use `DetectFormat(...)` for routing by encoded format only. +- Use `Identify(...)` when dimensions, frame count, pixel type, or metadata affect the decision. +- Use `GetPixelMemorySize()` before decoding untrusted or very large inputs. +- Still handle decode failures; identification is preflight, not a guarantee that the full image is valid. diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index 8638d9020..810273f18 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -179,3 +179,10 @@ The right encoder settings depend on the tradeoff you want to make between: - Image quality The format-specific pages below are the best place to start when you need to tune those tradeoffs. + +## Practical Guidance + +- Use explicit encoders when output behavior matters; file extensions are convenient but hide important defaults. +- Inspect the source with `Identify(...)` before conversion when alpha, animation, bit depth, or metadata changes the output decision. +- Treat metadata as part of format conversion: orientation, ICC profiles, animation timing, and comments may or may not survive a target format. +- Register only the formats your application needs when you want a smaller or more controlled decoding surface. diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index 1ae36295c..fd5a00efb 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -120,3 +120,10 @@ When enabled, ImageSharp adds implicit `global using` directives for: - `SixLabors.ImageSharp.Processing` You can turn this off by removing the property or setting it to `false`. + +## How to Use These Docs + +- Start with loading, identifying, processing, and saving if you are new to ImageSharp. +- Move to formats, metadata, color profiles, and security before accepting untrusted images in production. +- Use pixel-buffer and interop pages when you need direct memory access rather than normal processors. +- Read the migration pages when replacing APIs whose image model differs from ImageSharp's typed pixel model. diff --git a/articles/imagesharp/interop.md b/articles/imagesharp/interop.md index 6f5643088..e45f9436e 100644 --- a/articles/imagesharp/interop.md +++ b/articles/imagesharp/interop.md @@ -216,3 +216,10 @@ That is often the right move if the wrapped buffer has awkward lifetime rules, i - [Memory Management](memorymanagement.md) - [Troubleshooting](troubleshooting.md) - [Migrating from System.Drawing](migratingfromsystemdrawing.md) + +## Practical Guidance + +- Use `LoadPixelData(...)` when ImageSharp should own a copy of the pixels. +- Use `WrapMemory(...)` only when the external buffer lifetime is clearly controlled. +- Respect stride when importing or exporting foreign buffers. +- Clone wrapped images before operations that may require a different buffer shape or ownership model. diff --git a/articles/imagesharp/jpeg.md b/articles/imagesharp/jpeg.md index e20be841c..2841bd8d4 100644 --- a/articles/imagesharp/jpeg.md +++ b/articles/imagesharp/jpeg.md @@ -6,6 +6,12 @@ JPEG remains the workhorse format for photographs on the web and in many applica JPEG uses lossy compression. That means it reduces file size by permanently discarding some image information, which is usually acceptable for photos but much more noticeable on sharp edges, text, UI assets, or repeated save cycles. +JPEG is built around the assumption that photographic images can lose some high-frequency detail without the loss being obvious. It divides image data into blocks, transforms those blocks into frequency information, and quantizes that information according to the requested quality. This is why artifacts often appear as blockiness, ringing around edges, or smearing in areas that were originally detailed. + +Most JPEG workflows also use chroma subsampling: color detail is stored at lower resolution than brightness detail because human vision is usually more sensitive to luminance than chroma. That is very effective for photos, but it can make saturated text, icons, and UI edges look soft or discolored. If a file contains sharp colored edges, compare JPEG output carefully against PNG or WebP. + +JPEG has no alpha channel and no animation model. If the input contains transparency, the transparent pixels must be flattened onto a background before encoding. If the input is animated, save to a format that supports animation or choose a single frame deliberately. + A few practical implications: - JPEG is usually excellent for photos and gradients. @@ -42,6 +48,10 @@ The most commonly used `JpegEncoder` options are: JPEG is a lossy format and does not preserve alpha transparency. If the source image includes transparency, composite it onto a background first. +`Quality` is not a percentage of original image quality. It controls quantization strength, and the visual difference between values is not linear. A move from 95 to 85 may save a lot of bytes with little visual change on many photos, while a move from 45 to 35 can be much more obvious. Pick values by testing representative images at the sizes you actually serve. + +Progressive JPEG stores the image in multiple refinement passes. Browsers can show a rough version before the full file has arrived, which can improve perceived loading behavior for large images. Baseline JPEG is simpler and still broadly supported. Choose progressive output when public image delivery benefits from progressive rendering; choose baseline if a downstream system has strict compatibility requirements. + ## Read JPEG Metadata You can inspect format-specific metadata through `GetJpegMetadata()`: @@ -61,6 +71,8 @@ General image metadata such as EXIF and ICC profiles remains available through [ ImageSharp also exposes [`JpegDecoderOptions`](xref:SixLabors.ImageSharp.Formats.Jpeg.JpegDecoderOptions) for specialized JPEG decoding scenarios, including decoder-specific resize behavior. +Decoder-specific resizing can be useful when you only need a smaller representation of a large JPEG. It can reduce work before a full ImageSharp resize pipeline runs, but it should be treated as a decode optimization rather than a replacement for layout-aware resizing with `ResizeOptions`. + ## When to Use JPEG JPEG is usually a good fit when: @@ -76,3 +88,11 @@ JPEG is usually a poor fit when: - You need an alpha channel. If you need lossless output or alpha transparency, start with [PNG](png.md) or [WebP](webp.md) instead. + +## Practical Guidance + +Set `Quality` explicitly for public output. JPEG quality is a product decision that balances file size and visible artifacts, so it should be visible in code rather than inherited from whatever default is active. Test the value against representative photos, not only one sample image. + +Flatten transparent sources before saving as JPEG because the format has no alpha channel. Choose the background color deliberately; white is common, but product photos, logos, and UI previews often need a different page or brand background. + +Keep ICC metadata or convert to a known output profile when color consistency matters. Avoid repeated JPEG-to-JPEG saves in editing workflows because each lossy encode can discard additional detail. If users edit repeatedly, keep a higher-fidelity working source and encode JPEG only at the export boundary. diff --git a/articles/imagesharp/loadingandsaving.md b/articles/imagesharp/loadingandsaving.md index 9a3481ef7..3f5fbb3c1 100644 --- a/articles/imagesharp/loadingandsaving.md +++ b/articles/imagesharp/loadingandsaving.md @@ -83,7 +83,6 @@ using Image image = Image.Load("input.jpg"); image.Save("output.png"); ``` -If you want to preserve the original encoded format after processing, reuse the decoded format stored in metadata: If you save by path, ImageSharp already chooses the encoder from the destination file extension. Use `DecodedImageFormat` when you want to explicitly save to the originally decoded format, especially when writing to a stream: ```csharp @@ -100,6 +99,8 @@ if (image.Metadata.DecodedImageFormat is not null) `DecodedImageFormat` is only populated for images that were decoded from an existing source. Images created from scratch do not have an original encoded format to preserve. +Preserving the original format is not always the right choice. Choose the output format based on the job: JPEG or WebP for photographic delivery, PNG for lossless graphics or transparency, GIF/APNG/WebP for animations, TIFF or OpenEXR for workflows that need richer image data. + ## Choose Encoders Explicitly When you need control over output settings, pass an encoder directly: @@ -137,6 +138,16 @@ using Image image = Image.Load(options, "animated.webp"); These options let you limit decoded frames, skip metadata work, or decode directly to a target size when the format supports it. +Use `DecoderOptions` at trust boundaries. For upload validation, background queues, and web requests, it is better to decide frame limits, metadata policy, color-profile handling, and target decode size before allocating a full image. + +## Practical Guidance + +For production code, decide how much information you need before you decode pixels. `DetectFormat(...)` is the cheapest useful step when the only question is "which decoder would handle this?". `Identify(...)` is the better preflight when routing, validation, or policy depends on dimensions, frame count, encoded pixel type, or metadata. `Load(...)` should be the point where you have already decided the image is worth decoding. + +Streams must remain open and readable until the load operation completes. In web and queue-based systems, prefer the async overloads so image I/O follows the rest of the application’s asynchronous flow. Once the image is decoded, treat it as a significant resource: decoded pixel buffers can be much larger than the source file, especially for high-resolution photos and multi-frame formats. + +Saving deserves the same deliberate boundary. Save by extension for quick tools and samples; pass an explicit encoder when output quality, metadata, color profiles, animation settings, or compression tradeoffs are part of the contract. If a file crosses an API boundary, is cached publicly, or is compared in tests, the encoder settings should usually be visible in code. + ## Related Topics - [Working with Metadata](metadata.md) diff --git a/articles/imagesharp/metadata.md b/articles/imagesharp/metadata.md index 6fcd572d7..57a30e81d 100644 --- a/articles/imagesharp/metadata.md +++ b/articles/imagesharp/metadata.md @@ -34,6 +34,10 @@ Depending on the source format, `ImageMetadata` can expose several common profil These profile properties are nullable because not every image carries every kind of metadata. +Those profiles serve different purposes. EXIF often contains camera settings, timestamps, orientation, thumbnails, and sometimes GPS data. ICC profiles describe how color values should be interpreted. CICP metadata can carry color coding information used by some modern image and video workflows. IPTC and XMP often contain editorial, rights, authoring, and workflow data. + +That means metadata policy is not simply "keep" or "strip." A public thumbnail service may want to apply orientation and remove personal data. A print or archival pipeline may need to preserve color profiles and selected descriptive metadata. A conversion tool may need to translate what the destination format can represent and drop what it cannot. + ## Work with Format-Specific Metadata In addition to the common profiles, ImageSharp exposes format-specific metadata helpers: @@ -65,6 +69,8 @@ Console.WriteLine($"Frame count: {imageInfo.FrameMetadataCollection.Count}"); This is useful when inspecting animated formats without decoding every frame into pixel memory. +Frame metadata matters for animation. Delay, blend mode, disposal mode, frame dimensions, and format-specific values can change how a multi-frame image plays even when the decoded pixels look reasonable in isolation. + ## Strip Metadata Before Saving If you do not want to preserve the original metadata, clear the profiles before saving: @@ -94,3 +100,7 @@ ImageSharp preserves metadata by default when the decoder and encoder both suppo - Saving to a different format may change which metadata can be represented in the output. For deeper guidance on loading and saving workflows, see [Loading, Identifying, and Saving](loadingandsaving.md). For ICC and CICP-specific guidance, see [Color Profiles and Color Conversion](colorprofiles.md). + +## Practical Guidance + +Inspect metadata before decoding pixels when routing or validation only needs headers and profiles. Preserve ICC or CICP data when color interpretation matters, or convert to a known output profile before stripping it. Apply `AutoOrient()` before removing EXIF orientation if the visual orientation must remain correct. Treat metadata as user data in privacy-sensitive workflows; EXIF, IPTC, and XMP can contain identifying information. diff --git a/articles/imagesharp/migratingfromskiasharp.md b/articles/imagesharp/migratingfromskiasharp.md index 0ad1d185f..25a36d070 100644 --- a/articles/imagesharp/migratingfromskiasharp.md +++ b/articles/imagesharp/migratingfromskiasharp.md @@ -183,3 +183,10 @@ For most SkiaSharp image migrations: - [Working with Pixel Buffers](pixelbuffers.md) - [Interop and Raw Memory](interop.md) - [Migrating from SkiaSharp in ImageSharp.Drawing](../imagesharp.drawing/migratingfromskiasharp.md) + +## Practical Guidance + +- Keep image load/save behavior equivalent before changing processing behavior. +- Replace pixel storage decisions with explicit `Image` choices. +- Use row processing instead of per-pixel object-style APIs. +- Move canvas drawing concerns to ImageSharp.Drawing rather than forcing them into core ImageSharp processors. diff --git a/articles/imagesharp/migratingfromsystemdrawing.md b/articles/imagesharp/migratingfromsystemdrawing.md index a100e6336..bd3f4a457 100644 --- a/articles/imagesharp/migratingfromsystemdrawing.md +++ b/articles/imagesharp/migratingfromsystemdrawing.md @@ -209,3 +209,10 @@ For most migrations, the least painful path is: - [Working with Pixel Buffers](pixelbuffers.md) - [Interop and Raw Memory](interop.md) - [Pixel Formats](pixelformats.md) + +## Practical Guidance + +- Replace `Bitmap` with the `Image` type that matches your working pixel model. +- Replace `LockBits` loops with row-based processing. +- Keep rendering concerns in ImageSharp.Drawing when the old code used `Graphics`. +- Validate behavior on non-Windows systems when the migration goal is cross-platform support. diff --git a/articles/imagesharp/orientation.md b/articles/imagesharp/orientation.md index 2017f70d1..5f30ddc61 100644 --- a/articles/imagesharp/orientation.md +++ b/articles/imagesharp/orientation.md @@ -86,3 +86,10 @@ That keeps downstream dimensions and crop coordinates aligned with the final vis - [Processing Images](processing.md) - [Crop, Pad, and Canvas](cropandcanvas.md) - [Working with Metadata](metadata.md) + +## Practical Guidance + +- Call `AutoOrient()` early for user-uploaded photos unless preserving raw pixel orientation is intentional. +- Normalize orientation before crop and resize operations based on what a person sees. +- Strip or update orientation metadata only after the pixel data reflects the intended display orientation. +- Test orientation workflows with real phone images, not only images already stored upright. diff --git a/articles/imagesharp/pbm.md b/articles/imagesharp/pbm.md index 28738a601..1f524e54c 100644 --- a/articles/imagesharp/pbm.md +++ b/articles/imagesharp/pbm.md @@ -8,6 +8,10 @@ ImageSharp exposes PNM-specific APIs through [`PbmEncoder`](xref:SixLabors.Image The PNM family is best thought of as a simple interchange family rather than a compact delivery format. +The family covers three related subformats. PBM stores black-and-white images. PGM stores grayscale images. PPM stores RGB images. Each can be useful for tests, examples, and simple tooling because the structure is easy to generate and inspect. + +Plain-text encoding is human-readable, which can be valuable for debugging small fixtures. Binary encoding is more compact and more appropriate for larger files, but it is still not a modern compressed delivery format. The formats do not carry alpha transparency or rich metadata. + A few practical implications: - `PbmColorType.BlackAndWhite` maps to PBM output. @@ -42,6 +46,8 @@ The most commonly used `PbmEncoder` options are: - `ComponentType` selects 1-bit, 8-bit, or 16-bit component storage where that subformat allows it. - `Encoding` selects plain-text or binary pixel encoding. +Choose the subformat from the data model. A mask or thresholded image belongs in PBM, grayscale analysis output belongs in PGM, and ordinary RGB test data belongs in PPM. Choose plain text when inspection matters more than size. + ## Read PNM Metadata Use `GetPbmMetadata()` to inspect PNM-specific metadata: @@ -79,3 +85,10 @@ It is usually a poor fit when: - You need richer metadata, transparency, or modern delivery characteristics. For more compact or full-featured output, start with [PNG](png.md), [WebP](webp.md), or [QOI](qoi.md). + +## Practical Guidance + +- Use Netpbm formats for simple tooling, tests, and interchange workflows where readability matters. +- Avoid them for public delivery or storage where compression, metadata, or alpha support matters. +- Be explicit about plain versus binary encoding when files are consumed by external tools. +- Prefer PNG when you need a simple lossless format with a much broader ecosystem. diff --git a/articles/imagesharp/pixelbuffers.md b/articles/imagesharp/pixelbuffers.md index dfb68dbfc..ade1f72b8 100644 --- a/articles/imagesharp/pixelbuffers.md +++ b/articles/imagesharp/pixelbuffers.md @@ -154,3 +154,10 @@ Keep all row work inside the callback that received the accessor. - [Interop and Raw Memory](interop.md) - [Memory Management](memorymanagement.md) - [Migrating from System.Drawing](migratingfromsystemdrawing.md) + +## Practical Guidance + +- Prefer row access over per-pixel indexers for non-trivial work. +- Keep span usage inside the callback that supplied the row accessor. +- Use `ProcessPixelRowsAsVector4(...)` when logic should be pixel-format agnostic. +- Convert to a known working pixel format when the algorithm benefits from simpler direct access. diff --git a/articles/imagesharp/pixelformats.md b/articles/imagesharp/pixelformats.md index 69e11a279..216c07a7a 100644 --- a/articles/imagesharp/pixelformats.md +++ b/articles/imagesharp/pixelformats.md @@ -56,3 +56,11 @@ In practice, custom `TPixel` types should still fit the same RGBA-compatible con ## Single-Bit Monochrome Pixels ImageSharp does not currently support sub-byte `TPixel` formats such as a true 1-bit pixel type. That trade-off keeps the processing model and API surface much simpler, and it avoids paying a heavy CPU cost across the rest of the pipeline for a niche storage optimization. + +## Choosing a Working Pixel Format + +Use `Image` as the default when you need predictable direct pixel access and no special memory or precision constraint pushes you elsewhere. It is a practical working format for composition, overlays, custom row processing, and interop with APIs that expect RGBA-like data. + +Choose lower-memory formats only when the missing channels or precision are genuinely unnecessary. `L8` is a good fit for masks and grayscale analysis; `Rgb24` can be useful when alpha is not part of the workflow. Choose higher-precision formats because the processing pipeline benefits from them, such as repeated color transforms or high-bit-depth output, not simply because the source file is "high quality". + +Pixel format and color profile are related but separate decisions. `Image` tells you the in-memory channel layout and numeric representation; ICC and CICP handling tell you how color values should be interpreted or converted. A robust pipeline chooses both deliberately. diff --git a/articles/imagesharp/png.md b/articles/imagesharp/png.md index ef2b77a71..babde00e1 100644 --- a/articles/imagesharp/png.md +++ b/articles/imagesharp/png.md @@ -8,6 +8,12 @@ ImageSharp also supports animated PNG metadata and encoding scenarios. PNG is a lossless format. It preserves pixel data exactly, which makes it a strong fit for graphics where edges, text, and flat-color regions need to stay crisp. +PNG compresses image data without discarding pixel information. Before compression, scanline filters can transform rows into forms that compress better. The chosen filter does not change the decoded pixels, but it can change encoding speed and file size. Adaptive filtering lets the encoder choose filters per row and is usually a good default for mixed content. + +PNG supports several pixel representations, including grayscale, grayscale with alpha, RGB, RGB with alpha, and palette-indexed color. That is why `ColorType` and `BitDepth` matter: they decide how pixels are represented in the file, not just how strongly the file is compressed. A screenshot with a small number of colors may be much smaller as a palette PNG, while a translucent UI asset usually needs RGBA-style output. + +PNG can store ancillary information such as gamma, text chunks, and color-management data. Those chunks can be important for appearance or workflow, but they can also increase file size or carry information you do not want to publish. Treat metadata as part of the output decision. + A few practical implications: - PNG is excellent for screenshots, icons, logos, diagrams, and UI assets. @@ -48,6 +54,10 @@ The most commonly used `PngEncoder` options are: Because `PngEncoder` inherits from [`QuantizingAnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.QuantizingAnimatedImageEncoder), it also supports `Quantizer`, `PixelSamplingStrategy`, and `TransparentColorMode` when you are writing palette-based PNG data. +Compression level is a speed-versus-size choice. Higher compression can reduce output size, but it costs more CPU and does not improve image quality because PNG is already lossless. For high-volume services, benchmark realistic images before choosing the slowest compression level globally. + +Adam7 interlacing allows a progressively refined display as bytes arrive. That can help in some delivery scenarios, but it can also increase file size. For small UI assets and cached application images, non-interlaced PNG is often simpler. + ## Quantization and Palette PNGs PNG does not always quantize. Quantization is only part of the encode path when you target a palette PNG by setting [`PngColorType.Palette`](xref:SixLabors.ImageSharp.Formats.Png.PngColorType.Palette). For RGB, RGBA, grayscale, or grayscale-with-alpha PNG output, ImageSharp writes the image in those representations without first reducing it to a palette. @@ -127,3 +137,11 @@ PNG is usually a poor fit when: - You only need a web-first animated format and modern browser-oriented compression matters more than static PNG compatibility. If you want a lossy photographic format, start with [JPEG](jpeg.md). If you want a modern alternative that supports both lossy and lossless output, see [WebP](webp.md). + +## Practical Guidance + +Use PNG when lossless pixels, transparency, screenshots, diagrams, or UI assets matter more than the smallest possible file. It is a strong default for sharp graphics because it avoids lossy artifacts around text, icons, and hard edges. + +When PNG size matters, consider palette output and quantization rather than switching formats immediately. A palette PNG can be much smaller for limited-color graphics, but that choice should be tested against gradients, shadows, and transparency because quantization can introduce visible banding or dithering texture. + +Preserve or convert color profiles intentionally. PNG is often used in workflows where exact appearance matters, so silently dropping profile information can be a real output bug. For photographic delivery where smaller files matter more than lossless pixels, compare JPEG and WebP instead. diff --git a/articles/imagesharp/processing.md b/articles/imagesharp/processing.md index f1a89436f..a87ab24b6 100644 --- a/articles/imagesharp/processing.md +++ b/articles/imagesharp/processing.md @@ -31,6 +31,8 @@ This is the most common choice for request processing, thumbnails, and one-way e Use `Mutate()` when the loaded image is an intermediate value and there is no need to keep the original pixels. This keeps ownership simple and avoids a second full image allocation. +`Mutate()` does not mean "unsafe"; it means the current image instance is the output. That is exactly what you want for many pipelines: load a file, normalize it, resize it, adjust it, and save the result. The important ownership question is whether any later code still needs the original pixels. If not, mutating keeps memory use and code shape straightforward. + ## Clone When You Need to Preserve the Original Use `Clone()` when the original image must remain unchanged: @@ -51,6 +53,8 @@ This is useful when you need multiple derived outputs from the same source image Use `Clone()` when the original image is a reusable source asset: for example, generating several thumbnail sizes, producing multiple export formats, or running a preview operation while keeping an editable original. +`Clone()` creates a separate image with its own pixel buffers. That makes it the right tool for fan-out workflows, but it is not free. If a service generates five output sizes from one upload, cloning for each output may be worth the clarity. If a pipeline only writes one result, cloning usually just allocates another full image for no benefit. + ## Build Ordered Pipelines Processor order matters. For example, auto-orienting before resizing usually produces more predictable results than resizing first and correcting orientation later: @@ -75,6 +79,16 @@ As a rule of thumb: - Apply output-specific effects near the end of the pipeline. - Save with an explicit encoder when output quality, metadata, compression, or compatibility matters. +A useful way to think about processor order is to group the pipeline into stages: + +1. Normalize the source into the coordinate system you intend to work in. +2. Remove pixels you no longer need. +3. Resize or otherwise change geometry. +4. Apply visual effects that depend on the final output. +5. Encode with explicit output settings. + +That ordering is not mandatory, but it gives you a good default. For example, a blur before resize looks different from a blur after resize, and a crop before an expensive effect can reduce the amount of work dramatically. + ## Common Processing Topics - [Resizing Images](resize.md) covers `Resize()` and [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions). @@ -87,3 +101,7 @@ As a rule of thumb: ## Related APIs Most built-in processors live under the [`SixLabors.ImageSharp.Processing`](xref:SixLabors.ImageSharp.Processing) namespace. Import that namespace in files where you build processing pipelines. + +## Practical Guidance + +Use `Mutate()` for one-way processing and `Clone()` when the original image remains a source. Order processors in the same order you would describe the visual transformation, and be especially deliberate around orientation, crop, resize, and effects. Save with explicit encoder options at the final boundary so the processed pixels are not undermined by accidental output defaults. diff --git a/articles/imagesharp/qoi.md b/articles/imagesharp/qoi.md index 3696090fb..fc48efd56 100644 --- a/articles/imagesharp/qoi.md +++ b/articles/imagesharp/qoi.md @@ -8,6 +8,10 @@ ImageSharp exposes QOI-specific APIs through [`QoiEncoder`](xref:SixLabors.Image QOI is best thought of as a small, focused lossless format. +QOI is intentionally simple: it stores RGB or RGBA pixel streams using a compact set of operations that are easy to encode and decode. That simplicity is the appeal. It can be fast and convenient in controlled pipelines where both sides agree to use the format. + +The tradeoff is ecosystem breadth. QOI does not carry the same metadata surface as PNG, WebP, or TIFF, and it is not a browser delivery format. Use it when simple lossless interchange is valuable and the producer and consumer are both under your control. + A few practical implications: - QOI is lossless. @@ -40,6 +44,8 @@ The most useful QOI-specific values are: - `Channels`, which records whether the image is RGB or RGBA. - `ColorSpace`, which records whether the image is tagged as sRGB with linear alpha or all-channels-linear. +Those values describe how consumers should interpret the stored pixel stream. They are not a substitute for full color-management metadata, so QOI is usually a poor choice when ICC workflows or rich metadata are part of the contract. + ## Read QOI Metadata Use `GetQoiMetadata()` to inspect QOI-specific metadata: @@ -69,3 +75,10 @@ QOI is usually a poor fit when: - You need richer metadata or more mature ecosystem support. For wider compatibility, [PNG](png.md) and [WebP](webp.md) are usually better starting points. + +## Practical Guidance + +- Use QOI in controlled pipelines where both producer and consumer agree on the format. +- Prefer PNG or WebP when files need to be opened broadly by browsers, design tools, or operating systems. +- Treat QOI as an interchange or internal-storage choice, not a general publishing format. +- Test size and speed against representative data; simple formats are not automatically smaller for every image. diff --git a/articles/imagesharp/quantization.md b/articles/imagesharp/quantization.md index 0dc944806..0ca6145f0 100644 --- a/articles/imagesharp/quantization.md +++ b/articles/imagesharp/quantization.md @@ -134,3 +134,10 @@ Transparency handling matters most for GIF, palette PNG, ICO, and CUR output. [` - [PNG](png.md) - [Convert Between Formats](formatconversion.md) - [Read Image Info Without Decoding](identify.md) + +## Practical Guidance + +- Choose quantization when palette output is a requirement, not as a default quality improvement. +- Pick dithering based on the content; it can hide banding but add visible texture. +- Use extensive sampling only when rare colors matter enough to justify the extra work. +- Pay special attention to transparency for GIF, palette PNG, ICO, and CUR output. diff --git a/articles/imagesharp/recipes.md b/articles/imagesharp/recipes.md index 464e499da..e25d00072 100644 --- a/articles/imagesharp/recipes.md +++ b/articles/imagesharp/recipes.md @@ -2,6 +2,8 @@ These pages are the fast path through the ImageSharp docs. They skip most of the background explanation and focus on the handful of workflows people reach for over and over again, while linking back to the deeper guides when you need more context. +Use recipes when you already know the outcome you want: "make thumbnails", "convert this upload", "remove metadata", or "inspect before loading". Use the conceptual pages when you need to choose architecture, tune memory, handle untrusted input, or understand why a format behaves differently from another format. + ## Common Tasks - [Generate Thumbnails](thumbnails.md) for fit-within-box and square-crop thumbnail workflows. @@ -16,6 +18,12 @@ These pages are the fast path through the ImageSharp docs. They skip most of the - Choose encoder options deliberately when file size, quality, metadata retention, or color profile behavior matters. - Keep stream ownership clear: load from streams that stay readable for the load call, then save to streams you control. +## Practical Guidance + +The recipe examples show the core workflow, but production image pipelines need policy around the workflow. For untrusted images, put limits around request size, decoded pixel budget, and frame count before you decode the full image. Use `Identify(...)` to make those decisions cheaply when possible, then load only the images your application is willing to process. + +Normalize orientation before generating user-visible derivatives such as thumbnails, crops, or social cards. Decide what happens to metadata and color profiles before export: some workflows need privacy-focused stripping, while others need ICC conversion or preservation. Public output should usually use explicit encoders so format, quality, compression, and metadata behavior do not drift because of a file extension or default setting. + ## Related Topics - [Loading, Identifying, and Saving](loadingandsaving.md) diff --git a/articles/imagesharp/resize.md b/articles/imagesharp/resize.md index e6c49b36f..f19ab974b 100644 --- a/articles/imagesharp/resize.md +++ b/articles/imagesharp/resize.md @@ -4,6 +4,8 @@ Resizing looks simple on the surface, but it is also one of the easiest places t The simple `Resize()` overloads are good for direct width and height changes, while [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) gives you control over fit mode, anchor position, background padding, sampler choice, alpha handling, and manual target rectangles. +Start by choosing the layout promise. If the full image must remain visible, use a fit mode such as `Max` or `Pad`. If the output box must be completely filled, use `Crop` and choose an anchor or focal point. If exact aspect ratio is not important, `Stretch` is available, but it should be a deliberate visual choice. + ## Basic Resize Use the basic overloads when you already know the destination size: @@ -33,12 +35,17 @@ image.Mutate(x => x.Resize(600, 0)); ## Choose the Right Resampler -Resampler choice affects sharpness, smoothness, and aliasing: +Resampling is the part of resizing that decides how source pixels contribute to destination pixels. When shrinking an image, many source pixels must be combined into fewer destination pixels. When enlarging an image, new destination pixels must be estimated from the surrounding source pixels. Different resamplers make different tradeoffs between sharpness, smoothness, ringing, aliasing, and speed. + +[`Bicubic`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Bicubic) is ImageSharp's default because it is a balanced general-purpose choice. It produces smoother results than nearest-neighbor sampling and is less prone to visible ringing than more aggressive high-lobe filters. For many application thumbnails and ordinary web images, it is a good starting point. + +[`Lanczos3`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos3) is often a strong choice for high-quality downscaling because it preserves detail well. That extra sharpness can also produce halos or ringing around hard contrast edges, so it should be tested on screenshots, line art, product photos, and portraits before becoming a global default. Larger Lanczos variants such as `Lanczos5` and `Lanczos8` use wider kernels and can preserve even more detail, but they cost more work and can make ringing more visible. -- [`Bicubic`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Bicubic) is the balanced default. -- [`Lanczos3`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Lanczos3) is a strong choice for high-quality downscaling. -- [`Spline`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Spline) is often a good fit for enlargement. -- [`NearestNeighbor`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.NearestNeighbor) is useful for pixel art and hard-edged imagery. +[`Spline`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Spline), [`CatmullRom`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.CatmullRom), [`MitchellNetravali`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.MitchellNetravali), [`Robidoux`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Robidoux), and [`RobidouxSharp`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.RobidouxSharp) are useful when you are tuning a visual pipeline and want a different balance of softness and edge contrast. The right choice is content-dependent: UI screenshots, portraits, scanned documents, and generated artwork can prefer different filters. + +[`NearestNeighbor`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.NearestNeighbor) does no smoothing. It is the right choice for pixel art, masks, indexed-style data, and any workflow where hard pixel boundaries must remain hard. It is usually the wrong choice for photos because it creates blocky stair-step artifacts. + +Simple filters such as [`Box`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Box), [`Triangle`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Triangle), and [`Hermite`](xref:SixLabors.ImageSharp.Processing.KnownResamplers.Hermite) can be useful when speed, softness, or predictable low-detail output matters more than maximum sharpness. ```csharp using SixLabors.ImageSharp; @@ -49,6 +56,8 @@ using Image image = Image.Load("input.png"); image.Mutate(x => x.Resize(320, 240, KnownResamplers.Lanczos3)); ``` +If you are building a reusable pipeline, choose a default sampler per content type rather than one sampler for everything. For example, product thumbnails, user avatars, pixel-art previews, and scanned documents often deserve different choices. + ## Use ResizeOptions for Real-World Layout Rules [`ResizeOptions`](xref:SixLabors.ImageSharp.Processing.ResizeOptions) is the main API for fit-and-fill workflows. When you use it, set [`Mode`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.Mode) explicitly; its default is [`Crop`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Crop). @@ -79,6 +88,8 @@ The resize modes are: - [`Stretch`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Stretch): ignores aspect ratio and forces the exact size. - [`Manual`](xref:SixLabors.ImageSharp.Processing.ResizeMode.Manual): uses [`TargetRectangle`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.TargetRectangle) to place the resized result explicitly. +For user-visible images, `Crop`, `Pad`, and `Max` cover most layouts. `Manual` is for composition systems where you already calculated the destination rectangle. `Stretch` is mostly for data, masks, or intentionally distorted visual effects. + ## Position, Padding, and Manual Placement `ResizeOptions` also controls where the result lands inside the output canvas: @@ -105,9 +116,9 @@ image.Mutate(x => x.Resize(new ResizeOptions ## Companding and Alpha Handling -[`Compand`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.Compand) enables gamma-companded resizing, which can improve the visual quality of some photographic resizes. It is not always necessary, but it is worth testing when color accuracy matters. +Resizing blends neighboring pixels. If those pixel values are blended directly in a gamma-encoded space, midtones can shift in ways that are visible on gradients, shadows, and high-contrast photographic content. [`Compand`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.Compand) enables gamma-aware resizing so interpolation happens in a space that is often visually more appropriate. It costs extra work, so test it against your image set instead of enabling it blindly everywhere. -[`PremultiplyAlpha`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.PremultiplyAlpha) defaults to `true` and should usually stay enabled for transparent images, because interpolation behaves better when alpha is handled in premultiplied form. +Alpha needs similar care. Transparent images usually look better when color channels are interpolated in premultiplied form, because transparent edge pixels then contribute color in proportion to their coverage. [`PremultiplyAlpha`](xref:SixLabors.ImageSharp.Processing.ResizeOptions.PremultiplyAlpha) defaults to `true` and should normally stay enabled for logos, sprites, UI elements, and cutouts. Turning it off is an advanced choice for data images or pipelines that already handle alpha in a very specific way. ## Decode Smaller When That Is Enough @@ -121,6 +132,14 @@ See [Loading, Identifying, and Saving](loadingandsaving.md) and [Security Consid See [Interop and Raw Memory](interop.md) for the full wrapped-memory guidance. +## Practical Guidance + +A resize should start from the product promise, not from the overload. If a user must see the whole image, choose a fit mode such as `Max` or `Pad`. If the layout must be filled edge-to-edge, choose `Crop` and decide how the crop should be anchored. If you are building a composition engine and already know the exact destination rectangle, use `Manual`. `Stretch` is available, but it should be reserved for cases where distortion is acceptable or meaningful. + +For user-uploaded photos, call `AutoOrient()` before resizing unless preserving the raw encoded pixel orientation is intentional. Crop coordinates, anchors, and focal points are much easier to reason about after the pixels match the way a person sees the image. For very large inputs where only a bounded preview is needed, `DecoderOptions.TargetSize` can reduce decode cost before the resize pipeline runs. + +Resampler choice should be tested against representative images. A sharper result is not always a better result: line art, screenshots, photos, and pixel art often want different tradeoffs. After resizing, save with an explicit encoder when final quality, compression, metadata, or color handling must be predictable. + ## Related Topics - [Processing Images](processing.md) diff --git a/articles/imagesharp/stripmetadata.md b/articles/imagesharp/stripmetadata.md index bcd79e59d..c975855f5 100644 --- a/articles/imagesharp/stripmetadata.md +++ b/articles/imagesharp/stripmetadata.md @@ -52,3 +52,10 @@ This approach is useful when you want to inspect or edit metadata before decidin - If orientation matters, call `AutoOrient()` before stripping EXIF orientation metadata so the pixels are physically normalized. For more detail, see [Working with Metadata](metadata.md). + +## Practical Guidance + +- Use encoder-level `SkipMetadata` when the output should simply omit metadata. +- Clear profiles manually when you need to inspect, keep, or remove specific metadata groups. +- Convert or preserve color profiles intentionally before stripping ICC or CICP data. +- Apply `AutoOrient()` before stripping EXIF orientation metadata when display orientation matters. diff --git a/articles/imagesharp/tga.md b/articles/imagesharp/tga.md index 622a3951d..1764ab638 100644 --- a/articles/imagesharp/tga.md +++ b/articles/imagesharp/tga.md @@ -8,6 +8,10 @@ ImageSharp exposes TGA-specific APIs through [`TgaEncoder`](xref:SixLabors.Image TGA is best thought of as a simple raster format for tooling and interchange. +TGA is common in graphics pipelines because it is straightforward and historically supported by many tools. It can store 8, 16, 24, or 32-bit output, and the alpha-channel bit count is part of the metadata story. That makes it useful when an asset pipeline expects a simple raster file with predictable channel layout. + +Run-length encoding can reduce file size for images with repeated runs of pixels, such as flat-color artwork or masks. It is much less useful for noisy images or photographs. Choose compression based on the assets being exchanged and the expectations of the consuming tool. + A few practical implications: - ImageSharp can write TGA output at 8, 16, 24, or 32 bits per pixel. @@ -39,6 +43,8 @@ The most commonly used `TgaEncoder` options are: - `BitsPerPixel` controls the encoded TGA bit depth. - `Compression` switches between uncompressed and run-length encoded output. +Bit depth controls more than file size. A 32-bit TGA can carry alpha, while lower bit depths may not represent the same source data. Check `AlphaChannelBits` when moving assets through tools that care about alpha channels. + ## Read TGA Metadata Use `GetTgaMetadata()` to inspect TGA-specific metadata: @@ -73,3 +79,10 @@ TGA is usually a poor fit when: - You need richer metadata or broader ecosystem support. For ordinary web or application output, [PNG](png.md), [JPEG](jpeg.md), and [WebP](webp.md) are usually better starting points. + +## Practical Guidance + +- Use TGA when an asset pipeline or graphics toolchain explicitly expects it. +- Check alpha-channel bit depth when moving assets between tools. +- Keep an editable source format alongside generated TGA assets. +- Prefer PNG, JPEG, or WebP for application and web delivery. diff --git a/articles/imagesharp/thumbnails.md b/articles/imagesharp/thumbnails.md index 1dc3234c8..f63b0b42a 100644 --- a/articles/imagesharp/thumbnails.md +++ b/articles/imagesharp/thumbnails.md @@ -89,3 +89,10 @@ image.Save("thumbnail.png", new PngEncoder()); - Use explicit encoders when thumbnail quality, metadata, color profile behavior, or file size needs to be predictable. For more detail on resizing behavior, see [Resizing Images](resize.md). + +## Practical Guidance + +- Choose `Max` when the whole source must remain visible. +- Choose `Crop` when the output box must be fully filled. +- Normalize orientation before generating thumbnails from user-uploaded photos. +- Use explicit encoders so thumbnail quality, metadata, and file size are predictable. diff --git a/articles/imagesharp/tiff.md b/articles/imagesharp/tiff.md index 88f02dddd..22717c344 100644 --- a/articles/imagesharp/tiff.md +++ b/articles/imagesharp/tiff.md @@ -8,6 +8,12 @@ ImageSharp exposes a range of TIFF-specific encoder and metadata options for tho TIFF is best thought of as a flexible imaging container with multiple possible encodings and metadata conventions rather than a single narrow web format. +TIFF can describe images using different photometric interpretations, bit depths, compression schemes, byte orders, and predictors. That flexibility is why it remains useful in scanning, print, archival, and professional imaging workflows, but it also means "TIFF support" varies between tools. A pipeline should choose the specific TIFF shape the downstream system expects. + +Compression is not one-size-fits-all. LZW and deflate-style compression are common lossless choices, and predictors can improve compression by making neighboring sample values easier to encode. Those settings affect file size and compatibility rather than visual quality when the output is lossless. + +TIFF metadata can be part of the workflow contract. Some files carry scanner, camera, print, publishing, or application-specific metadata. Before stripping or rewriting metadata, decide whether another system relies on it. + A few practical implications: - TIFF is common in archival, print, scanning, publishing, and professional imaging workflows. @@ -47,6 +53,8 @@ The most commonly used `TiffEncoder` options are: Some compression and photometric values are defined by the TIFF specification but are not currently supported by the encoder. In those cases, the encoder falls back rather than emitting unsupported output. +Because TIFF has many valid combinations, choose `BitsPerPixel`, `PhotometricInterpretation`, compression, and predictor settings together. For example, an RGB interchange file, a bilevel scanned document, and a higher-bit-depth imaging asset are all TIFF files, but they should not use the same encoder assumptions. + ## Read TIFF Metadata Use `GetTiffMetadata()` to inspect TIFF-specific metadata: @@ -83,3 +91,11 @@ TIFF is usually a poor fit when: - You just need a simple photo or web asset format. For more typical application and web workloads, [PNG](png.md), [JPEG](jpeg.md), and [WebP](webp.md) are usually better starting points. + +## Practical Guidance + +Choose compression, predictor, and pixel layout explicitly when TIFF is part of an interchange contract. TIFF is a container family with many valid combinations, and downstream tools often support only the subset they care about. A file that is valid TIFF is not automatically compatible with every TIFF-consuming application. + +Treat metadata as workflow data. TIFF files often carry scanner, archival, print, or pipeline-specific metadata, so decide whether that information should be preserved, transformed, or stripped. Test with the consuming application, because compatibility matters more than theoretical format support. + +For browser delivery, ordinary thumbnails, and most application assets, PNG, JPEG, or WebP are usually easier to operate and validate. diff --git a/articles/imagesharp/troubleshooting.md b/articles/imagesharp/troubleshooting.md index ec52a4723..5b97ea6d0 100644 --- a/articles/imagesharp/troubleshooting.md +++ b/articles/imagesharp/troubleshooting.md @@ -130,3 +130,10 @@ When an image pipeline misbehaves, this order is usually productive: - [Configuration](configuration.md) - [Memory Management](memorymanagement.md) - [Interop and Raw Memory](interop.md) + +## Practical Guidance + +- Start by separating encoded-format detection, metadata inspection, full decode, and processing. +- Check stream position and configuration before assuming a codec bug. +- Use identify-based memory estimates before decoding large or untrusted images. +- Reduce the failing pipeline to the first processor or interop boundary that changes behavior. diff --git a/articles/imagesharp/webp.md b/articles/imagesharp/webp.md index 03304d042..b6718743c 100644 --- a/articles/imagesharp/webp.md +++ b/articles/imagesharp/webp.md @@ -20,6 +20,12 @@ A few practical implications: - Lossy and lossless modes have different tuning behavior. - Compatibility is generally strong in modern environments, but not identical to long-established formats like JPEG or PNG everywhere. +Lossy WebP is aimed at photographic and mixed web content, similar to JPEG, but with a different compression model and more tuning controls. It is often competitive for public delivery where byte size matters, especially after comparing quality settings against the JPEG baseline you would otherwise use. + +Lossless WebP is closer to PNG in intent: preserve exact pixels while reducing file size. It can be attractive for transparent graphics and UI assets when client support is known. It should still be tested against PNG because the smaller file is not guaranteed for every image. + +Animated WebP can replace GIF in modern delivery pipelines, but the animation behavior is more than the encoded frames. Frame delay, blend mode, disposal mode, repeat count, and background color all affect the final result. When converting an existing animation, inspect and set those values deliberately. + ## Save as WebP Use [`WebpEncoder`](xref:SixLabors.ImageSharp.Formats.Webp.WebpEncoder) when you want to tune WebP output: @@ -54,6 +60,10 @@ The most commonly used `WebpEncoder` options are: Because `WebpEncoder` inherits from [`AnimatedImageEncoder`](xref:SixLabors.ImageSharp.Formats.AnimatedImageEncoder), it also supports `RepeatCount`, `BackgroundColor`, and `AnimateRootFrame`. +In lossy mode, `Quality` controls the visual quality and compression tradeoff. In lossless mode, quality-style settings are better understood as compression-effort controls. `Method` also changes encoding effort: higher-quality methods can produce smaller or better-looking output but cost more CPU. That distinction matters on web servers where many variants may be generated on demand. + +Alpha compression is a separate concern from RGB compression. If transparent edges are important, test images with soft shadows, icons, and antialiased cutouts; those are the places where alpha handling becomes visible. + ## Read WebP Metadata Use `GetWebpMetadata()` to inspect WebP-specific metadata: @@ -89,3 +99,11 @@ WebP is a strong choice when you want: WebP is often the best first alternative to compare against both JPEG and PNG when optimizing for delivery size. If you need strict lossless preservation with a more traditional workflow, see [PNG](png.md). If you specifically need TIFF-style metadata and pixel layout control, see [TIFF](tiff.md). + +## Practical Guidance + +Compare lossy WebP against the JPEG settings you would otherwise ship. WebP often produces smaller files at similar visual quality, but the right quality value depends on content and delivery expectations. Test photos, screenshots, graphics, and mixed-content images separately. + +Use lossless WebP when transparency and smaller files matter but PNG compatibility is not required. That can be a good fit for controlled clients and modern web delivery, but it should not be the only public format unless your client and CDN support story is clear. + +Animated WebP carries timing, blending, disposal, repeat count, and background behavior. When converting from GIF or APNG, inspect that metadata instead of assuming the animation will behave identically after re-encoding. diff --git a/articles/polygonclipper/booleanoperations.md b/articles/polygonclipper/booleanoperations.md index 734251954..c93e6280a 100644 --- a/articles/polygonclipper/booleanoperations.md +++ b/articles/polygonclipper/booleanoperations.md @@ -11,6 +11,8 @@ The public entry points are the static methods on [`PolygonClipper`](xref:SixLab These are also the recommended entry points in the source, because they route work through internal reusable instances. +Boolean operations answer region questions. They do not preserve the input contour list as a drawing history. After an operation, the returned polygon describes the resulting filled area, and that may require new contours, fewer contours, or a different hierarchy. + ## Choose the Right Operation The four operations have different semantics: @@ -80,6 +82,14 @@ for (int i = 0; i < result.Count; i++) If you care about preserving hole structure or exporting contours to another renderer, inspect that hierarchy instead of assuming every returned contour is a top-level exterior ring. +## Practical Guidance + +Boolean operations combine regions. They are not a promise to preserve input contour order, input contour count, or drawing history. The result may contain several disjoint islands, holes, or a hierarchy that neither input had in exactly the same form. Production code should iterate the returned polygon and inspect hierarchy rather than assuming the first contour is the only contour that matters. + +Keep both inputs in the same coordinate system and units. `Difference(subject, clip)` is especially sensitive to naming because argument order changes the result: the clip is removed from the subject, not the other way around. Clear variable names make call sites much easier to audit. + +Use normalization when the problem is "clean this one messy region" rather than "combine these two regions." Imported or user-authored self-overlapping geometry is often easier to reason about after normalization, but there is no need to normalize every polygon before every boolean operation. Preserve hierarchy metadata when exporting to renderers or file formats that distinguish exterior rings from holes. + ## Used by ImageSharp.Drawing If you use [ImageSharp.Drawing](../imagesharp.drawing/index.md), this part of PolygonClipper may already be in your rendering pipeline. ImageSharp.Drawing converts path geometry into PolygonClipper polygons and uses these boolean operations internally when combining clipped path regions. diff --git a/articles/polygonclipper/gettingstarted.md b/articles/polygonclipper/gettingstarted.md index 0de0c6343..b03ae01bd 100644 --- a/articles/polygonclipper/gettingstarted.md +++ b/articles/polygonclipper/gettingstarted.md @@ -74,3 +74,10 @@ for (int i = 0; i < result.Count; i++) That contour hierarchy is one of the main things PolygonClipper preserves for you. If you want to understand how parent contours, holes, and winding fit together, the next page to read is [Polygons, Contours, and Holes](polygonsandcontours.md). Do not assume that one operation returns one contour. Intersections can split a region into multiple islands, differences can create holes, and normalization can reorganize self-intersecting input. Production callers should usually iterate the returned polygon rather than indexing directly into the first contour. + +## Practical Guidance + +- Keep all vertices in one coordinate system for a given operation. +- Do not repeat the first vertex at the end of ordinary boolean-operation contours. +- Prefer the static entry points unless you are building an advanced manual flow. +- Iterate every returned contour and inspect hierarchy when exporting or rendering the result. diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index 9704f930a..4abb51fc0 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -94,3 +94,10 @@ Build as normal after the file or property is configured. If the license is miss - [Boolean Operations](booleanoperations.md) covers [`Intersection`](xref:SixLabors.PolygonClipper.PolygonClipper.Intersection*), [`Union`](xref:SixLabors.PolygonClipper.PolygonClipper.Union*), [`Difference`](xref:SixLabors.PolygonClipper.PolygonClipper.Difference*), and [`Xor`](xref:SixLabors.PolygonClipper.PolygonClipper.Xor*), including subject-versus-clip semantics. - [Normalization and Winding](normalization.md) explains when to use [`Normalize(...)`](xref:SixLabors.PolygonClipper.PolygonClipper.Normalize*) to resolve self-intersections and overlaps into positive-winding output. - [Stroking](stroking.md) covers [`PolygonStroker`](xref:SixLabors.PolygonClipper.PolygonStroker), [`StrokeOptions`](xref:SixLabors.PolygonClipper.StrokeOptions), joins, caps, and open-versus-closed path behavior. + +### How to Use These Docs + +- Start with contours and polygons before choosing a boolean or stroking operation. +- Use boolean operations when combining two regions. +- Use normalization when cleaning one messy region. +- Use stroking when a line or path needs to become filled outline geometry. diff --git a/articles/polygonclipper/normalization.md b/articles/polygonclipper/normalization.md index e5b97806a..9d013e8d0 100644 --- a/articles/polygonclipper/normalization.md +++ b/articles/polygonclipper/normalization.md @@ -57,3 +57,10 @@ Reach for normalization when your goal is specifically: - cleaning up one polygon rather than combining two; - resolving self-overlap into a canonical result; - preparing output for systems that rely on positive-winding contour semantics. + +## Practical Guidance + +- Normalize at import, export, or cache boundaries rather than after every small edit. +- Use normalization for one messy polygon; use boolean operations for combining two regions. +- Expect contour count and hierarchy to change when self-intersections are resolved. +- Preserve positive-winding semantics when passing output to renderers or geometry systems that depend on winding. diff --git a/articles/polygonclipper/polygonsandcontours.md b/articles/polygonclipper/polygonsandcontours.md index f0c3b2abc..7304cc04f 100644 --- a/articles/polygonclipper/polygonsandcontours.md +++ b/articles/polygonclipper/polygonsandcontours.md @@ -8,6 +8,8 @@ PolygonClipper's public model is intentionally small. Most of the time you only That small model is enough to describe simple shapes, complex multi-contour shapes, and polygons with holes. +The important mental model is that contours are topology, not styling. PolygonClipper does not know about brushes, pens, fill colors, or pixels. It returns geometry that another layer can render, serialize, hit-test, or combine with more geometry. + ## A `Contour` Is One Ring A [`Contour`](xref:SixLabors.PolygonClipper.Contour) is a sequence of vertices. For clipping and normalization, it is treated as implicitly closed, so the library always considers an edge between the last vertex and the first vertex. @@ -26,6 +28,8 @@ contour.Add(new Vertex(0, 60)); There is no need to append `(0, 0)` again at the end unless you are deliberately feeding the stroker a contour you want treated as explicitly closed. +Avoid duplicate closing vertices in boolean inputs unless your data source naturally includes them and you have chosen to preserve them. Repeating the first vertex usually adds no information for region operations. + ## A `Polygon` Is a Collection of Contours A [`Polygon`](xref:SixLabors.PolygonClipper.Polygon) is simply a list of contours: @@ -77,6 +81,8 @@ outer.AddHoleIndex(1); When you do not already know the hierarchy, boolean operations and normalization will compute it for the returned polygon. +If you construct hierarchy yourself, keep `ParentIndex`, `Depth`, and hole indexes consistent. Those values are part of how consumers understand which contours are exterior regions and which contours subtract from a parent region. + ## Orientation Helpers [`Contour`](xref:SixLabors.PolygonClipper.Contour) also exposes orientation helpers: @@ -104,3 +110,10 @@ polygon.Translate(10, 20); ``` Those helpers are especially useful when you are staging input, culling broad regions, or preparing geometry for a later clip or stroke pass. + +## Practical Guidance + +- Store source geometry in one consistent coordinate space before clipping. +- Treat returned polygons as region results, not as a promise to preserve input contour order. +- Inspect `Depth` and `ParentIndex` when exporting to formats that need exterior and hole rings separately. +- Use bounding boxes for broad-phase rejection before expensive geometry work when you have many polygons. diff --git a/articles/polygonclipper/stroking.md b/articles/polygonclipper/stroking.md index 4b5c63d1d..0679133b3 100644 --- a/articles/polygonclipper/stroking.md +++ b/articles/polygonclipper/stroking.md @@ -87,3 +87,11 @@ Negative widths are supported for advanced scenarios. They flip the emitted side ## Used by ImageSharp.Drawing ImageSharp.Drawing also uses PolygonClipper for stroke geometry generation. Its higher-level stroke options are mapped down to PolygonClipper's `LineJoin`, `LineCap`, miter, and normalization settings before outline polygons are generated. + +## Practical Guidance + +Use stroking when a path or polyline needs to become filled outline geometry. The result is a polygon region, not a rendering command, so it can be inspected, clipped, exported, or rendered by another system. + +Decide whether the input is open or closed before choosing caps. Open inputs emit end caps; closed inputs do not. Stroke width is expressed in the same coordinate units as the source geometry, so scaling source coordinates without scaling stroke width changes the visual result. + +Join, cap, miter, and cleanup options should match the renderer or exporter that will consume the outline. Inspect the returned polygon as geometry: complex strokes can produce multiple contours and holes, especially around self-overlap, sharp joins, or closed paths. diff --git a/articles/toc.md b/articles/toc.md index 3f17bbeee..a50559730 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -49,6 +49,10 @@ ## [Transforms and Composition](imagesharp.drawing/transformsandcomposition.md) ## [Drawing Text](imagesharp.drawing/text.md) ## [WebGPU](imagesharp.drawing/webgpu.md) +### [WebGPU Environment and Support](imagesharp.drawing/webgpuenvironment.md) +### [WebGPU Window Rendering](imagesharp.drawing/webgpuwindow.md) +### [WebGPU External Surfaces](imagesharp.drawing/webgpuexternalsurface.md) +### [WebGPU Offscreen Render Targets](imagesharp.drawing/webgpurendertarget.md) ## [Migrating from System.Drawing](imagesharp.drawing/migratingfromsystemdrawing.md) ## [Migrating from SkiaSharp](imagesharp.drawing/migratingfromskiasharp.md) ## [Recipes](imagesharp.drawing/recipes.md) diff --git a/templates/modern/public/main.css b/templates/modern/public/main.css index 0b93aa83a..920fb0d2e 100644 --- a/templates/modern/public/main.css +++ b/templates/modern/public/main.css @@ -195,6 +195,14 @@ header .navbar { * ################### */ +code { + background-color: #ebebeb; +} + +[data-bs-theme="dark"] code { + background-color: #333; +} + .frame { position: relative; margin: 2rem 1rem; @@ -222,16 +230,16 @@ header .navbar { background-image: linear-gradient(45deg, #e4d101 0%, #e30183 100%); } -.frame > pre { +.frame>pre { border-radius: 0.75rem; position: relative; z-index: 1; overflow-x: hidden; } -.frame > pre::before, -.frame > pre::after, -.frame > pre code::before { +.frame>pre::before, +.frame>pre::after, +.frame>pre code::before { content: ""; position: absolute; z-index: 1; @@ -243,21 +251,21 @@ header .navbar { height: 0.75rem; } -.frame > pre::before { +.frame>pre::before { background-color: #ef4444; } -.frame > pre::after { +.frame>pre::after { left: 1.75rem; background-color: #fbbf24; } -.frame > pre code::before { +.frame>pre code::before { left: 2.75rem; background-color: #4ade80; } -.frame > pre code::after { +.frame>pre code::after { content: ""; position: absolute; z-index: 1; @@ -270,13 +278,14 @@ header .navbar { opacity: 0.25; } -.frame > pre code { +.frame>pre code { padding: 3.5rem 0.75rem 0.75rem 0.75rem !important; max-width: 100%; overflow-x: auto; } @media (min-width: 1200px) { + .frame::before, .frame::after { left: -0.5rem; @@ -293,6 +302,6 @@ header .navbar { } /* Fixes for code action disapearring due to the dark-mode hack for pre */ -pre>.code-action{ - color: #fff!important; +pre>.code-action { + color: #fff !important; } \ No newline at end of file diff --git a/templates/modern/public/main.js b/templates/modern/public/main.js index 7a4c0f9a1..9fe4ad147 100644 --- a/templates/modern/public/main.js +++ b/templates/modern/public/main.js @@ -6,17 +6,18 @@ export default { return; } - // API reference pages contain many generated signature blocks. Keep the - // framed treatment for authored article pages. - if (document.body.dataset.yamlMime === "ManagedReference") { - return; - } - const article = document.querySelector("article"); if (!article) { return; } + const isApiReference = document.body.dataset.yamlMime === "ManagedReference"; + + for (const codeWrapper of article.querySelectorAll(".codewrapper")) { + // API reference pages wrap generated pre blocks in codewrapper containers. + codeWrapper.dataset.bsTheme = "dark"; + } + for (const pre of article.querySelectorAll("pre")) { // Keep Highlight.js colors consistent everywhere, including DocFX tab // groups whose structure we should not wrap. From 635ecb8d2d98b61876c521ccd0d58cdc5f588901 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 May 2026 21:53:04 +1000 Subject: [PATCH 20/21] Update to latest tags --- articles/fonts/hinting.md | 100 ++ articles/fonts/hintingandshaping.md | 160 --- articles/fonts/index.md | 9 +- articles/fonts/opentypefeatures.md | 2 +- articles/fonts/shaping.md | 164 +++ articles/fonts/texthittesting.md | 23 + articles/fonts/textlayout.md | 2 +- articles/imagesharp.drawing/canvas.md | 14 +- articles/imagesharp.drawing/gettingstarted.md | 6 + articles/imagesharp.drawing/index.md | 6 +- articles/imagesharp.web/configuration.md | 11 + articles/imagesharp.web/index.md | 6 +- articles/imagesharp/colorprofiles.md | 138 ++- articles/imagesharp/formatconversion.md | 18 +- articles/imagesharp/imageformats.md | 45 + articles/imagesharp/index.md | 6 +- articles/imagesharp/loadingandsaving.md | 10 + articles/imagesharp/memorymanagement.md | 12 +- articles/imagesharp/pixelbuffers.md | 6 + articles/imagesharp/processing.md | 6 + articles/imagesharp/security.md | 10 +- articles/polygonclipper/index.md | 6 +- .../polygonclipper/polygonsandcontours.md | 6 + articles/toc.md | 3 +- ext/ImageSharp | 2 +- ext/ImageSharp.Drawing | 2 +- ext/ImageSharp.Web | 2 +- index.md | 63 +- templates/modern/public/main.css | 11 +- templates_bak/android-chrome-192x192.png | Bin 23547 -> 0 bytes templates_bak/android-chrome-512x512.png | Bin 148113 -> 0 bytes templates_bak/apple-touch-icon.png | Bin 21220 -> 0 bytes templates_bak/favicon-16x16.png | Bin 653 -> 0 bytes templates_bak/favicon-32x32.png | Bin 1562 -> 0 bytes templates_bak/favicon.ico | Bin 15406 -> 0 bytes .../fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes .../fonts/glyphicons-halflings-regular.svg | 288 ------ .../fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes templates_bak/logo.svg | 1 - templates_bak/styles/main.css | 936 ------------------ templates_bak/styles/main.js | 33 - templates_bak/styles/vs2015.css | 115 --- 44 files changed, 599 insertions(+), 1623 deletions(-) create mode 100644 articles/fonts/hinting.md delete mode 100644 articles/fonts/hintingandshaping.md create mode 100644 articles/fonts/shaping.md delete mode 100644 templates_bak/android-chrome-192x192.png delete mode 100644 templates_bak/android-chrome-512x512.png delete mode 100644 templates_bak/apple-touch-icon.png delete mode 100644 templates_bak/favicon-16x16.png delete mode 100644 templates_bak/favicon-32x32.png delete mode 100644 templates_bak/favicon.ico delete mode 100644 templates_bak/fonts/glyphicons-halflings-regular.eot delete mode 100644 templates_bak/fonts/glyphicons-halflings-regular.svg delete mode 100644 templates_bak/fonts/glyphicons-halflings-regular.ttf delete mode 100644 templates_bak/fonts/glyphicons-halflings-regular.woff delete mode 100644 templates_bak/fonts/glyphicons-halflings-regular.woff2 delete mode 100644 templates_bak/logo.svg delete mode 100644 templates_bak/styles/main.css delete mode 100644 templates_bak/styles/main.js delete mode 100644 templates_bak/styles/vs2015.css diff --git a/articles/fonts/hinting.md b/articles/fonts/hinting.md new file mode 100644 index 000000000..e85eea1bb --- /dev/null +++ b/articles/fonts/hinting.md @@ -0,0 +1,100 @@ +# TrueType Hinting + +Font hinting is the use of instructions in a font to adjust outline glyphs for a raster grid. In TrueType fonts, those instructions are bytecode programs stored in the font. The rasterizer executes them at a given size and DPI to move outline points before the glyph is drawn. + +## Why Hinting Exists + +Outline fonts are scalable. Raster images are not. When a glyph is small, its outline has to be represented by a limited number of pixels. Without adjustment, similar stems can round to different widths, horizontal features can fall between pixel rows, and small counters or serifs can lose definition. + +Hinting changes the scaled outline before rasterization. It can improve: + +- stem thickness +- counter shape +- baseline alignment +- x-height consistency +- serif and bar visibility +- mark attachment stability + +The effect is most visible for small UI text at ordinary screen DPI. At larger sizes or high-resolution outputs, the outline has more pixels available and hinting usually has less visible effect. + +## How Fonts Applies Hinting + +[`TextOptions.HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) controls whether Fonts applies TrueType hinting: + +- [`HintingMode.None`](xref:SixLabors.Fonts.HintingMode.None) leaves glyph outlines unhinted. +- [`HintingMode.Standard`](xref:SixLabors.Fonts.HintingMode.Standard) applies the library's FreeType v40-compatible TrueType hinting behavior. + +```csharp +using SixLabors.Fonts; + +Font font = SystemFonts.CreateFont("Segoe UI", 11); +TextOptions options = new(font) +{ + Dpi = 96, + HintingMode = HintingMode.Standard +}; +``` + +The active font size and [`Dpi`](xref:SixLabors.Fonts.TextOptions.Dpi) matter because hinting targets a specific pixels-per-em scale. + +## Fonts' Hinting Approach + +Fonts uses a TrueType bytecode interpreter modeled on FreeType's v40 subpixel hinting behavior. In practical terms, that means Fonts preserves full vertical TrueType instruction processing while intentionally disabling horizontal hinting. + +This approach is designed for modern antialiased text rendering, where horizontal subpixel placement should remain smooth and glyph advances should not be forced into old bi-level grid-fitting behavior. It gives small text the vertical alignment benefits of TrueType hinting while avoiding legacy horizontal snapping that can make spacing and shapes less consistent in modern raster output. + +When hinting is active, Fonts: + +- executes the font program from `fpgm` to initialize TrueType function definitions +- scales the Control Value Table from `cvt ` for the current size and DPI +- executes the `prep` program to establish the graphics state for glyph programs +- applies `cvar` deltas to control values for variable TrueType fonts before hinting +- provides normalized variation coordinates for TrueType variation-aware instructions +- adds the four TrueType phantom points used for horizontal and vertical metrics during glyph hinting +- executes each glyph's TrueType instructions against the resolved outline +- leaves the outline unhinted if a glyph has no instructions, hinting is inhibited by the font program, or instruction execution fails + +## TrueType Scope + +Fonts applies this hinting path to TrueType outlines. It does not turn CFF or CFF2 outlines into hinted TrueType outlines, and it does not change which glyphs are selected for the text. + +Within the TrueType path, Fonts supports: + +- TrueType glyph instruction execution. +- Standard TrueType hinting tables such as `fpgm`, `prep`, and `cvt `. +- Per-glyph hinting at the active size and DPI. +- `cvar`-driven control-value adjustments for variable TrueType fonts before hinting runs. +- Hinted contour-point resolution for GPOS anchor data when a font uses contour-point anchors. +- Font-specific compatibility behavior for fonts known to require hinting. + +## When to Enable It + +Use [`HintingMode.Standard`](xref:SixLabors.Fonts.HintingMode.Standard) when rendering small TrueType UI text to a raster target and you want grid-fitted outlines. + +Use [`HintingMode.None`](xref:SixLabors.Fonts.HintingMode.None) when you want raw outline behavior, when you are rendering large display text, or when the text is being treated as artwork rather than screen UI. + +There is no universal best setting. Hinting is a raster-quality tradeoff: it can make small text clearer, but it can also move outlines away from their pure scaled design. + +## Common Misunderstandings + +Hinting does not: + +- fix missing glyphs +- enable ligatures or OpenType features +- choose fallback fonts +- reorder complex scripts +- resolve bidirectional text +- change Unicode indexing or grapheme behavior + +Those are layout and shaping concerns. For those, see [Text Shaping](shaping.md). + +## Further Reading + +[The Raster Tragedy](http://rastertragedy.com/) is a useful deeper discussion of why rasterizing outline text is difficult and why hinting can matter for small text. + +## Related Topics + +- [Text Layout and Options](textlayout.md) +- [Text Shaping](shaping.md) +- [Font Metrics](fontmetrics.md) +- [Custom Rendering](customrendering.md) diff --git a/articles/fonts/hintingandshaping.md b/articles/fonts/hintingandshaping.md deleted file mode 100644 index bdfccc761..000000000 --- a/articles/fonts/hintingandshaping.md +++ /dev/null @@ -1,160 +0,0 @@ -# Hinting and Shaping - -Hinting and shaping are often mentioned in the same breath because both influence the final appearance of text. For newcomers, it helps to separate them early: shaping decides which glyphs and positions the layout engine should use, while hinting adjusts outlines for pixel-oriented rendering. - -Shaping answers "which glyphs should this text use, and where do those glyphs go?" - -Hinting answers "how should this specific TrueType outline be adjusted for this size and DPI so it lands cleanly on the pixel grid?" - -Fonts has comprehensive support for both, but the scope is different: - -- shaping is a full text-layout concern and runs for all normal measurement and rendering -- hinting is a TrueType outline concern and runs when hinted glyph outlines are materialized - -### The short version - -| Topic | Shaping | Hinting | -| --- | --- | --- | -| Input | Unicode text, script, direction, font selection, OpenType features | A concrete glyph outline, size, and DPI | -| Output | The final glyph sequence and glyph positions | A grid-fitted outline for raster-oriented rendering | -| Main goal | Correct text layout and glyph choice | Better small-size screen rendering | -| Controlled by | [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection), [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags), [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode), [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking), [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns), [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies), [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) | [`HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) | - -### What shaping means - -Shaping is the process of turning text into the glyph sequence a font actually needs. - -That is more than a simple character-to-glyph lookup. A shaping engine may need to: - -- choose different glyph forms depending on neighboring text -- form ligatures such as `ffi` -- apply fractions or numeral variants -- position marks relative to a base glyph -- apply kerning and cursive attachment -- reorder glyphs for complex scripts -- resolve bidirectional text and mirrored forms - -In other words, shaping works at the typography level. It decides what the text is supposed to look like before any pixel-grid tuning happens. - -### Fonts has comprehensive shaping support - -Fonts does not require a separate public shaping API for normal use because shaping is built into the layout engine that backs both `TextMeasurer` and `TextRenderer`. - -That shaping support includes: - -- full OpenType layout processing through GSUB and GPOS -- bidirectional analysis and automatic direction handling through `TextDirection.Auto` -- mirrored-form substitution for right-to-left text where required -- script-aware shapers in the codebase for Arabic, Hangul, Hebrew, Indic, Myanmar, and Thai/Lao text -- a Universal Shaping Engine for additional complex scripts -- kerning, ligatures, fractions, tabular figures, vertical alternates, and other OpenType feature-driven behaviors -- font fallback and per-range font selection through `FallbackFontFamilies` and `TextRuns` - -This is why measurement and rendering stay aligned when you use the same [`TextOptions`](xref:SixLabors.Fonts.TextOptions) instance for both. Fonts measures shaped text, not a simplified pre-layout approximation. - -### What you control in shaping - -The main shaping controls are: - -- [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection) to force left-to-right, right-to-left, or automatic bidi resolution -- [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode) for horizontal and vertical layout behavior -- [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) to request additional OpenType features such as fractions or tabular figures -- [`KerningMode`](xref:SixLabors.Fonts.TextOptions.KerningMode) to enable or disable font-provided kerning during shaping -- [`Tracking`](xref:SixLabors.Fonts.TextOptions.Tracking) to add uniform em-based spacing after rendered graphemes, after the font's own spacing behavior -- [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) when the main font does not cover every glyph you need -- [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) when different text ranges need different fonts, attributes, or decorations - -Required script shaping still happens automatically. [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) is for extra typographic features you want to request on top of that baseline shaping behavior. - -```csharp -using SixLabors.Fonts; -using SixLabors.Fonts.Tables.AdvancedTypographic; - -Font font = SystemFonts.CreateFont("Segoe UI", 18); -TextOptions options = new(font) -{ - TextDirection = TextDirection.Auto, - KerningMode = KerningMode.Standard, - FeatureTags = - [ - KnownFeatureTags.Fractions, - KnownFeatureTags.TabularFigures - ] -}; - -FontRectangle bounds = TextMeasurer.MeasureAdvance("9/2", options); -``` - -### What hinting means - -Hinting is not about choosing glyphs. It is about adjusting the points in a glyph outline so the shape lands better on the pixel grid at a particular size and DPI. - -That matters most at smaller sizes, where a one-pixel decision can noticeably affect: - -- stem thickness -- counter shape -- bar height -- baseline alignment -- mark attachment consistency - -At larger sizes the difference is usually much smaller because the outline already has enough pixel resolution to describe itself cleanly. - -### Fonts has comprehensive TrueType hinting support - -Within the scope of TrueType outlines, Fonts has comprehensive hinting support. - -[`TextOptions.HintingMode`](xref:SixLabors.Fonts.TextOptions.HintingMode) controls whether that hinting path is active: - -- `HintingMode.None` leaves outlines unhinted -- `HintingMode.Standard` applies the library's FreeType v40-compatible TrueType hinting behavior - -That means Fonts uses a modern screen-oriented TrueType hinting model rather than treating hinting as old black-and-white full-grid-fitting for legacy CRT text. - -The hinting pipeline in Fonts includes: - -- TrueType glyph instruction execution -- support for the standard TrueType hinting tables such as `fpgm`, `prep`, and `cvt` -- per-glyph hinting at the active size and DPI -- `cvar`-driven control-value adjustments for variable TrueType fonts before hinting runs -- hinted contour-point resolution for GPOS anchor data when the font uses contour-point anchors - -This is specifically a TrueType feature. Fonts only applies this hinting path to TrueType glyph data, so CFF and CFF2 outlines do not gain extra hinting behavior from [`HintingMode.Standard`](xref:SixLabors.Fonts.HintingMode.Standard). - -```csharp -using SixLabors.Fonts; - -Font font = SystemFonts.CreateFont("Segoe UI", 11); -TextOptions options = new(font) -{ - Dpi = 96, - HintingMode = HintingMode.Standard -}; -``` - -### Hinting and shaping are separate stages - -The easiest way to think about the pipeline is: - -1. Fonts analyzes the text, script, direction, features, and font selection. -2. Fonts shapes the text into the correct glyph sequence and glyph positions. -3. If the resolved glyphs are TrueType outlines and hinting is enabled, Fonts adjusts those outlines for the current size and DPI. - -So: - -- shaping decides which glyphs you get and where they belong -- hinting adjusts how those resolved glyphs are fit to the raster grid - -Hinting does not choose ligatures, apply Arabic joining, reorder Indic glyphs, or enable OpenType features. Those are shaping concerns. - -Shaping does not grid-fit outlines. It decides the typographic result that hinting may later refine for small-size raster output. - -### Practical guidance - -- Use [`TextDirection.Auto`](xref:SixLabors.Fonts.TextDirection.Auto) unless you have a specific reason to force directionality. -- Use [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies) for multilingual text, emoji, and scripts your main font does not cover. -- Use [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) for discretionary features such as fractions, stylistic sets, or tabular figures. -- Use [`HintingMode.Standard`](xref:SixLabors.Fonts.HintingMode.Standard) when rendering small TrueType UI text and leave it off when you want the raw outline behavior. -- Treat shaping as a typography and layout concern. -- Treat hinting as a size-dependent TrueType raster-quality concern. - -For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For multilingual font fallback, see [Fallback Fonts and Multilingual Text](fallbackfonts.md). diff --git a/articles/fonts/index.md b/articles/fonts/index.md index 37b49d731..052f277d8 100644 --- a/articles/fonts/index.md +++ b/articles/fonts/index.md @@ -18,7 +18,7 @@ The Unicode pages are part of the practical API story, not a side topic. Most re Fonts is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/Fonts/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with Fonts 3.0.0, projects that directly depend on SixLabors.Fonts require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with Fonts 3.0.0, projects that directly depend on SixLabors.Fonts require a valid Six Labors license at build time. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation @@ -55,9 +55,9 @@ paket add SixLabors.Fonts --version VERSION_NUMBER ### How to use the license file -Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. +By default, the build searches from each project directory for `sixlabors.lic`. Place the supplied file in the directory that contains the project file, or in a subdirectory below it. Use the file as supplied; it already contains the complete license string required by the build. -If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: +If you want to keep the file somewhere else, including a repository root that sits above the project directory, set `SixLaborsLicenseFile` in your project file or a shared props file: ```xml @@ -104,7 +104,8 @@ If you are new to Fonts, start with [Loading Fonts and Collections](gettingstart - [Selection and Bidi Drag](caretsandselection.md) - [Text Layout and Options](textlayout.md) - [OpenType Features](opentypefeatures.md) -- [Hinting and Shaping](hintingandshaping.md) +- [Text Shaping](shaping.md) +- [TrueType Hinting](hinting.md) - [Color Fonts](colorfonts.md) - [Unicode, Code Points, and Graphemes](unicode.md) - [Fallback Fonts and Multilingual Text](fallbackfonts.md) diff --git a/articles/fonts/opentypefeatures.md b/articles/fonts/opentypefeatures.md index ae10d51c9..dabaddc6b 100644 --- a/articles/fonts/opentypefeatures.md +++ b/articles/fonts/opentypefeatures.md @@ -126,7 +126,7 @@ Some OpenType features are especially relevant in vertical layout, such as [`Kno Those work alongside [`LayoutMode`](xref:SixLabors.Fonts.TextOptions.LayoutMode); they do not replace it. -For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For the broader shaping pipeline, see [Hinting and Shaping](hintingandshaping.md). +For the surrounding layout controls, see [Text Layout and Options](textlayout.md). For the broader shaping pipeline, see [Text Shaping](shaping.md). ### Practical guidance diff --git a/articles/fonts/shaping.md b/articles/fonts/shaping.md new file mode 100644 index 000000000..e27c9d7cb --- /dev/null +++ b/articles/fonts/shaping.md @@ -0,0 +1,164 @@ +# Text Shaping + +Text shaping converts Unicode text into glyph IDs and positions. It takes the source string, the selected fonts, script and direction information, OpenType tables, fallback rules, and requested typographic features, then produces the glyph run that measurement and rendering use. + +The input is text. The output is not text anymore. It is an ordered set of glyph IDs, glyph advances, offsets, bidi levels, source indexes, and font metrics. That output is what lets Fonts measure, wrap, hit-test, and render text consistently. + +## Why Shaping Exists + +Unicode stores text as code points. Fonts store drawable shapes as glyphs. The relationship between the two is many-to-many: + +- One code point can map to one glyph, as with many Latin letters. +- Several code points can become one glyph, as with ligatures or composed forms. +- One code point can become multiple glyphs, as with decomposition and fallback behavior. +- A glyph can move because of kerning, mark positioning, cursive attachment, vertical layout, or script rules. +- The visual order can differ from the logical string order in bidirectional text. + +For simple English text in a basic font, shaping may look almost invisible. The same pipeline still matters because kerning, ligatures, fallback fonts, line breaks, and source indexes all depend on the shaped result. + +## Shaping and Rendering + +Shaping decides which glyphs should be used and where those glyphs should be placed. Rendering draws those glyphs onto a target surface. + +In Fonts, shaping and rendering are connected but separate responsibilities. Fonts prepares the glyph run and layout data. A renderer then consumes that data to draw paths, color glyph layers, SVG glyphs, or another representation through [`TextRenderer`](xref:SixLabors.Fonts.Rendering.TextRenderer) and [`IGlyphRenderer`](xref:SixLabors.Fonts.Rendering.IGlyphRenderer). ImageSharp.Drawing provides a renderer for drawing text into images, but Fonts itself also supports custom renderers. + +This separation matters when you build your own renderer. Do not map characters to glyphs inside the renderer. Let Fonts shape the text once, then render the glyphs it gives you. + +## What Fonts Produces + +After shaping, Fonts has enough information to answer layout questions in visual terms while still mapping back to the original string. + +The shaped data records: + +- which font supplied each glyph +- the glyph ID or glyph sequence that should be rendered +- the source code point and grapheme indexes +- the resolved bidi run and embedding level +- glyph advance and positioning data +- glyph bounds and line metrics +- text-run attributes and decorations + +This is why shaped text affects more than drawing. It changes measured width, line wrapping, caret movement, hit testing, selection, and text bounds. + +## The Fonts Shaping Pipeline + +Fonts shapes text during normal measurement and rendering. [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer), [`TextBlock`](xref:SixLabors.Fonts.TextBlock), and [`TextRenderer`](xref:SixLabors.Fonts.Rendering.TextRenderer) all use the same [`TextOptions`](xref:SixLabors.Fonts.TextOptions) contract. + +At a high level, Fonts does the following: + +1. Builds the resolved [`TextRun`](xref:SixLabors.Fonts.TextRun) list for the string. If no runs are supplied, the whole string uses `TextOptions.Font`. If runs are supplied, Fonts orders them, fills gaps with the default font, and trims overlaps. +2. Runs Unicode bidirectional analysis using [`TextDirection`](xref:SixLabors.Fonts.TextOptions.TextDirection) and [`TextBidiMode`](xref:SixLabors.Fonts.TextOptions.TextBidiMode). +3. Walks the text by grapheme and then code point so source indexes remain meaningful for caret, selection, and hit testing. +4. Maps code points to glyph IDs with the current text-run font. +5. Applies right-to-left mirrored forms and vertical alternates where the layout requires them. +6. Applies OpenType GSUB substitutions through the appropriate script shaper. +7. Applies GPOS positioning for kerning, marks, cursive attachment, and related placement behavior. +8. Retries unmapped code points with the configured fallback font families. +9. Updates final glyph positions for every font involved in the shaped result. + +The first shaping result is independent of wrapping width. Line composition and alignment happen after shaping, so the same shaped text can be measured, wrapped, rendered, or inspected without changing which glyphs were chosen. + +## Script Shapers + +Fonts uses the OpenType Layout shaping model. It reads Unicode script data and the OpenType script tags available in the font, then chooses the script shaper for the run. + +Specialized shapers handle scripts whose glyph selection or ordering has rules beyond the default feature plan: + +- Arabic-family scripts, including Arabic, Syriac, Nko, Mongolian, Mandaic, Manichaean, Phags Pa, and Psalter Pahlavi. +- Hebrew. +- Thai and Lao. +- Hangul. +- Indic scripts such as Devanagari, Bengali, Gujarati, Gurmukhi, Kannada, Malayalam, Oriya, Tamil, Telugu, and Khmer. +- Myanmar when the font exposes the modern `mym2` shaping model. +- Universal Shaping Engine scripts such as Balinese, Brahmi, Chakma, Javanese, Tibetan, Sinhala, Tai Tham, and other complex scripts covered by the USE model. + +Other scripts use the default shaper, which still applies common OpenType behavior such as composition, localized forms, required ligatures, standard ligatures, contextual alternates, mark positioning, kerning, and directional alternates. + +Fonts does not use HarfBuzz, Graphite, Apple Advanced Typography, Uniscribe, or platform text APIs. It implements its shaping behavior directly in managed code using the font data it loads. + +## OpenType Features + +Fonts applies required features automatically. You do not need to request core script behavior with [`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags). + +The default feature plan includes common features such as: + +- `ccmp` for glyph composition and decomposition +- `locl` for localized forms +- `rlig` for required ligatures +- `mark` and `mkmk` for mark positioning +- `calt`, `clig`, `liga`, `rclt`, and `curs` for horizontal text where applicable +- `kern` unless kerning is disabled +- `vert` for vertical glyph alternates where applicable +- directional features such as `ltra`, `ltrm`, `rtla`, and `rtlm` +- `rvrn` for required variation alternates + +[`FeatureTags`](xref:SixLabors.Fonts.TextOptions.FeatureTags) is for optional features you want to request from the font, such as tabular figures, fractions, stylistic sets, discretionary ligatures, or small capitals. Feature support depends on the font. + +```csharp +using SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + +Font font = SystemFonts.CreateFont("Segoe UI", 18); +TextOptions options = new(font) +{ + TextDirection = TextDirection.Auto, + KerningMode = KerningMode.Standard, + FeatureTags = + [ + KnownFeatureTags.Fractions, + KnownFeatureTags.TabularFigures + ] +}; + +FontRectangle bounds = TextMeasurer.MeasureAdvance("9/2", options); +``` + +Fractions are a good example of a feature that changes the glyph plan. Fonts handles the required `frac`, `numr`, and `dnom` feature assignment around fraction sequences when fraction features are requested. + +## Direction and Bidi + +Bidirectional text is handled before font substitution and positioning. Fonts resolves directional runs, records the bidi run for each shaped glyph, and uses that information during line layout. + +Use [`TextDirection.Auto`](xref:SixLabors.Fonts.TextDirection.Auto) unless your input has an external direction contract. Use [`TextBidiMode`](xref:SixLabors.Fonts.TextOptions.TextBidiMode) when you need override behavior rather than normal Unicode bidi resolution. + +Mirrored forms, such as paired punctuation in right-to-left runs, are part of the shaping result. Fonts first relies on font support and also uses Unicode mirror data where needed. + +## Fallback Fonts + +Fallback is not just a missing-glyph replacement step at the end of rendering. It participates in shaping because each font has its own glyph coverage, metrics, OpenType tables, script tags, and mark positioning behavior. + +When the primary text-run font cannot map every code point, Fonts retries unresolved text against [`FallbackFontFamilies`](xref:SixLabors.Fonts.TextOptions.FallbackFontFamilies). The fallback font supplies the glyphs and positions for the code points it covers. + +For multilingual text, emoji, and complex scripts, validate fallback with real production strings. A font can contain the individual code points but still lack the OpenType data needed for correct shaping. + +## Text Runs and Placeholders + +Use [`TextRuns`](xref:SixLabors.Fonts.TextOptions.TextRuns) when a range of text needs a different font, style attributes, decorations, or a placeholder. + +Text runs are indexed by grapheme range, not raw UTF-16 code unit count. Fonts resolves the run list before shaping, so font selection and attributes are known when code points are mapped to glyphs. + +Placeholder runs are inserted into the layout stream without consuming source text. That makes them useful for inline objects while preserving source text indexes for the surrounding content. + +## Measurement and Rendering + +Shaping changes advance widths and glyph positions, so measurement and rendering must use the same options. Do not measure with one font, direction, feature set, fallback list, or wrapping policy and render with another. + +For one-off layout, use [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer). Use [`TextBlock`](xref:SixLabors.Fonts.TextBlock) when the shaped result becomes state: you need to measure it more than once, render it later, inspect its lines, hit-test it, or support caret and selection behavior. + +## Practical Guidance + +- Treat [`TextOptions`](xref:SixLabors.Fonts.TextOptions) as the shaping contract. +- Use `TextDirection.Auto` for natural text unless a protocol or UI explicitly supplies direction. +- Use `FeatureTags` for optional typography, not for required script shaping. +- Use `FallbackFontFamilies` for multilingual text and test fallback with realistic content. +- Use `TextRuns` for known range-level font or attribute changes. +- Keep measurement, rendering, hit testing, and selection on the same shaped options. +- Validate complex scripts with the actual fonts and strings your application will ship. + +## Related Topics + +- [Text Layout and Options](textlayout.md) +- [OpenType Features](opentypefeatures.md) +- [Fallback Fonts and Multilingual Text](fallbackfonts.md) +- [Unicode, Code Points, and Graphemes](unicode.md) +- [Prepared Text with TextBlock](textblock.md) diff --git a/articles/fonts/texthittesting.md b/articles/fonts/texthittesting.md index 7d0a5e5bf..8ffd72c25 100644 --- a/articles/fonts/texthittesting.md +++ b/articles/fonts/texthittesting.md @@ -1,9 +1,17 @@ # Hit Testing and Caret Movement +Hit testing resolves a point in laid-out text back to a text position. In Fonts, hit testing is not a yes/no collision test against visible pixels. [`HitTest(...)`](xref:SixLabors.Fonts.TextMetrics.HitTest*) returns a [`TextHit`](xref:SixLabors.Fonts.TextHit) describing the nearest grapheme, the line it belongs to, the source string index, and whether the point resolved to the leading or trailing side of that grapheme. + Once text has been laid out, applications usually need to translate between pixels, character positions, and editor commands. Fonts exposes a small set of types that own the bidi, grapheme, and hard-break rules so callers do not need to reimplement them: [`TextHit`](xref:SixLabors.Fonts.TextHit), [`CaretPosition`](xref:SixLabors.Fonts.CaretPosition), [`CaretPlacement`](xref:SixLabors.Fonts.CaretPlacement), and [`CaretMovement`](xref:SixLabors.Fonts.CaretMovement). All positional values returned by these APIs are in pixel units. +### How this differs from graphics hit testing + +In general graphics APIs, hit testing usually means checking whether a cursor point intersects a shape, path, bounding box, or visual object. Text interaction has a different goal. A text editor must answer "where would the caret go?" even when the point is over whitespace, outside the ink, above the first line, or beyond the end of a wrapped line. + +Fonts therefore resolves the point to a text position rather than returning a collision result. The returned `TextHit` is an input to caret placement, selection, word lookup, and movement. If you need geometric picking against custom rendered glyph outlines, use the geometry produced by your renderer for that purpose; do not substitute ink bounds for caret hit testing. + ### Get a measurement object Hit testing, caret positioning, and caret movement all operate on a [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) (whole-block) or [`LineLayout`](xref:SixLabors.Fonts.LineLayout) (single line). Either come from [`TextMeasurer`](xref:SixLabors.Fonts.TextMeasurer) or from a prepared [`TextBlock`](xref:SixLabors.Fonts.TextBlock). See [Prepared Text with TextBlock](textblock.md) for when to prefer one over the other. @@ -23,6 +31,16 @@ TextOptions options = new(font) TextMetrics metrics = TextMeasurer.Measure("Hello, world!", options); ``` +### Coordinate space and hit targets + +The point passed to `HitTest(...)` is in the same pixel coordinate space as the measured layout. That includes the [`TextOptions.Origin`](xref:SixLabors.Fonts.TextOptions.Origin), wrapping length, layout mode, text direction, fallback fonts, and interaction mode that were used when the `TextMetrics` or `LineLayout` was produced. + +Fonts uses the logical advance rectangle of each laid-out grapheme as the hit target. It does not hit-test rendered ink bounds. Ink bounds are unsuitable for text interaction because whitespace can have no ink, accents and glyph overhangs can extend outside the advance, and some glyphs draw less than the area users expect to click or select. + +For whole-block hit testing, `TextMetrics` first chooses the nearest laid-out line on the cross axis. It then resolves the nearest grapheme on that line's primary axis. In horizontal layout the primary axis is X; in vertical layout the primary axis is Y. + +Points outside the text block clamp to the nearest line and grapheme instead of returning no hit. This is intentional for editor-style behavior: clicking to the left, right, above, or below the text can still place a caret at the nearest valid text position. + ### Choose paragraph or editor mode [`TextOptions.TextInteractionMode`](xref:SixLabors.Fonts.TextOptions.TextInteractionMode) controls how trailing whitespace and terminal hard breaks behave for hit testing, caret movement, and selection. @@ -50,6 +68,8 @@ int stringIndex = hit.StringIndex; bool trailing = hit.IsTrailing; ``` +`IsTrailing` records which side of the grapheme was hit. For left-to-right text, a point after the grapheme midpoint on the primary axis resolves to the trailing side. For right-to-left text, the visual side is reversed. Prefer [`GetCaretPosition(hit)`](xref:SixLabors.Fonts.TextMetrics.GetCaretPosition*) when you need caret geometry; it applies the side and direction rules for the resolved layout. + [`TextHit`](xref:SixLabors.Fonts.TextHit) is meant to be passed straight back into the interaction APIs — [`GetCaretPosition(hit)`](xref:SixLabors.Fonts.TextMetrics.GetCaretPosition*), [`GetSelectionBounds(anchor, focus)`](xref:SixLabors.Fonts.TextMetrics.GetSelectionBounds*), [`GetWordMetrics(hit)`](xref:SixLabors.Fonts.TextMetrics.GetWordMetrics*). Those overloads consume the hit directly and apply the trailing-side and bidi rules internally, so callers do not need to compute the visual side themselves. The properties are exposed for diagnostics and for cases where you need to point back into your own text — for example, mapping the hit to a position in your source string. `GraphemeInsertionIndex` is the insertion position within the laid-out grapheme array; you rarely need to read it yourself. @@ -166,3 +186,6 @@ For more on the underlying measurement model and the `TextMetrics` shape, see [M - Use `LineLayout` only when the caller already knows the interaction is line-local. - Choose `TextInteractionMode.Editor` for editable text and `Paragraph` for display layout. - Keep hit testing, caret movement, and selection tied to the same measured layout. +- Hit-test the measured layout, not rendered glyph ink bounds. +- Treat `TextHit` as a resolved text interaction position, not proof that the pointer was inside visible ink. +- Expect clamping for points outside the text block. diff --git a/articles/fonts/textlayout.md b/articles/fonts/textlayout.md index b962d8584..1136c5a31 100644 --- a/articles/fonts/textlayout.md +++ b/articles/fonts/textlayout.md @@ -121,7 +121,7 @@ TextOptions options = new(font) }; ``` -For a deeper explanation of how Fonts applies GSUB/GPOS shaping, bidi analysis, fallback runs, and TrueType hinting, see [Hinting and Shaping](hintingandshaping.md). +For a deeper explanation of GSUB/GPOS shaping, bidi analysis, fallback runs, and TrueType hinting, see [Text Shaping](shaping.md) and [TrueType Hinting](hinting.md). ### Fallback fonts and color fonts diff --git a/articles/imagesharp.drawing/canvas.md b/articles/imagesharp.drawing/canvas.md index f66a515fa..581288979 100644 --- a/articles/imagesharp.drawing/canvas.md +++ b/articles/imagesharp.drawing/canvas.md @@ -22,11 +22,23 @@ image.Mutate(ctx => ctx.Paint(canvas => The callback receives a canvas for the current frame. Use the canvas for all drawing work that should happen together. +## Immediate Mode, Retained Mode, and Canvas + +Graphics APIs are often described as either immediate mode or retained mode. + +In an immediate-mode API, the application issues the drawing commands needed for the current output. The graphics library does not own an editable scene graph for the application. If the output needs to be drawn again, the application normally issues those commands again. + +In a retained-mode API, the graphics library owns a persistent object model of the scene. Application calls usually update that model rather than directly describing the drawing for one output pass. The library can then decide when and how to render the retained scene. + +`DrawingCanvas` is closest to immediate-mode drawing at the public API level: application code calls `Fill(...)`, `Draw(...)`, `DrawText(...)`, `DrawImage(...)`, `Save(...)`, and `Restore()` in the order the output should be produced. The canvas is not a retained scene graph. You do not create editable rectangle, path, text, or image nodes and then change their properties later. + +The important difference from a strictly immediate pixel-writing API is replay. Canvas calls record ordered drawing intent into a timeline. That timeline is replayed into the active backend when the canvas is disposed, or sealed into a reusable backend scene when you call `CreateScene()`. This lets ImageSharp.Drawing keep an immediate-style authoring model while still batching work, inserting barriers, supporting layers, and reusing backend-prepared scenes where that is useful. + ## Ordered Calls and Replay [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) is an ordered drawing API backed by a replay timeline. The calls look familiar if you have used immediate-mode drawing APIs: [`Fill(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Fill*), [`Draw(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Draw*), [`DrawText(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.DrawText*), [`Save(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Save*), and [`Restore()`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Restore) are made in the order you want drawing to happen. The canvas does not, however, promise that each call immediately writes pixels to the destination. -It is also not a retained object tree. The canvas does not keep editable shape objects that automatically redraw when their properties change. It records ordered drawing intent, seals that intent into timeline entries, and replays the timeline into the active backend. +The timeline is the core of the model. The canvas records drawing intent, seals that intent into timeline entries, and replays the timeline into the active backend. Most drawing calls append drawing intent to a command buffer. Calls that must happen at a specific point, such as [`Apply(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.Apply*) and [`RenderScene(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.RenderScene*) are stored as entries in the canvas replay timeline. [`SaveLayer(...)`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas.SaveLayer*) is also timeline-sensitive: it records an isolated group that is later composited back into the parent. diff --git a/articles/imagesharp.drawing/gettingstarted.md b/articles/imagesharp.drawing/gettingstarted.md index 03322fe33..a85c19085 100644 --- a/articles/imagesharp.drawing/gettingstarted.md +++ b/articles/imagesharp.drawing/gettingstarted.md @@ -11,6 +11,12 @@ The main workflow is: The same canvas can mix all of those operations. The important idea is that drawing is recorded through [`DrawingCanvas`](xref:SixLabors.ImageSharp.Drawing.Processing.DrawingCanvas) in the order you call it, then replayed into the current frame. That replay model lets the library share the same public drawing code across CPU images, retained backend scenes, and WebGPU targets. +## Vector Geometry, Raster Output + +ImageSharp.Drawing lets you describe vector geometry, but the final target is still an ImageSharp raster image unless you are using a retained or GPU backend explicitly. Paths, shapes, strokes, text glyphs, clips, and brushes are converted into pixel coverage during replay. Antialiasing, transforms, blend modes, alpha composition, and layer boundaries all affect that rasterization step. + +Keep geometry, styling, and canvas state separate in your code. Geometry answers where drawing can occur. Brushes and pens answer how covered pixels are shaded. Canvas state answers how later commands are transformed, clipped, blended, grouped, or processed. + ## Draw a Shape Start with geometry, then choose how it is painted. Built-in shapes such as [`StarPolygon`](xref:SixLabors.ImageSharp.Drawing.StarPolygon), [`RectanglePolygon`](xref:SixLabors.ImageSharp.Drawing.RectanglePolygon), and [`EllipsePolygon`](xref:SixLabors.ImageSharp.Drawing.EllipsePolygon) are reusable geometry objects. A brush fills the area covered by the shape, and a pen generates and fills the stroke outline. diff --git a/articles/imagesharp.drawing/index.md b/articles/imagesharp.drawing/index.md index 00c428eb0..b757dc113 100644 --- a/articles/imagesharp.drawing/index.md +++ b/articles/imagesharp.drawing/index.md @@ -34,7 +34,7 @@ Built against [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/d ImageSharp.Drawing is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with ImageSharp.Drawing 3.0.0, projects that directly depend on ImageSharp.Drawing require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with ImageSharp.Drawing 3.0.0, projects that directly depend on ImageSharp.Drawing require a valid Six Labors license at build time. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation @@ -71,9 +71,9 @@ paket add SixLabors.ImageSharp.Drawing --version VERSION_NUMBER ### How to use the license file -Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. +By default, the build searches from each project directory for `sixlabors.lic`. Place the supplied file in the directory that contains the project file, or in a subdirectory below it. Use the file as supplied; it already contains the complete license string required by the build. -If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: +If you want to keep the file somewhere else, including a repository root that sits above the project directory, set `SixLaborsLicenseFile` in your project file or a shared props file: ```xml diff --git a/articles/imagesharp.web/configuration.md b/articles/imagesharp.web/configuration.md index f3d6e8c0e..5d3d03ef1 100644 --- a/articles/imagesharp.web/configuration.md +++ b/articles/imagesharp.web/configuration.md @@ -2,6 +2,17 @@ ImageSharp.Web is assembled through `AddImageSharp()` and the returned [`IImageSharpBuilder`](xref:SixLabors.ImageSharp.Web.IImageSharpBuilder). Most applications never need to replace every piece, but it helps to know what is there so you can change the correct layer without over-customizing the whole pipeline. +## Request Pipeline Model + +For a processed image request, the middleware does four separate jobs: + +- parse and normalize the requested commands; +- resolve the source image through a provider; +- look up or write the processed result through a cache; +- decode, process, and encode the image when the cache does not already contain a valid result. + +Those layers are intentionally separate. Providers are source-of-truth readers, caches store derived output, processors transform the decoded image, and encoders decide the response format. Most customization should replace one layer at a time instead of rebuilding the whole middleware configuration. + ## What `AddImageSharp()` Registers The default registration wires up: diff --git a/articles/imagesharp.web/index.md b/articles/imagesharp.web/index.md index 6e3cdca97..cbffdb467 100644 --- a/articles/imagesharp.web/index.md +++ b/articles/imagesharp.web/index.md @@ -13,7 +13,7 @@ Use ImageSharp.Web when image variants are determined by HTTP requests: responsi ImageSharp.Web is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Web/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with ImageSharp.Web 4.0.0, projects that directly depend on ImageSharp.Web require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with ImageSharp.Web 4.0.0, projects that directly depend on ImageSharp.Web require a valid Six Labors license at build time. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ## Install ImageSharp.Web @@ -50,9 +50,9 @@ paket add SixLabors.ImageSharp.Web --version VERSION_NUMBER ## How to use the license file -Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. +By default, the build searches from each project directory for `sixlabors.lic`. Place the supplied file in the directory that contains the project file, or in a subdirectory below it. Use the file as supplied; it already contains the complete license string required by the build. -If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: +If you want to keep the file somewhere else, including a repository root that sits above the project directory, set `SixLaborsLicenseFile` in your project file or a shared props file: ```xml diff --git a/articles/imagesharp/colorprofiles.md b/articles/imagesharp/colorprofiles.md index d755bc7d0..77c1a3849 100644 --- a/articles/imagesharp/colorprofiles.md +++ b/articles/imagesharp/colorprofiles.md @@ -1,23 +1,60 @@ # Color Profiles and Color Conversion -Color management can feel intimidating at first because there are really two related topics hiding under one name: the profiles attached to image files, and the value-level color conversions you may apply in your own code. This page separates those concerns so it is easier to decide when you need metadata preservation, when you need actual color conversion, and when you need both. +Color management in ImageSharp has two related but separate parts: -ImageSharp exposes color management in two different layers: +- Embedded color metadata, such as [`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) and [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile), which describes how encoded color values should be interpreted. +- Color conversion APIs in [`SixLabors.ImageSharp.ColorProfiles`](xref:SixLabors.ImageSharp.ColorProfiles), which convert color values between supported color spaces and profiles. -- Embedded color metadata on decoded images, such as [`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) and [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile). -- Value-level conversion APIs in [`SixLabors.ImageSharp.ColorProfiles`](xref:SixLabors.ImageSharp.ColorProfiles). +Preserving a profile is not the same thing as converting pixels. A profile can remain attached to an image as metadata without changing the decoded pixel values, and a conversion can change pixel values without preserving the original profile in the output file. -Those layers are related, but they are not the same thing. Preserving an embedded ICC profile does not automatically mean pixels were converted, and converting pixels does not automatically mean every output format can store the same profile metadata. +Most applications only need the first part: let ImageSharp decode the image and choose whether to preserve, compact, or convert embedded ICC profile data at the decode boundary. [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) is a lower-level API for code that is explicitly working with color values or custom color pipelines. + +## What ICC Profiles Do + +An ICC profile describes the color space of encoded image data. The same numeric pixel value can represent different visible colors depending on the profile attached to the file. A pixel value that looks correct as sRGB may look too saturated, too dull, or shifted if it is interpreted as Adobe RGB, ProPhoto RGB, a display profile, or a printer profile without conversion. + +That means an ICC profile is not just descriptive trivia. It tells color-managed software how to interpret the numbers in the file and, when needed, how to convert those numbers into another color space while preserving the intended appearance. + +There are two common outcomes: + +- Preserve the profile and pixel values so another color-managed tool can interpret the image later. +- Convert the pixels into a known working space, usually sRGB, so the rest of your pipeline can treat loaded images consistently. + +## What ImageSharp Does + +By default, [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) preserves embedded ICC profiles in metadata and does not transform the decoded pixels. + +Use [`DecoderOptions.ColorProfileHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.ColorProfileHandling) when your decode boundary needs a different policy: + +- [`Preserve`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Preserve) leaves embedded ICC profiles intact. +- [`Compact`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Compact) removes embedded standard sRGB ICC profiles without transforming pixels. +- [`Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert) transforms pixels from the embedded ICC profile to the sRGB v4 profile and removes the original profile. + +```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +DecoderOptions options = new() +{ + // Convert embedded ICC color data to sRGB during decode. + ColorProfileHandling = ColorProfileHandling.Convert +}; + +using Image image = Image.Load(options, "input-with-icc.jpg"); +``` + +`Convert` is useful when you want the rest of your pipeline to operate on normalized sRGB pixel values. `Preserve` is useful when the original profile should stay attached for round-tripping or later export. `Compact` is useful when you want to remove redundant standard sRGB profile data without changing pixel values. ## Inspect Embedded Color Metadata -Embedded color metadata is available through [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata): +Embedded color metadata is exposed through [`ImageMetadata`](xref:SixLabors.ImageSharp.Metadata.ImageMetadata): ```csharp using SixLabors.ImageSharp; using Image image = Image.Load("input.jpg"); +// ICC and CICP are metadata profiles; reading them does not convert pixels. if (image.Metadata.IccProfile is not null) { Console.WriteLine(image.Metadata.IccProfile.Header.ProfileConnectionSpace); @@ -33,49 +70,41 @@ if (image.Metadata.CicpProfile is not null) } ``` -[`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) is the richer general-purpose color profile mechanism. [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile) carries standardized color signaling values such as primaries, transfer characteristics, matrix coefficients, and range information. - -## Control ICC Handling During Decode +[`IccProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Icc.IccProfile) is the richer general-purpose profile format used by many image workflows. [`CicpProfile`](xref:SixLabors.ImageSharp.Metadata.Profiles.Cicp.CicpProfile) stores coding-independent color signaling values such as primaries, transfer characteristics, matrix coefficients, and range. -By default, [`DecoderOptions`](xref:SixLabors.ImageSharp.Formats.DecoderOptions) preserves embedded ICC profiles in metadata and does not transform the decoded pixels. +## Color Profile Types Are Not Pixel Formats -If you need different behavior, use [`DecoderOptions.ColorProfileHandling`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.ColorProfileHandling): +The value types used by [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) are not [`Image`](xref:SixLabors.ImageSharp.Image`1) storage formats. -- [`Preserve`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Preserve) keeps embedded ICC profiles intact. -- [`Compact`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Compact) removes canonical sRGB ICC profiles without changing the pixels. -- [`Convert`](xref:SixLabors.ImageSharp.Formats.ColorProfileHandling.Convert) converts decoded pixels to sRGB v4 and removes the original ICC profile. +Types such as [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb), [`Cmyk`](xref:SixLabors.ImageSharp.ColorProfiles.Cmyk), [`Hsl`](xref:SixLabors.ImageSharp.ColorProfiles.Hsl), [`YCbCr`](xref:SixLabors.ImageSharp.ColorProfiles.YCbCr), [`CieLab`](xref:SixLabors.ImageSharp.ColorProfiles.CieLab), and [`CieXyz`](xref:SixLabors.ImageSharp.ColorProfiles.CieXyz) model color values for conversion. They are not general-purpose pixel formats and do not implement [`IPixel`](xref:SixLabors.ImageSharp.PixelFormats.IPixel`1). -```csharp -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; +That distinction matters because ImageSharp image processing is built around pixel formats that can move efficiently through RGBA-oriented [`Vector4`](xref:System.Numerics.Vector4) conversion paths. Color profile types model color spaces and profile connection spaces instead. -DecoderOptions options = new() -{ - ColorProfileHandling = ColorProfileHandling.Convert -}; +## Convert Color Values -using Image image = Image.Load(options, "input-with-icc.jpg"); -``` - -This is useful when you want a decode pipeline that normalizes images into a predictable sRGB output space up front. +Use [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) to convert individual color values or spans of values: -## Color Profile Values Are Not `TPixel` Formats +```csharp +using SixLabors.ImageSharp.ColorProfiles; -The value types used by [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) are not the same thing as ImageSharp pixel formats. +ColorProfileConverter converter = new(); -Types such as [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb), [`Cmyk`](xref:SixLabors.ImageSharp.ColorProfiles.Cmyk), [`Hsl`](xref:SixLabors.ImageSharp.ColorProfiles.Hsl), [`YCbCr`](xref:SixLabors.ImageSharp.ColorProfiles.YCbCr), [`CieLab`](xref:SixLabors.ImageSharp.ColorProfiles.CieLab), and [`CieXyz`](xref:SixLabors.ImageSharp.ColorProfiles.CieXyz) are value types used for color-space conversion. They participate in the [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) pipeline, but they are not [`Image`](xref:SixLabors.ImageSharp.Image`1) storage formats and do not implement [`IPixel`](xref:SixLabors.ImageSharp.PixelFormats.IPixel`1). +Rgb rgb = new(0.25F, 0.5F, 0.75F); +CieLab lab = converter.Convert(rgb); +``` -That distinction matters because ImageSharp image processing is built around pixel formats that can move efficiently through RGBA-oriented [`Vector4`](xref:System.Numerics.Vector4) conversion paths. Color profile types model color spaces and profile connection spaces instead. +The converter works with color-profile value types, not whole images. This is appropriate when your own code is calculating, sampling, comparing, or exporting color values directly. -## Convert Between Working Spaces +## Convert Between RGB Working Spaces -[`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) converts color values and spans between supported profile types. For RGB-to-RGB conversions, the working spaces come from [`ColorConversionOptions`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions): +RGB-to-RGB conversion can still change values when the source and destination working spaces are different. Set the working spaces through [`ColorConversionOptions`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions): ```csharp using SixLabors.ImageSharp.ColorProfiles; ColorProfileConverter converter = new(new ColorConversionOptions { + // The same Rgb value type can represent different RGB working spaces. SourceRgbWorkingSpace = KnownRgbWorkingSpaces.SRgb, TargetRgbWorkingSpace = KnownRgbWorkingSpaces.AdobeRgb1998 }); @@ -84,11 +113,9 @@ Rgb source = new(0.25F, 0.5F, 0.75F); Rgb converted = converter.Convert(source); ``` -The source and target value types are both [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb) here, but the conversion still changes because the working-space definitions are different. This is value-level color conversion, not a change to the in-memory `TPixel` type of an [`Image`](xref:SixLabors.ImageSharp.Image`1). +The source and target value types are both [`Rgb`](xref:SixLabors.ImageSharp.ColorProfiles.Rgb), but the working-space definitions are different. This is value-level color conversion, not a change to the in-memory `TPixel` type of an image. -## Choose Working Spaces Explicitly - -[`KnownRgbWorkingSpaces`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces) exposes the built-in RGB working spaces, including: +[`KnownRgbWorkingSpaces`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces) includes common built-in working spaces such as: - [`SRgb`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.SRgb) - [`Rec709`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.Rec709) @@ -97,31 +124,50 @@ The source and target value types are both [`Rgb`](xref:SixLabors.ImageSharp.Col - [`ProPhotoRgb`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.ProPhotoRgb) - [`WideGamutRgb`](xref:SixLabors.ImageSharp.ColorProfiles.KnownRgbWorkingSpaces.WideGamutRgb) -Choose the working spaces explicitly when you are doing color conversion outside the normal decode pipeline and need to be clear about the source and destination assumptions. +Choose the source and target working spaces explicitly when color values come from a known space outside the normal image decode path. + +## Convert Using ICC Profiles -## Use ICC Profiles for Explicit Conversion +When you have actual source and destination ICC profiles, set [`SourceIccProfile`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.SourceIccProfile) and [`TargetIccProfile`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.TargetIccProfile): -If you have actual ICC profiles for the source and destination, set [`ColorConversionOptions.SourceIccProfile`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.SourceIccProfile) and [`ColorConversionOptions.TargetIccProfile`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.TargetIccProfile). The same [`ColorProfileConverter`](xref:SixLabors.ImageSharp.ColorProfiles.ColorProfileConverter) API will then use the ICC-based conversion path instead of only the working-space defaults. +```csharp +using System.IO; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +IccProfile sourceProfile = new(File.ReadAllBytes("source.icc")); +IccProfile targetProfile = new(File.ReadAllBytes("target.icc")); + +ColorProfileConverter converter = new(new ColorConversionOptions +{ + // Supplying both ICC profiles selects the ICC-based conversion path. + SourceIccProfile = sourceProfile, + TargetIccProfile = targetProfile +}); + +Rgb source = new(0.25F, 0.5F, 0.75F); +Rgb converted = converter.Convert(source); +``` -This is the right choice when the embedded or device-specific ICC data matters more than a generic named working space. +When both ICC profiles are supplied, the converter uses the ICC conversion path for supported source and destination color value types. Use this path when device-specific or embedded profile data matters more than a generic named working space. ## Advanced Conversion Options -[`ColorConversionOptions`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions) also lets you tune lower-level conversion details: +[`ColorConversionOptions`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions) also exposes lower-level settings for specialized workflows: - [`SourceWhitePoint`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.SourceWhitePoint) and [`TargetWhitePoint`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.TargetWhitePoint) - [`AdaptationMatrix`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.AdaptationMatrix), which defaults to [`KnownChromaticAdaptationMatrices.Bradford`](xref:SixLabors.ImageSharp.ColorProfiles.KnownChromaticAdaptationMatrices.Bradford) - [`YCbCrTransform`](xref:SixLabors.ImageSharp.ColorProfiles.ColorConversionOptions.YCbCrTransform), which defaults to [`KnownYCbCrMatrices.BT601`](xref:SixLabors.ImageSharp.ColorProfiles.KnownYCbCrMatrices.BT601) -Most applications do not need to override those defaults, but they are available when you need tighter control over conversion behavior. +Most application code should leave these defaults alone. Change them only when your color pipeline has an explicit requirement for a different white point, chromatic adaptation matrix, or YCbCr transform. ## Practical Guidance -- Preserve embedded ICC metadata when you need round-tripping or want the original profile to stay attached to the image. -- Use decode-time `ColorProfileHandling.Convert` when you want pixels normalized to sRGB as soon as the image is loaded. -- Use `Compact` when you want to remove redundant canonical sRGB ICC profiles without changing pixel values. -- Do not confuse metadata preservation with pixel conversion. They solve different problems. -- Do not confuse color profile value types with ImageSharp pixel formats. `ColorProfileConverter` works on color-space values, while `Image` works with `IPixel` storage types. +- Preserve embedded ICC metadata when the original profile should remain attached to the image. +- Use decode-time `ColorProfileHandling.Convert` when you want loaded images normalized to sRGB pixel values. +- Use `ColorProfileHandling.Compact` when you want to remove redundant standard sRGB profile data without changing pixels. +- Use `ColorProfileConverter` when you are converting color values in your own code rather than changing file metadata. +- Keep pixel format decisions separate from color profile decisions. `Image` describes memory layout; ICC and CICP data describe color interpretation. ## Related Topics diff --git a/articles/imagesharp/formatconversion.md b/articles/imagesharp/formatconversion.md index ee82dadc1..2f07ea50c 100644 --- a/articles/imagesharp/formatconversion.md +++ b/articles/imagesharp/formatconversion.md @@ -1,11 +1,23 @@ # Convert Between Formats -Format conversion is one of the most common reasons people adopt ImageSharp in the first place. The nice part is that you usually do not have to think in terms of format-to-format adapters; you load into ImageSharp's common image model, make any changes you need, and then save to the destination path, stream, or encoder. +Format conversion is one of the most common reasons people adopt ImageSharp. In ImageSharp, conversion is a decode-and-encode workflow: load into the common image model, make any changes the output requires, then save to the destination path, stream, or encoder. That decode-and-re-encode flow is not a blind one. Once an image is loaded, the processing pipeline works with format-agnostic pixel data, while the metadata layer still carries enough information for the destination format to choose the best representation it supports. For common conversions, saving to a destination path or format is intentionally useful. ImageSharp combines the decoded image, bridged metadata, pixel information, and registered encoder defaults to produce strong automated output. Use explicit encoders when your application has a specific output policy to express, not because the default conversion path is something to avoid. +## What Conversion Changes + +Format conversion has three separate parts: + +- Decode the source format into ImageSharp's image model. +- Optionally process or transform the image. +- Encode the result into the destination format. + +The destination encoder decides what can be represented in the output file. If the target format cannot store something from the source, that information must be transformed, approximated, or dropped. Common examples are alpha transparency when writing JPEG, high bit depth when writing an 8-bit output, rich TIFF metadata when writing a simpler web format, or animation timing when writing a single-frame format. + +Changing the file format does not automatically choose a better working pixel type, repair lossy compression damage, apply EXIF orientation, flatten transparency, or convert color profiles into a preferred output space. Those are separate policy decisions. ImageSharp gives you APIs for each step, but the conversion boundary is where your application decides what the destination file is allowed to mean. + ## How ImageSharp Bridges Formats ImageSharp's built-in codec metadata translates through [`FormatConnectingMetadata`](xref:SixLabors.ImageSharp.Formats.FormatConnectingMetadata) and [`FormatConnectingFrameMetadata`](xref:SixLabors.ImageSharp.Formats.FormatConnectingFrameMetadata). Those bridge types carry the common image and frame semantics that can be shared across formats, including: @@ -15,7 +27,7 @@ ImageSharp's built-in codec metadata translates through [`FormatConnectingMetada - Indexed-color settings such as shared color table mode. - Animation settings such as background color, repeat count, frame duration, blend mode, and disposal mode. -That is why ImageSharp's conversion story is more comprehensive than simply decoding everything to one in-memory layout and forgetting how the source was encoded. For example, PNG metadata can derive palette, grayscale, RGB, or RGBA output and choose 1, 2, 4, 8, or 16-bit encoding from bridged pixel information, while GIF metadata can carry indexed color-table mode and repeat-count behavior forward when the target format supports them. These bridges are what make the automatic conversion APIs useful for real application workflows rather than only toy examples. +That is why ImageSharp's conversion model carries more information than a raw pixel buffer alone. For example, PNG metadata can derive palette, grayscale, RGB, or RGBA output and choose 1, 2, 4, 8, or 16-bit encoding from bridged pixel information, while GIF metadata can carry indexed color-table mode and repeat-count behavior forward when the target format supports them. These bridges make the automatic conversion APIs useful for real application workflows where pixel data, metadata, and target format capabilities all matter. ## Use Identify to Plan the Conversion @@ -119,6 +131,8 @@ else - Converting from a lossy format to a lossless format does not restore discarded detail. - Converting a transparent image to JPEG requires flattening or compositing first. +- Converting to a palette or lower-bit-depth format is a color-reduction step. +- Converting an animated image to a single-frame format requires choosing which frame or composited result you want. - ImageSharp uses bridged metadata, pixel-type information, and encoder defaults to pick good destination settings when the target format can represent them. - Save-by-extension is the simplest and recommended path for ordinary conversions. Pass an explicit encoder when you want to override defaults for quality, compression, bit depth, palette behavior, metadata handling, or another application policy. - Format conversion is also a metadata decision. Decide whether orientation, color profiles, animation timing, and authoring metadata should be preserved, transformed, or stripped. diff --git a/articles/imagesharp/imageformats.md b/articles/imagesharp/imageformats.md index 810273f18..c007110b4 100644 --- a/articles/imagesharp/imageformats.md +++ b/articles/imagesharp/imageformats.md @@ -4,6 +4,49 @@ ImageSharp keeps the in-memory image model separate from the file format on disk This page is the format map for the library: which built-in formats ship by default, what each one is good at, and where to go next for format-specific guidance. +## Format, Codec, and Pixel Type + +These terms refer to different parts of the imaging pipeline: + +- A file format is the encoded representation on disk or in a stream, such as JPEG, PNG, WebP, or TIFF. +- A decoder reads encoded data from a file format and produces an [`Image`](xref:SixLabors.ImageSharp.Image) or [`Image`](xref:SixLabors.ImageSharp.Image`1). +- An encoder writes an image back to a chosen file format. +- A pixel type, such as [`Rgba32`](xref:SixLabors.ImageSharp.PixelFormats.Rgba32), describes the in-memory representation used while the image is loaded and processed. +- Metadata describes information carried alongside the pixels, such as orientation, ICC profiles, frame timing, comments, and format-specific tags. + +Changing the file format is not the same operation as changing the in-memory pixel type. Saving an `Image` as JPEG writes JPEG-encoded data from RGBA pixels; loading a PNG as `Image` converts decoded image samples into an 8-bit luminance pixel buffer. Metadata handling is a separate concern again, controlled by decoder and encoder options. + +## What a Format Decides + +An image file format is not only a filename extension. It defines which image information can be represented and how that information is stored. The important questions are: + +- Is the encoded pixel data compressed, and is that compression lossy or lossless? +- Which color models, bit depths, and component precisions can the format represent? +- Can the format store alpha transparency, and if so is it full alpha or index-based transparency? +- Can the format store multiple frames, animation timing, blending, and disposal behavior? +- Which metadata can be represented, such as EXIF, ICC profiles, text chunks, frame metadata, or format-specific tags? +- Which applications, browsers, operating systems, and asset pipelines need to read the output? + +These questions are why there is no universal "best" image format. JPEG, PNG, GIF, WebP, TIFF, and OpenEXR are not interchangeable containers with different extensions; they preserve and discard different parts of the image model. + +## Delivery, Interchange, and Working Formats + +Many format decisions become clearer when you separate the job the file has to do: + +- Delivery formats prioritize compatibility, size, and decode behavior for the consuming client. JPEG, PNG, GIF, and WebP are common examples. +- Interchange formats preserve information for another tool or workflow. TIFF, OpenEXR, TGA, BMP, QOI, and Netpbm-style formats can be useful here depending on the pipeline. +- Working formats are the files you keep before final export. They may be larger or richer than the public output because they need to preserve editability, metadata, precision, layers in another application, or a lossless source for later conversions. + +ImageSharp works with raster images. Vector artwork, document formats, and application-native design files are outside the built-in codec set, although you can render or import them through other tools before handing raster pixels to ImageSharp. + +## Compression and Re-encoding + +Lossless formats preserve the decoded pixel values exactly, subject to the color and pixel representation chosen by the encoder. PNG, QOI, lossless WebP, and many TIFF configurations fall into this category. + +Lossy formats intentionally discard information to reduce file size. JPEG and lossy WebP are useful because the loss is often acceptable for photographs, but re-encoding lossy inputs can compound artifacts. Converting a JPEG to PNG does not restore detail that the JPEG encoder already removed; it only stores the current decoded pixels losslessly. + +Some formats can be either lossy or lossless depending on encoder settings. WebP and TIFF are format families with multiple encoding modes, so the encoder configuration matters as much as the extension. + ## Built-In Formats The source of truth for the built-in format list is [`Configuration`](xref:SixLabors.ImageSharp.Configuration): the default ImageSharp configuration preregisters encoder, decoder, and detector modules for the following public [`IImageFormat`](xref:SixLabors.ImageSharp.Formats.IImageFormat) types: @@ -46,6 +89,8 @@ Another way to think about it: No single format is best everywhere. The right choice depends on whether your priority is fidelity, file size, transparency, animation, compatibility, or workflow metadata. +When the output crosses a boundary you do not control, compatibility usually outranks theoretical capability. A format can support a feature and still be a poor choice if the receiving client, CDN, print tool, browser, or asset pipeline handles that feature inconsistently. + ## Load, Detect, and Preserve Formats [`Image`](xref:SixLabors.ImageSharp.Image`1) represents decoded pixel data. Once an image is loaded into memory, it is no longer tied to a specific file format unless you explicitly inspect or preserve that information. diff --git a/articles/imagesharp/index.md b/articles/imagesharp/index.md index fd5a00efb..0ad6ac7c1 100644 --- a/articles/imagesharp/index.md +++ b/articles/imagesharp/index.md @@ -13,7 +13,7 @@ For production code, the important choices are usually not individual method nam ImageSharp is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with ImageSharp 4.0.0, projects that directly depend on ImageSharp require a valid Six Labors license at build time. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ## Install ImageSharp @@ -50,9 +50,9 @@ paket add SixLabors.ImageSharp --version VERSION_NUMBER ## How to use the license file -Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. +By default, the build searches from each project directory for `sixlabors.lic`. Place the supplied file in the directory that contains the project file, or in a subdirectory below it. Use the file as supplied; it already contains the complete license string required by the build. -If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: +If you want to keep the file somewhere else, including a repository root that sits above the project directory, set `SixLaborsLicenseFile` in your project file or a shared props file: ```xml diff --git a/articles/imagesharp/loadingandsaving.md b/articles/imagesharp/loadingandsaving.md index 3f5fbb3c1..d6a83c89c 100644 --- a/articles/imagesharp/loadingandsaving.md +++ b/articles/imagesharp/loadingandsaving.md @@ -4,6 +4,16 @@ Most ImageSharp applications start here. Whether images come from disk, streams, The core idea is straightforward: use `Image.Load()` when you need pixels, `Image.Identify()` when you only need dimensions or metadata, and `Image.DetectFormat()` when you only need to know what kind of file you were given. +## The Three Questions + +Loading APIs answer different questions: + +- `DetectFormat(...)` asks which registered detector recognizes the encoded bytes. +- `Identify(...)` asks what the decoder can learn from the headers and metadata without allocating the full pixel buffer. +- `Load(...)` asks the decoder to materialize pixel data into an `Image` or `Image`. + +Those operations are intentionally separate. A file extension can be wrong, a header can be recognizable while the image data is still corrupt, and a small compressed file can decode into a large pixel buffer. Production code should choose the cheapest operation that answers the current question, then still handle failure at the later decode boundary. + ## Load Images You can load images from a file path, a stream, or an in-memory byte buffer: diff --git a/articles/imagesharp/memorymanagement.md b/articles/imagesharp/memorymanagement.md index cf29325d4..e0c325bc9 100644 --- a/articles/imagesharp/memorymanagement.md +++ b/articles/imagesharp/memorymanagement.md @@ -4,6 +4,12 @@ ImageSharp is designed so large images are practical to process without forcing This page explains the parts most developers eventually need: the default pooled allocator, when to customize it, and how those choices affect lower-level interop code. +## Compressed Size Is Not Memory Size + +An encoded file can be much smaller than the image memory needed to process it. A 20 MB JPEG may decode into hundreds of megabytes of pixel data, and a multi-frame image can require far more when all frames are loaded. Memory planning should therefore start from decoded dimensions, pixel format, and frame count rather than the source file size alone. + +Use [`Image.Identify()`](xref:SixLabors.ImageSharp.Image.Identify*) and [`ImageInfo.GetPixelMemorySize()`](xref:SixLabors.ImageSharp.ImageInfo.GetPixelMemorySize) when you need to estimate decoded memory before loading pixels. Use [`DecoderOptions.MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) and [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) when the workload can deliberately load less data. + ## Default Behavior [`Configuration.MemoryAllocator`](xref:SixLabors.ImageSharp.Configuration.MemoryAllocator) defaults to [`MemoryAllocator.Default`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator.Default). For most applications, that default allocator is the right choice. @@ -22,11 +28,12 @@ Configuration config = Configuration.Default.Clone(); config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { MaximumPoolSizeMegabytes = 128, - AllocationLimitMegabytes = 1024 + AllocationLimitMegabytes = 1024, + AccumulativeAllocationLimitMegabytes = 2048 }); ``` -[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow for one live allocation group. +[`MaximumPoolSizeMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.MaximumPoolSizeMegabytes) controls retained pool size. [`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) controls the maximum discontiguous buffer size the allocator will allow for one live allocation group. When it is unset, the platform default is 1 GB on 32-bit processes and 4 GB on 64-bit processes. [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) controls the maximum combined size of all active allocations made through that allocator instance. It is unset by default, so the allocator does not impose an accumulative cap unless you configure one. ## Prefer Contiguous Buffers Only When You Need Them @@ -90,6 +97,7 @@ That tells the allocator to drop retained pooled buffers that are no longer need - Keep [`MemoryAllocator.Default`](xref:SixLabors.ImageSharp.Memory.MemoryAllocator.Default) unless profiling shows a real need to customize it. - Use one shared allocator per process rather than many temporary allocators. +- Use [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) when the total amount of live ImageSharp allocation should be capped, not only the size of one image allocation group. - Avoid forcing contiguous buffers unless you truly need a single `Memory` or pointer. - Use [`DecoderOptions.TargetSize`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.TargetSize) and [`DecoderOptions.MaxFrames`](xref:SixLabors.ImageSharp.Formats.DecoderOptions.MaxFrames) when you want to limit decode cost up front. - Track leaked images with [`MemoryDiagnostics`](xref:SixLabors.ImageSharp.Diagnostics.MemoryDiagnostics) if disposal bugs are suspected. diff --git a/articles/imagesharp/pixelbuffers.md b/articles/imagesharp/pixelbuffers.md index ade1f72b8..0b7b263f3 100644 --- a/articles/imagesharp/pixelbuffers.md +++ b/articles/imagesharp/pixelbuffers.md @@ -4,6 +4,12 @@ When you first start with ImageSharp, the indexer is often enough. As soon as pe This page is the map for that lower-level work. +## Pixel Buffers Are Decoded Image Data + +Pixel-buffer APIs expose the decoded raster grid in memory. They do not expose the original file bytes and they do not preserve format-specific packing such as JPEG blocks, PNG filters, GIF color-table indexes, or TIFF strip layout. By the time you are working with row spans, ImageSharp has decoded the source into the chosen `TPixel` representation. + +Rows are addressed by image coordinates: `y` selects a row and `x` selects a pixel within that row. The APIs are row-oriented because image memory is optimized for scanning contiguous rows, even when a large image is backed by several internal buffers instead of one single allocation. + ## Choose the Right Access Pattern Use: diff --git a/articles/imagesharp/processing.md b/articles/imagesharp/processing.md index a87ab24b6..e20fd29d5 100644 --- a/articles/imagesharp/processing.md +++ b/articles/imagesharp/processing.md @@ -9,6 +9,12 @@ The main entry points are [`Mutate`](xref:SixLabors.ImageSharp.Processing.Proces Processors are deliberately composable. Each call in the pipeline receives the result of the previous call, so the code order is also the image-processing order. That makes pipelines easy to read, but it also means a misplaced operation can change the result significantly. +## The Raster Processing Model + +ImageSharp processes raster images: a rectangular grid of pixels with a width, height, pixel type, and optional frame collection. Processing changes that in-memory image model. It does not work on encoded JPEG blocks, PNG chunks, file extensions, or metadata records unless a processor is specifically about those concerns. + +That distinction matters when designing a pipeline. A crop removes pixel regions from the decoded image. A resize resamples pixels into a new grid. A color effect changes pixel values. An encoder later decides how those processed pixels become JPEG, PNG, WebP, TIFF, or another file format. Metadata such as EXIF orientation and ICC profiles can influence the right processing choices, but they are not the same thing as pixel processing. + ## Mutate the Current Image Use `Mutate()` when you want to transform the current image in place: diff --git a/articles/imagesharp/security.md b/articles/imagesharp/security.md index 41786db47..49012bada 100644 --- a/articles/imagesharp/security.md +++ b/articles/imagesharp/security.md @@ -89,13 +89,15 @@ using SixLabors.ImageSharp.Memory; Configuration config = Configuration.Default.Clone(); config.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { - // Roughly limits the workload to about 64 megapixels of Rgba32 data. - AllocationLimitMegabytes = 256 + AllocationLimitMegabytes = 256, + + // Limits the combined size of all active allocations made through this allocator. + AccumulativeAllocationLimitMegabytes = 512 }); ``` -[`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) limits the size of any one live allocation group. +[`AllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AllocationLimitMegabytes) limits the size of any one live allocation group. When it is unset, ImageSharp uses the platform default: 1 GB on 32-bit processes and 4 GB on 64-bit processes. [`AccumulativeAllocationLimitMegabytes`](xref:SixLabors.ImageSharp.Memory.MemoryAllocatorOptions.AccumulativeAllocationLimitMegabytes) limits the combined size of all active allocations made through the allocator instance. It is unset by default, which means there is no accumulative cap unless you configure one. Use both limits together when a service can process several images or several intermediate buffers at the same time. This is one of the most important safeguards for services that handle arbitrary uploads. For broader guidance on allocator behavior and tradeoffs, see [Memory Management](memorymanagement.md). @@ -120,4 +122,4 @@ For ImageSharp.Web command signing, see [Securing Requests in ImageSharp.Web](.. - Use `TargetSize`, `MaxFrames`, and `SkipMetadata` to shrink decode cost up front. - Prefer [`Strict`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.Strict) or the default [`IgnoreAncillary`](xref:SixLabors.ImageSharp.Formats.SegmentIntegrityHandling.IgnoreAncillary) over broader error ignoring on untrusted inputs. - Restrict the enabled format modules when your workload only needs a few codecs. -- Use allocator limits and host-level request limits together rather than relying on only one layer. +- Use per-allocation, accumulative allocator, and host-level request limits together rather than relying on only one layer. diff --git a/articles/polygonclipper/index.md b/articles/polygonclipper/index.md index 4abb51fc0..c4122993f 100644 --- a/articles/polygonclipper/index.md +++ b/articles/polygonclipper/index.md @@ -14,7 +14,7 @@ Most users should begin with the static boolean, normalization, and stroking ent PolygonClipper is licensed under the terms of the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE). See https://sixlabors.com/pricing for commercial licensing details. >[!IMPORTANT] ->Starting with PolygonClipper 1.0.0, projects that directly depend on SixLabors.PolygonClipper require a valid Six Labors license at build time. Add `sixlabors.lic` to your repository root, set `SixLaborsLicenseFile`, or set `SixLaborsLicenseKey`. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. +>Starting with PolygonClipper 1.0.0, projects that directly depend on SixLabors.PolygonClipper require a valid Six Labors license at build time. This enforcement applies to direct dependencies only. See [License Enforcement Changes and a New Subscription Tier](https://sixlabors.com/posts/licence-enforcement-changes/) for details. ### Installation @@ -51,9 +51,9 @@ paket add SixLabors.PolygonClipper --version VERSION_NUMBER ### How to use the license file -Add the supplied `sixlabors.lic` file to your repository root. Use the file as supplied; it already contains the complete license string required by the build. +By default, the build searches from each project directory for `sixlabors.lic`. Place the supplied file in the directory that contains the project file, or in a subdirectory below it. Use the file as supplied; it already contains the complete license string required by the build. -If you want to keep the file somewhere else, set `SixLaborsLicenseFile` in your project file or a shared props file: +If you want to keep the file somewhere else, including a repository root that sits above the project directory, set `SixLaborsLicenseFile` in your project file or a shared props file: ```xml diff --git a/articles/polygonclipper/polygonsandcontours.md b/articles/polygonclipper/polygonsandcontours.md index 7304cc04f..550e865ff 100644 --- a/articles/polygonclipper/polygonsandcontours.md +++ b/articles/polygonclipper/polygonsandcontours.md @@ -10,6 +10,12 @@ That small model is enough to describe simple shapes, complex multi-contour shap The important mental model is that contours are topology, not styling. PolygonClipper does not know about brushes, pens, fill colors, or pixels. It returns geometry that another layer can render, serialize, hit-test, or combine with more geometry. +## Regions, Not Drawing Commands + +PolygonClipper treats polygons as filled regions bounded by contours. It is not a path recorder and it does not preserve the original drawing commands that produced the contour data. After normalization, clipping, or boolean operations, the output may contain different contours because the library is describing the resulting region, not the editing history. + +This is the same distinction that matters in rendering systems: a path is a set of geometric edges, while a filled region is the area those edges enclose under a fill rule. PolygonClipper operates on the region model. + ## A `Contour` Is One Ring A [`Contour`](xref:SixLabors.PolygonClipper.Contour) is a sequence of vertices. For clipping and normalization, it is treated as implicitly closed, so the library always considers an edge between the last vertex and the first vertex. diff --git a/articles/toc.md b/articles/toc.md index a50559730..a48471163 100644 --- a/articles/toc.md +++ b/articles/toc.md @@ -85,7 +85,8 @@ ## [Selection and Bidi Drag](fonts/caretsandselection.md) ## [Text Layout and Options](fonts/textlayout.md) ## [OpenType Features](fonts/opentypefeatures.md) -## [Hinting and Shaping](fonts/hintingandshaping.md) +## [Text Shaping](fonts/shaping.md) +## [TrueType Hinting](fonts/hinting.md) ## [Color Fonts](fonts/colorfonts.md) ## [Unicode, Code Points, and Graphemes](fonts/unicode.md) ## [Fallback Fonts and Multilingual Text](fonts/fallbackfonts.md) diff --git a/ext/ImageSharp b/ext/ImageSharp index 2b1c7e0cf..ff36e83c7 160000 --- a/ext/ImageSharp +++ b/ext/ImageSharp @@ -1 +1 @@ -Subproject commit 2b1c7e0cf1d38494c9467f47b68b1d33a8aaf356 +Subproject commit ff36e83c740e8049574eb468798151e96a176f12 diff --git a/ext/ImageSharp.Drawing b/ext/ImageSharp.Drawing index c063e6ca3..0019fdb43 160000 --- a/ext/ImageSharp.Drawing +++ b/ext/ImageSharp.Drawing @@ -1 +1 @@ -Subproject commit c063e6ca3c238aed30efe19e9f82ee4cde2240f6 +Subproject commit 0019fdb439ff85378427bfb8d5d51ba9f69b89b7 diff --git a/ext/ImageSharp.Web b/ext/ImageSharp.Web index fefde84b7..592083f26 160000 --- a/ext/ImageSharp.Web +++ b/ext/ImageSharp.Web @@ -1 +1 @@ -Subproject commit fefde84b756d0b8783cfe63b824526afc0f6a164 +Subproject commit 592083f26961650592a3578f2058c01b242989df diff --git a/index.md b/index.md index 5852a9922..6cd8cfcb0 100644 --- a/index.md +++ b/index.md @@ -1,26 +1,39 @@ # Six Labors Documentation -Six Labors builds high-performance, cross-platform graphics libraries for modern .NET. The libraries are designed for production workloads across cloud services, desktop applications, mobile devices, and embedded/IoT environments. +Six Labors builds high-performance, cross-platform graphics libraries for modern .NET applications. The libraries are designed for production workloads where image quality, throughput, memory use, correctness, and predictable deployment all matter. -This documentation covers the full Six Labors stack: ImageSharp for image processing, ImageSharp.Drawing for 2D vector drawing, ImageSharp.Web for ASP.NET Core image middleware, Fonts for advanced text layout, and PolygonClipper for geometry operations. +The stack is intentionally layered. ImageSharp is the imaging foundation; ImageSharp.Drawing adds canvas drawing, vector geometry, image composition, text, and optional WebGPU output; ImageSharp.Web turns ImageSharp into ASP.NET Core middleware for web delivery; Fonts provides the text engine for shaping, measuring, layout, and rendering; and PolygonClipper provides robust polygon boolean operations, normalization, and stroke geometry. ->[!NOTE] ->Documentation for previous releases can be found at . +Use the articles when you are learning a workflow, making architectural choices, or trying to understand how the pieces fit together. Use the API reference when you need the exact public contract for a type, method, property, option, or enum value. ### [API documentation](api/index.md) -Browse the generated API reference for every public type, method, property, and option across the Six Labors projects. +The generated API reference covers public types and members across the Six Labors projects. It is the place to check overloads, constructors, option defaults, enum values, inherited members, extension methods, and namespace organization once you know which feature you are using. + +The reference pages are generated from source-level documentation. They describe the observable public API contract; implementation details live in the source repositories and are intentionally not repeated in the reference unless they are part of the behavior developers can rely on. + +### How to Use These Docs + +Start with the product article that matches the problem you are solving, then move outward: + +- Use ImageSharp when you need to load, identify, resize, transform, inspect, convert, encode, or work directly with pixels. +- Add ImageSharp.Drawing when generated output needs shapes, paths, brushes, pens, text, overlays, masks, layers, or GPU-backed drawing targets. +- Use ImageSharp.Web when images are requested through ASP.NET Core and should be resized, encoded, cached, signed, or served as named variants. +- Use Fonts directly when text layout is the product concern: measurement, shaping, fallback, hit testing, caret movement, selection, variable fonts, color fonts, or custom renderers. +- Use PolygonClipper when your application owns polygon data and needs boolean operations, contour cleanup, winding normalization, or generated stroke outlines. + +Most real systems use more than one package. A web image pipeline might use ImageSharp.Web for public requests, ImageSharp for encoder policy, ImageSharp.Drawing for watermarks, Fonts for localized labels, and PolygonClipper for complex mask geometry. The docs are organized so those boundaries stay visible. ### Project Documentation -Each library is focused, but the projects are designed to work together as one graphics stack. Start with the product area closest to your task, then follow the linked guides into formats, drawing, text, middleware, or geometry as needed. +Each library is focused, but the projects are designed to work together as one graphics stack. Start with the product area closest to your task, then follow the linked guides into formats, resizing, drawing, text layout, middleware, or geometry as your workflow expands.
ImageSharp Logo
ImageSharp
-

Image processing for .NET with broad format support, pixel-level control, and rich metadata handling.

+

High-performance managed image processing for .NET with broad format support, color management, and pixel-level control.

Learn More @@ -30,7 +43,7 @@ Each library is focused, but the projects are designed to work together as one g
ImageSharp.Drawing
-

2D drawing and text rendering for ImageSharp with paths, brushes, and rich typography.

+

High-performance canvas drawing for ImageSharp with paths, brushes, rich text, composition, and WebGPU output.

Learn More @@ -40,7 +53,7 @@ Each library is focused, but the projects are designed to work together as one g
ImageSharp.Web
-

On-the-fly image processing, caching, and secure delivery for ASP.NET Core.

+

High-performance on-the-fly image processing, caching, signing, and extensible delivery for ASP.NET Core.

Learn More @@ -50,7 +63,7 @@ Each library is focused, but the projects are designed to work together as one g
Fonts
-

Advanced font loading, shaping, measuring, layout, and rendering for .NET.

+

High-performance font loading, shaping, layout, measurement, inspection, and custom text rendering for .NET.

Learn More @@ -60,7 +73,7 @@ Each library is focused, but the projects are designed to work together as one g
PolygonClipper Logo
PolygonClipper
-

Boolean operations, normalization, and stroke geometry for .NET.

+

High-performance polygon booleans, contour hierarchy, normalization, and stroke-outline geometry for .NET.

Learn More @@ -68,6 +81,32 @@ Each library is focused, but the projects are designed to work together as one g
+>[!NOTE] +>Documentation for previous releases can be found at . + +### Common Starting Points + +- New to ImageSharp: start with [ImageSharp Getting Started](articles/imagesharp/gettingstarted.md), then read [Loading, Identifying, and Saving](articles/imagesharp/loadingandsaving.md), [Resizing Images](articles/imagesharp/resize.md), and [Image Formats](articles/imagesharp/imageformats.md). +- Migrating from platform graphics APIs: start with [ImageSharp: Migrating from System.Drawing](articles/imagesharp/migratingfromsystemdrawing.md), [ImageSharp: Migrating from SkiaSharp](articles/imagesharp/migratingfromskiasharp.md), [ImageSharp.Drawing: Migrating from System.Drawing](articles/imagesharp.drawing/migratingfromsystemdrawing.md), or [ImageSharp.Drawing: Migrating from SkiaSharp](articles/imagesharp.drawing/migratingfromskiasharp.md). +- Generating graphics: start with [ImageSharp.Drawing Getting Started](articles/imagesharp.drawing/gettingstarted.md), then move through [Canvas Drawing](articles/imagesharp.drawing/canvas.md), [Paths and Shapes](articles/imagesharp.drawing/pathsandshapes.md), [Brushes and Pens](articles/imagesharp.drawing/brushesandpens.md), and [Drawing Text](articles/imagesharp.drawing/text.md). +- Serving images from ASP.NET Core: start with [ImageSharp.Web Getting Started](articles/imagesharp.web/gettingstarted.md), then read [Configuration and Pipeline](articles/imagesharp.web/configuration.md), [Processing Commands](articles/imagesharp.web/processingcommands.md), and [Securing Requests](articles/imagesharp.web/security.md). +- Working with text: start with [Fonts Loading Fonts and Collections](articles/fonts/gettingstarted.md), then read [Measuring Text](articles/fonts/measuringtext.md), [Prepared Text with TextBlock](articles/fonts/textblock.md), [Text Layout and Options](articles/fonts/textlayout.md), and [Unicode, Code Points, and Graphemes](articles/fonts/unicode.md). +- Working with geometry: start with [PolygonClipper Getting Started](articles/polygonclipper/gettingstarted.md), then read [Polygons, Contours, and Holes](articles/polygonclipper/polygonsandcontours.md), [Boolean Operations](articles/polygonclipper/booleanoperations.md), [Normalization and Winding](articles/polygonclipper/normalization.md), and [Stroking](articles/polygonclipper/stroking.md). + +### What the Guides Cover + +The article guides are written for implementation work, not just feature discovery. They explain the concepts behind the API, the coordinate systems and lifetime rules that matter in real applications, the defaults that are safe to rely on, and the places where you should make policy explicit. + +Across the site you will find guidance for: + +- choosing image formats, encoder settings, metadata policy, color-profile handling, and resize samplers; +- building safe upload and conversion pipelines for untrusted images; +- composing vector drawing, source images, text, clipping, layers, transforms, and processors in one ordered drawing pipeline; +- using WebGPU targets when output should stay on the GPU or be presented directly to a native surface; +- shaping and measuring multilingual text with fallback fonts, OpenType features, color fonts, variable fonts, and grapheme-indexed rich text runs; +- configuring web image delivery with request parsing, named presets, HMAC signing, provider selection, cache behavior, and custom processors; +- modelling polygon data, resolving intersections, choosing fill semantics, and generating stroke geometry for downstream renderers. + ### [Examples Repository](https://github.com/SixLabors/Samples) -The [Six Labors Samples](https://github.com/SixLabors/Samples) repository contains small, self-contained projects that show common workflows end to end. +The [Six Labors Samples](https://github.com/SixLabors/Samples) repository contains small, self-contained projects that show common workflows end to end. Use it when you want runnable code beside the conceptual guides and API reference. diff --git a/templates/modern/public/main.css b/templates/modern/public/main.css index 920fb0d2e..36a58d23c 100644 --- a/templates/modern/public/main.css +++ b/templates/modern/public/main.css @@ -12,13 +12,18 @@ body { h1, h2, h3 { - font-weight: 800; + font-weight: 800!important; text-decoration: none; line-height: 1.15; word-wrap: none !important; word-break: normal !important; } +h4, +h5 { + font-weight: 600 !important; +} + h1 { font-size: 2.5rem; margin: 1rem 0; @@ -225,6 +230,10 @@ code { background-color: #efefef; } +html[data-bs-theme="dark"] .frame::before { + background-color: #333; +} + .frame::after { transform: rotate(2.5deg); background-image: linear-gradient(45deg, #e4d101 0%, #e30183 100%); diff --git a/templates_bak/android-chrome-192x192.png b/templates_bak/android-chrome-192x192.png deleted file mode 100644 index 39fcc99f0cb80af109e77817379d6a9dbdf626db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23547 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