diff --git a/Source/Demo/Common/DemoUtils.cs b/Source/Demo/Common/DemoUtils.cs index 682f325f5..a14b33b3f 100644 --- a/Source/Demo/Common/DemoUtils.cs +++ b/Source/Demo/Common/DemoUtils.cs @@ -74,8 +74,8 @@ public static string GetStylesheet(string src) a:link { text-decoration: none; } a:hover { text-decoration: underline; } .gray { color:gray; } - .example { background-color:#efefef; corner-radius:5px; padding:0.5em; } - .whitehole { background-color:white; corner-radius:10px; padding:15px; } + .example { background-color:#efefef; border-radius:5px; padding:0.5em; } + .whitehole { background-color:white; border-radius:10px; padding:15px; } .caption { font-size: 1.1em } .comment { color: green; margin-bottom: 5px; margin-left: 3px; } .comment2 { color: green; }"; diff --git a/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj b/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj index 7a66b29ab..7128e4a70 100644 --- a/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj +++ b/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj @@ -42,6 +42,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -66,12 +94,14 @@ + + diff --git a/Source/Demo/Common/Resources.cs b/Source/Demo/Common/Resources.cs index 6370c567c..0a8552d25 100644 --- a/Source/Demo/Common/Resources.cs +++ b/Source/Demo/Common/Resources.cs @@ -38,6 +38,27 @@ public static byte[] CustomFont } } + /// + /// Reads one of the bundled Resources\Fonts\*.ttf demo fonts (Liberation/Noto/whimsical - + /// see Resources\Fonts\README.md for the full list and provenance) by file name, e.g. + /// GetFontBytes("LiberationSans-Regular.ttf"). + /// + public static byte[] GetFontBytes(string fileName) + { + var stream = GetManifestResourceStream("Fonts." + fileName); + + var buffer = new byte[16 * 1024]; + using (var ms = new MemoryStream()) + { + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } + } + public static Stream Comment16 { get { return GetManifestResourceStream("comment16.gif"); } diff --git a/Source/Demo/Common/Resources/Fonts/Caveat-Regular.ttf b/Source/Demo/Common/Resources/Fonts/Caveat-Regular.ttf new file mode 100644 index 000000000..f84acf2ce Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/Caveat-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/ComicNeue-Bold.ttf b/Source/Demo/Common/Resources/Fonts/ComicNeue-Bold.ttf new file mode 100644 index 000000000..378eb2004 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/ComicNeue-Bold.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/ComicNeue-BoldItalic.ttf b/Source/Demo/Common/Resources/Fonts/ComicNeue-BoldItalic.ttf new file mode 100644 index 000000000..8c437a353 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/ComicNeue-BoldItalic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/ComicNeue-Italic.ttf b/Source/Demo/Common/Resources/Fonts/ComicNeue-Italic.ttf new file mode 100644 index 000000000..72d9b0b82 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/ComicNeue-Italic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/ComicNeue-Regular.ttf b/Source/Demo/Common/Resources/Fonts/ComicNeue-Regular.ttf new file mode 100644 index 000000000..88e9417f0 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/ComicNeue-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/DancingScript-Regular.ttf b/Source/Demo/Common/Resources/Fonts/DancingScript-Regular.ttf new file mode 100644 index 000000000..9f521e7fb Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/DancingScript-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/IndieFlower-Regular.ttf b/Source/Demo/Common/Resources/Fonts/IndieFlower-Regular.ttf new file mode 100644 index 000000000..547ca5803 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/IndieFlower-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationMono-Bold.ttf b/Source/Demo/Common/Resources/Fonts/LiberationMono-Bold.ttf new file mode 100644 index 000000000..2e46737ac Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationMono-Bold.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationMono-BoldItalic.ttf b/Source/Demo/Common/Resources/Fonts/LiberationMono-BoldItalic.ttf new file mode 100644 index 000000000..d1f46d7cd Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationMono-BoldItalic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationMono-Italic.ttf b/Source/Demo/Common/Resources/Fonts/LiberationMono-Italic.ttf new file mode 100644 index 000000000..954c39436 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationMono-Italic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationMono-Regular.ttf b/Source/Demo/Common/Resources/Fonts/LiberationMono-Regular.ttf new file mode 100644 index 000000000..e774859cb Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationMono-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSans-Bold.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSans-Bold.ttf new file mode 100644 index 000000000..dc5d57f15 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSans-Bold.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSans-BoldItalic.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSans-BoldItalic.ttf new file mode 100644 index 000000000..158488a12 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSans-BoldItalic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSans-Italic.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSans-Italic.ttf new file mode 100644 index 000000000..25970d9d5 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSans-Italic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSans-Regular.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSans-Regular.ttf new file mode 100644 index 000000000..e6339859d Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSans-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSerif-Bold.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSerif-Bold.ttf new file mode 100644 index 000000000..3c7c55b57 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSerif-Bold.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSerif-BoldItalic.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSerif-BoldItalic.ttf new file mode 100644 index 000000000..6b35d9f7c Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSerif-BoldItalic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSerif-Italic.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSerif-Italic.ttf new file mode 100644 index 000000000..54d516481 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSerif-Italic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/LiberationSerif-Regular.ttf b/Source/Demo/Common/Resources/Fonts/LiberationSerif-Regular.ttf new file mode 100644 index 000000000..5e5550c0a Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/LiberationSerif-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSans-Bold.ttf b/Source/Demo/Common/Resources/Fonts/NotoSans-Bold.ttf new file mode 100644 index 000000000..aae7546dc Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSans-Bold.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSans-BoldItalic.ttf b/Source/Demo/Common/Resources/Fonts/NotoSans-BoldItalic.ttf new file mode 100644 index 000000000..6f685b2e5 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSans-BoldItalic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSans-Italic.ttf b/Source/Demo/Common/Resources/Fonts/NotoSans-Italic.ttf new file mode 100644 index 000000000..7f5313334 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSans-Italic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSans-Regular.ttf b/Source/Demo/Common/Resources/Fonts/NotoSans-Regular.ttf new file mode 100644 index 000000000..f27f4ff59 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSans-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSerif-Bold.ttf b/Source/Demo/Common/Resources/Fonts/NotoSerif-Bold.ttf new file mode 100644 index 000000000..352b80735 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSerif-Bold.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSerif-BoldItalic.ttf b/Source/Demo/Common/Resources/Fonts/NotoSerif-BoldItalic.ttf new file mode 100644 index 000000000..f0fbb6fb9 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSerif-BoldItalic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSerif-Italic.ttf b/Source/Demo/Common/Resources/Fonts/NotoSerif-Italic.ttf new file mode 100644 index 000000000..ee42fc10e Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSerif-Italic.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/NotoSerif-Regular.ttf b/Source/Demo/Common/Resources/Fonts/NotoSerif-Regular.ttf new file mode 100644 index 000000000..123a8c576 Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/NotoSerif-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/OFL-Caveat.txt b/Source/Demo/Common/Resources/Fonts/OFL-Caveat.txt new file mode 100644 index 000000000..6e5220c95 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-Caveat.txt @@ -0,0 +1,93 @@ +Copyright 2014 The Caveat Project Authors (https://github.com/googlefonts/caveat) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Source/Demo/Common/Resources/Fonts/OFL-ComicNeue.txt b/Source/Demo/Common/Resources/Fonts/OFL-ComicNeue.txt new file mode 100644 index 000000000..1a1fefa43 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-ComicNeue.txt @@ -0,0 +1,93 @@ +Copyright 2014 The Comic Neue Project Authors (https://github.com/crozynski/comicneue) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Source/Demo/Common/Resources/Fonts/OFL-DancingScript.txt b/Source/Demo/Common/Resources/Fonts/OFL-DancingScript.txt new file mode 100644 index 000000000..07ff8df96 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-DancingScript.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Dancing Script Project Authors (https://github.com/googlefonts/DancingScript), with Reserved Font Name 'Dancing Script'. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Source/Demo/Common/Resources/Fonts/OFL-IndieFlower.txt b/Source/Demo/Common/Resources/Fonts/OFL-IndieFlower.txt new file mode 100644 index 000000000..5d36552c7 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-IndieFlower.txt @@ -0,0 +1,93 @@ +Copyright 2010 The Indie Flower Authors (kimberlygeswein.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Source/Demo/Common/Resources/Fonts/OFL-LiberationFonts.txt b/Source/Demo/Common/Resources/Fonts/OFL-LiberationFonts.txt new file mode 100644 index 000000000..aba73e8a4 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-LiberationFonts.txt @@ -0,0 +1,102 @@ +Digitized data copyright (c) 2010 Google Corporation + with Reserved Font Arimo, Tinos and Cousine. +Copyright (c) 2012 Red Hat, Inc. + with Reserved Font Name Liberation. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE The goals of the Open Font License (OFL) are to stimulate +worldwide development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to provide +a free and open framework in which fonts may be shared and improved in +partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. +The fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + + + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. +This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components +as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting ? in part or in whole ? +any of the components of the Original Version, by changing formats or +by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer +or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components,in + Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the + corresponding Copyright Holder. This restriction only applies to the + primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + +5) The Font Software, modified or unmodified, in part or in whole, must + be distributed entirely under this license, and must not be distributed + under any other license. The requirement for fonts to remain under + this license does not apply to any document created using the Font + Software. + + + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + + + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + diff --git a/Source/Demo/Common/Resources/Fonts/OFL-NotoFonts.txt b/Source/Demo/Common/Resources/Fonts/OFL-NotoFonts.txt new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-NotoFonts.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Source/Demo/Common/Resources/Fonts/OFL-Pacifico.txt b/Source/Demo/Common/Resources/Fonts/OFL-Pacifico.txt new file mode 100644 index 000000000..33864cc8d --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/OFL-Pacifico.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Pacifico Project Authors (https://github.com/googlefonts/Pacifico) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Source/Demo/Common/Resources/Fonts/Pacifico-Regular.ttf b/Source/Demo/Common/Resources/Fonts/Pacifico-Regular.ttf new file mode 100644 index 000000000..27765d02a Binary files /dev/null and b/Source/Demo/Common/Resources/Fonts/Pacifico-Regular.ttf differ diff --git a/Source/Demo/Common/Resources/Fonts/README.md b/Source/Demo/Common/Resources/Fonts/README.md new file mode 100644 index 000000000..bfbd42cd3 --- /dev/null +++ b/Source/Demo/Common/Resources/Fonts/README.md @@ -0,0 +1,18 @@ +# Bundled demo fonts + +All fonts here are open source (SIL Open Font License 1.1 - see the accompanying `OFL-*.txt` files, +one per family/foundry since copyright holders differ) and are used to exercise the `@font-face` demo +sample (`TestSamples/20.Fonts decorations.htm`) and the integration test suite, without depending on +whatever fonts happen to be installed on the machine running them. + +| Family | Faces | Source | +| --- | --- | --- | +| Liberation Sans/Serif/Mono | Regular, Bold, Italic, Bold Italic | https://github.com/liberationfonts/liberation-fonts, release 2.1.5 (`liberation-fonts-ttf-2.1.5.tar.gz`) | +| Noto Sans/Serif | Regular, Bold, Italic, Bold Italic | https://github.com/notofonts/notofonts.github.io, `fonts//hinted/ttf/` | +| Pacifico | Regular | https://github.com/google/fonts, `ofl/pacifico/` | +| Dancing Script | Regular (variable font, default instance) | https://github.com/google/fonts, `ofl/dancingscript/` | +| Caveat | Regular (variable font, default instance) | https://github.com/google/fonts, `ofl/caveat/` | +| Indie Flower | Regular | https://github.com/google/fonts, `ofl/indieflower/` | +| Comic Neue | Regular, Bold, Italic, Bold Italic | https://github.com/google/fonts, `ofl/comicneue/` | + +Plain TTF only (no WOFF/WOFF2) - this project's `@font-face` support doesn't decompress WOFF/WOFF2 in v1. diff --git a/Source/Demo/Common/Samples/00.Intro.htm b/Source/Demo/Common/Samples/00.Intro.htm index 894052814..bc14d7616 100644 --- a/Source/Demo/Common/Samples/00.Intro.htm +++ b/Source/Demo/Common/Samples/00.Intro.htm @@ -4,7 +4,7 @@ Intro - +

HTML Renderer Project - $$Platform$$
diff --git a/Source/Demo/Common/Samples/07.Additional features.htm b/Source/Demo/Common/Samples/07.Additional features.htm index fcc209e82..db4854fe2 100644 --- a/Source/Demo/Common/Samples/07.Additional features.htm +++ b/Source/Demo/Common/Samples/07.Additional features.htm @@ -6,20 +6,18 @@ - - -

Case 1

-

- Warning:P tags must be closed. In fact all tags with the - end tag marked as optional, must be closed. It may be fixed by next release. -

-
-

Text align justify with background colors

-

- Lorem ipsum dolor sit amet, consectetur adipisicing - elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua tempor incididunt ut labore et dolore magna aliqua incididunt ut labore et dolore magna aliqua. -

-
-

Right align adjusts

- - - - - - - -
978
32
-
-

Transparent text

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer sagittis. Fusce elementum commodo felis. Vivamus lacinia eleifend libero. Donec - lacus. Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla - elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. -

- -

- - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer sagittis. Fusce elementum commodo felis. Vivamus lacinia eleifend libero. Donec - lacus. - -
- - Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla - elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. - -

-

- - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer_sagittis_Fusce_elementum_commodo_felis_Vivamus_lacinia_eleifend_libero_Donec - lacus. - -
- - Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla - elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. - -

-
-

RTL text

-

- בדיקה של טקסט ימין לשמאל -

-

- בדיקה של טקסט ימין לשמאל normal text -

-

- בדיקה של טקסט ימין לשמאל normal text -

- - \ No newline at end of file + + + + Text + + + + +

Case 1

+

+ Warning:P tags must be closed. In fact all tags with the + end tag marked as optional, must be closed. It may be fixed by next release. +

+
+

Text align justify with background colors

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua tempor incididunt ut labore et dolore magna aliqua incididunt ut labore et dolore magna aliqua. +

+
+

Right align adjusts

+ + + + + + + +
978
32
+
+

Transparent text

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer sagittis. Fusce elementum commodo felis. Vivamus lacinia eleifend libero. Donec + lacus. Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla + elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. +

+ +

+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer sagittis. Fusce elementum commodo felis. Vivamus lacinia eleifend libero. Donec + lacus. + +
+ + Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla + elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. + +

+

+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer_sagittis_Fusce_elementum_commodo_felis_Vivamus_lacinia_eleifend_libero_Donec + lacus. + +
+ + Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla + elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. + +

+
+

RTL text

+

+ בדיקה של טקסט ימין לשמאל +

+

+ בדיקה של טקסט ימין לשמאל normal text +

+

+ בדיקה של טקסט ימין לשמאל normal text +

+ + diff --git a/Source/Demo/Common/TestSamples/13.Tables.htm b/Source/Demo/Common/TestSamples/13.Tables.htm index fc896eda6..a58ac927c 100644 --- a/Source/Demo/Common/TestSamples/13.Tables.htm +++ b/Source/Demo/Common/TestSamples/13.Tables.htm @@ -3,7 +3,7 @@ Tables -

All fonts with different decorations

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

- - +

@font-face fonts with different decorations

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

@font-face weight and style matching (proves nearest-face selection, not just decoration)

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

- \ No newline at end of file + diff --git a/Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm b/Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm new file mode 100644 index 000000000..501f1c3d2 --- /dev/null +++ b/Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm @@ -0,0 +1,99 @@ + + + + + + +
+

prefers-color-scheme

+ This card follows the platform colour scheme. Reported scheme: . +
GDI+ and WPF read the Windows app theme; PdfSharp always reports light.
+
+ +
+

@media screen

+ Visible because the adapter's media type is screen. +
+ + + +
+

Unsupported media feature

+ This card stays neutral. A media feature the matcher cannot evaluate drops its whole block + rather than applying the rules inside it, so the red styling in + @media all and (scan: interlace) must not win. +
+ +
+

Viewport width

+ +
+ + + diff --git a/Source/Demo/Common/TestSamples/36.Float wrap.htm b/Source/Demo/Common/TestSamples/36.Float wrap.htm new file mode 100644 index 000000000..60ac3c7c0 --- /dev/null +++ b/Source/Demo/Common/TestSamples/36.Float wrap.htm @@ -0,0 +1,12 @@ + + + + Float wrap + + +
+
+

This paragraph wraps around a blue box floated left and a red box floated right, so its lines should narrow to avoid both floats until the text runs past their bottom edges and returns to the full available width for the remaining lines of text in this paragraph.

+
This div has clear: both and should start below both floated boxes, not overlap them.
+ + diff --git a/Source/Demo/WPF/DemoWindow.xaml.cs b/Source/Demo/WPF/DemoWindow.xaml.cs index 168ac351b..be75f0372 100644 --- a/Source/Demo/WPF/DemoWindow.xaml.cs +++ b/Source/Demo/WPF/DemoWindow.xaml.cs @@ -116,13 +116,13 @@ private void OnGenerateImage_Click(object sender, RoutedEventArgs e) /// /// Create PDF using PdfSharp project, save to file and open that file. /// - private void OnGeneratePdf_Click(object sender, RoutedEventArgs e) + private async void OnGeneratePdf_Click(object sender, RoutedEventArgs e) { var config = new PdfGenerateConfig(); config.PageSize = PageSize.A4; config.SetMargins(20); - var doc = PdfGenerator.GeneratePdf(_mainControl.GetHtml(), config, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoadPdfSharp); + var doc = await PdfGenerator.GeneratePdf(_mainControl.GetHtml(), config, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoadPdfSharp); var tmpFile = Path.GetTempFileName(); var pdfFile = Path.ChangeExtension(tmpFile, ".pdf"); diff --git a/Source/Demo/WPF/GenerateImageWindow.xaml.cs b/Source/Demo/WPF/GenerateImageWindow.xaml.cs index 0520b23d4..d41ef87ad 100644 --- a/Source/Demo/WPF/GenerateImageWindow.xaml.cs +++ b/Source/Demo/WPF/GenerateImageWindow.xaml.cs @@ -11,6 +11,7 @@ // "The Art of War" using System.IO; +using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; using TheArtOfDev.HtmlRenderer.Demo.Common; @@ -33,7 +34,7 @@ public GenerateImageWindow(string html) InitializeComponent(); - Loaded += (sender, args) => GenerateImage(); + Loaded += async (sender, args) => await GenerateImage(); } private void OnSaveToFile_click(object sender, RoutedEventArgs e) @@ -53,16 +54,16 @@ private void OnSaveToFile_click(object sender, RoutedEventArgs e) } } - private void OnGenerateImage_Click(object sender, RoutedEventArgs e) + private async void OnGenerateImage_Click(object sender, RoutedEventArgs e) { - GenerateImage(); + await GenerateImage(); } - private void GenerateImage() + private async Task GenerateImage() { if (_imageBoxBorder.RenderSize.Width > 0 && _imageBoxBorder.RenderSize.Height > 0) { - _generatedImage = HtmlRender.RenderToImage(_html, _imageBoxBorder.RenderSize, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoad); + _generatedImage = await HtmlRender.RenderToImageAsync(_html, _imageBoxBorder.RenderSize, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoad); _imageBox.Source = _generatedImage; } } diff --git a/Source/Demo/WPF/MainControl.xaml.cs b/Source/Demo/WPF/MainControl.xaml.cs index bce20029e..6aebecc43 100644 --- a/Source/Demo/WPF/MainControl.xaml.cs +++ b/Source/Demo/WPF/MainControl.xaml.cs @@ -383,10 +383,23 @@ private void OnLinkClicked(object sender, RoutedEventArgs /// Set html syntax color text on the RTF html editor. /// + /// + /// Both branches must go through , not just the + /// color one: _htmlEditor (an xctk:RichTextBox) parses its Text setter + /// as RTF, and the old !color branch (text.Replace("\n", "\\par ")) produced neither + /// a valid RTF header nor escaped literal {/} characters - harmless for markup-only + /// samples, but any sample whose <style> block is brace-dense (e.g. a page with many + /// @font-face { ... } rules) had its RTF group structure corrupted right at those braces, + /// silently truncating/mangling the <style> content that + /// later reads back out - which is what fed a mangled stylesheet into PDF export. Using the same + /// uniform (uncolored) color for every element still produces valid, correctly-escaped RTF. + /// private void SetColoredText(string text, bool color) { var selectionStart = _htmlEditor.CaretPosition; - _htmlEditor.Text = color ? HtmlSyntaxHighlighter.Process(text) : text.Replace("\n", "\\par "); + _htmlEditor.Text = color + ? HtmlSyntaxHighlighter.Process(text) + : HtmlSyntaxHighlighter.Process(text, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black); _htmlEditor.CaretPosition = selectionStart; } diff --git a/Source/Demo/WinForms/DemoForm.cs b/Source/Demo/WinForms/DemoForm.cs index b408d3176..b68da61f6 100644 --- a/Source/Demo/WinForms/DemoForm.cs +++ b/Source/Demo/WinForms/DemoForm.cs @@ -138,13 +138,13 @@ private void OnGenerateImage_Click(object sender, EventArgs e) /// /// Create PDF using PdfSharp project, save to file and open that file. /// - private void OnGeneratePdf_Click(object sender, EventArgs e) + private async void OnGeneratePdf_Click(object sender, EventArgs e) { PdfGenerateConfig config = new PdfGenerateConfig(); config.PageSize = PageSize.A4; config.SetMargins(20); - var doc = PdfGenerator.GeneratePdf(_mainControl.GetHtml(), config, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoadPdfSharp); + var doc = await PdfGenerator.GeneratePdf(_mainControl.GetHtml(), config, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoadPdfSharp); var tmpFile = Path.GetTempFileName(); var pdfFile = Path.ChangeExtension(tmpFile, ".pdf"); // Preserves the full path diff --git a/Source/Demo/WinForms/GenerateImageForm.cs b/Source/Demo/WinForms/GenerateImageForm.cs index f4e8a4577..dcdb915dc 100644 --- a/Source/Demo/WinForms/GenerateImageForm.cs +++ b/Source/Demo/WinForms/GenerateImageForm.cs @@ -16,6 +16,7 @@ using System.Drawing.Drawing2D; using System.Drawing.Text; using System.Reflection; +using System.Threading.Tasks; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; @@ -71,31 +72,31 @@ private void OnSaveToFile_Click(object sender, EventArgs e) } } - private void OnUseGdiPlus_Click(object sender, EventArgs e) + private async void OnUseGdiPlus_Click(object sender, EventArgs e) { _useGdiPlusTSB.Checked = !_useGdiPlusTSB.Checked; _textRenderingHintTSCB.Visible = _useGdiPlusTSB.Checked; _backgroundColorTSB.Visible = !_useGdiPlusTSB.Checked; _toolStripLabel.Text = _useGdiPlusTSB.Checked ? "Text Rendering Hint:" : "Background:"; - GenerateImage(); + await GenerateImage(); } - private void OnBackgroundColor_SelectedIndexChanged(object sender, EventArgs e) + private async void OnBackgroundColor_SelectedIndexChanged(object sender, EventArgs e) { - GenerateImage(); + await GenerateImage(); } - private void _textRenderingHintTSCB_SelectedIndexChanged(object sender, EventArgs e) + private async void _textRenderingHintTSCB_SelectedIndexChanged(object sender, EventArgs e) { - GenerateImage(); + await GenerateImage(); } - private void OnGenerateImage_Click(object sender, EventArgs e) + private async void OnGenerateImage_Click(object sender, EventArgs e) { - GenerateImage(); + await GenerateImage(); } - private void GenerateImage() + private async Task GenerateImage() { if (_backgroundColorTSB.SelectedItem != null && _textRenderingHintTSCB.SelectedItem != null) { @@ -105,17 +106,11 @@ private void GenerateImage() Image img; if (_useGdiPlusTSB.Checked) { - img = HtmlRender.RenderToImageGdiPlus(_html, _pictureBox.ClientSize, textRenderingHint, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoad); + img = await HtmlRender.RenderToImageGdiPlusAsync(_html, _pictureBox.ClientSize, textRenderingHint, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoad); } else { - EventHandler stylesheetLoad = DemoUtils.OnStylesheetLoad; - EventHandler imageLoad = HtmlRenderingHelper.OnImageLoad; - var objects = new object[] { _html, _pictureBox.ClientSize, backgroundColor, null, stylesheetLoad, imageLoad }; - - var types = new[] { typeof(String), typeof(Size), typeof(Color), typeof(CssData), typeof(EventHandler), typeof(EventHandler) }; - var m = typeof(HtmlRender).GetMethod("RenderToImage", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, types, null); - img = (Image)m.Invoke(null, objects); + img = await HtmlRender.RenderToImageAsync(_html, _pictureBox.ClientSize, backgroundColor, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoad); } _pictureBox.Image = img; } diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs new file mode 100644 index 000000000..b92bf73d3 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs @@ -0,0 +1,46 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// A multi-stop linear gradient "brush" for the PdfSharp backend. Unlike , + /// this does not wrap a real PdfSharp.Drawing.XBrush - XLinearGradientBrush in the + /// PDFsharp 1.50 package this project depends on only supports 2 colors, no stop list. Instead this + /// just carries the gradient line and stops; 's DrawPath/ + /// DrawRectangle special-case this type and paint it as a series of adjacent 2-color + /// XLinearGradientBrush bands, one per consecutive stop pair - each band is a real 2-color + /// linear gradient by definition, so the composite is an exact piecewise-linear rendering, not an + /// approximation. + /// + internal sealed class GradientBrushAdapter : RBrush + { + public GradientBrushAdapter(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops) + { + P1 = p1; + P2 = p2; + Stops = stops; + } + + public RPoint P1 { get; } + + public RPoint P2 { get; } + + public (RColor Color, double Position)[] Stops { get; } + + public override void Dispose() + { } + } +} diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs index c6d406c58..76ede5ea1 100644 --- a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs +++ b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs @@ -155,6 +155,14 @@ public override void DrawRectangle(RPen pen, double x, double y, double width, d public override void DrawRectangle(RBrush brush, double x, double y, double width, double height) { + if (brush is GradientBrushAdapter gradient) + { + var rectPath = new XGraphicsPath(); + rectPath.AddRectangle(x, y, width, height); + FillGradient(gradient, rectPath); + return; + } + var xBrush = ((BrushAdapter)brush).Brush; var xTextureBrush = xBrush as XTextureBrush; if (xTextureBrush != null) @@ -188,6 +196,12 @@ public override void DrawPath(RPen pen, RGraphicsPath path) public override void DrawPath(RBrush brush, RGraphicsPath path) { + if (brush is GradientBrushAdapter gradient) + { + FillGradient(gradient, ((GraphicsPathAdapter)path).GraphicsPath); + return; + } + _g.DrawPath((XBrush)((BrushAdapter)brush).Brush, ((GraphicsPathAdapter)path).GraphicsPath); } @@ -199,6 +213,64 @@ public override void DrawPolygon(RBrush brush, RPoint[] points) } } + /// + /// Paints a multi-stop linear gradient by clipping to and drawing + /// one real 2-color band per consecutive stop pair, each + /// spanning the full perpendicular extent needed to cover the target - see + /// for why this backend needs banding instead of a single brush. + /// + private void FillGradient(GradientBrushAdapter gradient, XGraphicsPath targetPath) + { + var stops = gradient.Stops; + if (stops.Length == 0) + return; + + _g.Save(); + _g.IntersectClip(targetPath); + + double dx = gradient.P2.X - gradient.P1.X; + double dy = gradient.P2.Y - gradient.P1.Y; + double len = Math.Sqrt(dx * dx + dy * dy); + + if (stops.Length == 1 || len < 1e-6) + { + // Degenerate gradient line (single stop, or a zero-size box) - just flat-fill with the + // last color, matching what a real linear gradient converges to in that case. + var flatBrush = new XSolidBrush(Utils.Convert(stops[stops.Length - 1].Color)); + _g.DrawRectangle(flatBrush, -1e5, -1e5, 2e5, 2e5); + _g.Restore(); + _g.DrawRectangle(XBrushes.White, 0, 0, 0.1, 0.1); + return; + } + + double ux = dx / len, uy = dy / len; + double perpX = -uy, perpY = ux; + double perpHalf = Math.Max(len, 1.0) * 4.0; + + for (int i = 0; i < stops.Length - 1; i++) + { + double t1 = stops[i].Position, t2 = stops[i + 1].Position; + var bp1 = new XPoint(gradient.P1.X + ux * len * t1, gradient.P1.Y + uy * len * t1); + var bp2 = new XPoint(gradient.P1.X + ux * len * t2, gradient.P1.Y + uy * len * t2); + + var band = new[] + { + new XPoint(bp1.X - perpX * perpHalf, bp1.Y - perpY * perpHalf), + new XPoint(bp1.X + perpX * perpHalf, bp1.Y + perpY * perpHalf), + new XPoint(bp2.X + perpX * perpHalf, bp2.Y + perpY * perpHalf), + new XPoint(bp2.X - perpX * perpHalf, bp2.Y - perpY * perpHalf), + }; + + var bandBrush = new XLinearGradientBrush(bp1, bp2, Utils.Convert(stops[i].Color), Utils.Convert(stops[i + 1].Color)); + _g.DrawPolygon(bandBrush, band, XFillMode.Winding); + } + + _g.Restore(); + + // handle bug in PdfSharp that keeps the brush color for next string draw + _g.DrawRectangle(XBrushes.White, 0, 0, 0.1, 0.1); + } + #endregion } } \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs index 48290543c..9e057d8e9 100644 --- a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs +++ b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs @@ -51,11 +51,11 @@ public override void LineTo(double x, double y) _lastPoint = new RPoint(x, y); } - public override void ArcTo(double x, double y, double size, Corner corner) + public override void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner) { - float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? size : 0)); - float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? size : 0)); - _graphicsPath.AddArc(left, top, (float)size * 2, (float)size * 2, GetStartAngle(corner), 90); + float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? radiusX : 0)); + float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? radiusY : 0)); + _graphicsPath.AddArc(left, top, (float)radiusX * 2, (float)radiusY * 2, GetStartAngle(corner), 90); _lastPoint = new RPoint(x, y); } diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs index 4e74601bc..001392f6c 100644 --- a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs +++ b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs @@ -13,9 +13,13 @@ using PdfSharp.Drawing; using PdfSharp.Pdf; using System; +using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Network; using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution; using TheArtOfDev.HtmlRenderer.PdfSharp.Utilities; @@ -59,6 +63,19 @@ private PdfSharpAdapter() } } + /// + /// Also recognizes a family registered via - those live entirely in + /// , invisible to the shared FontsHandler the base implementation + /// checks (see 's own doc comment for why). Without this override, + /// would never see an @font-face-only + /// family as "existing" and would silently substitute + /// for it before layout ever gets a chance to resolve the real face. + /// + public override bool IsFontExists(string font) + { + return base.IsFontExists(font) || _fontResolver.HasFamily(font); + } + /// /// Singleton instance of global adapter. /// @@ -75,6 +92,22 @@ internal FontResolver FontResolver get { return _fontResolver; } } + /// + /// Paged output, so @media print applies and @media screen does not. + /// + public override string DefaultMediaType + { + get { return "print"; } + } + + /// + /// A PDF has no system theme to follow, so prefers-color-scheme always reports light. + /// + public override RColorScheme SystemColorScheme + { + get { return RColorScheme.Light; } + } + protected override RColor GetColorInt(string colorName) { try @@ -123,18 +156,9 @@ protected override RBrush CreateSolidBrush(RColor color) return new BrushAdapter(solidBrush); } - protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + protected override RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops) { - XLinearGradientMode mode; - if (angle < 45) - mode = XLinearGradientMode.ForwardDiagonal; - else if (angle < 90) - mode = XLinearGradientMode.Vertical; - else if (angle < 135) - mode = XLinearGradientMode.BackwardDiagonal; - else - mode = XLinearGradientMode.Horizontal; - return new BrushAdapter(new XLinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), mode)); + return new GradientBrushAdapter(p1, p2, stops); } protected override RImage ConvertImageInt(object image) @@ -160,5 +184,80 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty var xFont = new XFont(((FontFamilyAdapter)family).FontFamily.Name, size, fontStyle, new XPdfFontOptions(PdfFontEncoding.Unicode)); return new FontAdapter(xFont); } + + /// + /// Never called: this backend overrides directly (see its own doc + /// comment) rather than routing through the base's LoadFontFaceFontInt-based path. + /// + protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath) + { + throw new NotSupportedException("PdfSharpAdapter overrides AddFontFace directly and never calls LoadFontFaceFontInt."); + } + + /// + /// Bypasses the base /shared FontsHandler registry + /// entirely: PDFsharp's needs raw font bytes for PDF embedding, which + /// + /// already stores and matches against directly - no platform font-family handle is involved. + /// + public override async Task AddFontFace(string familyName, RUri uri, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + var networkResponse = await GetResourceStream(uri).ConfigureAwait(false); + if (networkResponse?.ResourceStream == null) + { + return false; + } + + try + { + using (networkResponse.ResourceStream) + { + _fontResolver.AddFont(networkResponse.ResourceStream, familyName, weight, isItalic, stretch, ranges); + } + return true; + } + catch + { + return false; + } + } + + /// Bypasses the shared registry, for the same reason as - see its doc comment. + public override bool AddFontFaceFromLocalFamily(string familyName, string localFamilyName, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + return _fontResolver.AddLocalFontFamily(familyName, localFamilyName, weight, isItalic, stretch, ranges); + } + + /// + /// Bypasses the shared registry, for the same reason as - see its doc + /// comment. PDFsharp's own + /// construction only ever resolves by (family name, bold, italic) internally - there is no public + /// PDFsharp API surface to hand it an already-chosen numeric weight/stretch/face directly - so a + /// numeric can only steer face selection as far as PDFsharp's own + /// bold/not-bold threshold allows; , which that 2-bool resolution can't + /// express at all, is still honored precisely by consulting the richer + /// overload up front purely to + /// preserve the "codepoint-scoped miss returns null" contract. + /// + public override RFont GetFont(string family, double size, RFontStyle style, int weight, int stretch, int? codepoint) + { + var isItalic = (style & RFontStyle.Italic) != 0; + + if (codepoint.HasValue) + { + var info = _fontResolver.ResolveTypeface(family, weight, isItalic, stretch, codepoint); + if (info == null) + { + return null; + } + } + + var isBold = weight >= 600; + var residualStyle = (style & (RFontStyle.Underline | RFontStyle.Strikeout)) + | (isBold ? RFontStyle.Bold : RFontStyle.Regular) + | (isItalic ? RFontStyle.Italic : RFontStyle.Regular); + + return CreateFontInt(family, size, residualStyle); + } } } \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/DirectoryFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/DirectoryFontDiscovery.cs deleted file mode 100644 index c36b2292a..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/DirectoryFontDiscovery.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal static class DirectoryFontDiscovery - { - public static List DiscoverFontFilesFromDirectories(List customFontDirectories) - { - var fontDirectories = new List(); - - // Add platform-specific directories - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fontDirectories.Add(Environment.GetFolderPath(Environment.SpecialFolder.Fonts)); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - fontDirectories.AddRange(new[] - { - "/usr/share/fonts", - "/usr/local/share/fonts", - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".fonts"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/fonts") - }); - } - - // Add custom directories - fontDirectories.AddRange(customFontDirectories); - - return DiscoverFontFilesInDirectories(fontDirectories); - } - - public static List DiscoverFontFilesInDirectories(List directories) - { - var fontInfos = new List(); - - foreach (var directory in directories) - { - if (!Directory.Exists(directory)) - { - continue; - } - - try - { - fontInfos.AddRange(FontDiscoveryService.SupportedFontExtensions.SelectMany(e => Directory.GetFiles(directory, $"*{e}", SearchOption.AllDirectories)) - .Select(f => new FontInfo - { - Name = Path.GetFileNameWithoutExtension(f), - FilePath = f - })); - } - catch - { - // Silently continue on directory access errors - } - } - - return fontInfos; - } - } -} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/FontDiscoveryService.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/FontDiscoveryService.cs deleted file mode 100644 index 81b793cc8..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/FontDiscoveryService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal class FontDiscoveryService - { - private readonly List _customFontDirectories = new List(); - - public static string[] SupportedFontExtensions { get; } = new[] { ".ttf", ".otf" }; - - public List DiscoverFontInfos() - { - var fontInfos = new List(); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fontInfos = WindowsFontDiscovery.DiscoverFontInfos(); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - fontInfos = LinuxFontDiscovery.DiscoverFontFiles(); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - fontInfos = MacOsFontDiscovery.DiscoverFontFiles(); - } - - // Always use directory scan as fallback - fontInfos.AddRange(DirectoryFontDiscovery.DiscoverFontFilesFromDirectories(_customFontDirectories)); - - return fontInfos; - } - - public void RegisterCustomFontDirectory(string fontDirectory) - { - if (_customFontDirectories.Contains(fontDirectory)) - { - return; - } - - _customFontDirectories.Add(fontDirectory); - } - } -} - diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/LinuxFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/LinuxFontDiscovery.cs deleted file mode 100644 index 9bfee68eb..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/LinuxFontDiscovery.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal static class LinuxFontDiscovery - { - public static List DiscoverFontFiles() - { - var fontInfos = new List(); - - if (!IsFontConfigAvailable()) - { - return fontInfos; - } - - try - { - var startInfo = new ProcessStartInfo - { - FileName = "fc-list", - Arguments = ":", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (var process = Process.Start(startInfo)) - { - if (process == null) - { - return fontInfos; - } - - var output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - if (process.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) - { - return fontInfos; - } - - var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - var fontFamilies = DiscoverFontFile(line); - - if (fontFamilies.Count > 0) - { - fontInfos.AddRange(fontFamilies); - } - } - } - } - catch - { - // Silently continue on errors - } - - return fontInfos; - } - - private static bool IsFontConfigAvailable() - { - try - { - var startInfo = new ProcessStartInfo - { - FileName = "which", - Arguments = "fc-list", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (var process = Process.Start(startInfo)) - { - if (process == null) - { - return false; - } - - process.WaitForExit(); - return process.ExitCode == 0; - } - } - catch - { - return false; - } - } - - private static List DiscoverFontFile(string line) - { - var fonts = new List(); - - // Parse fc-list output: "path: family1,family2:style=style" - var parts = line.Split(new[] { ':' }, 2, StringSplitOptions.None); - if (parts.Length < 2) - { - return fonts; - } - - var fontFilePath = parts[0].Trim(); - var families = parts[1].Trim().Split(','); - - if (!File.Exists(fontFilePath) || - !FontDiscoveryService.SupportedFontExtensions.Any(e => fontFilePath.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) - { - return fonts; - } - - foreach (var family in families) - { - var familyName = family.Trim(); - if (!string.IsNullOrEmpty(familyName) && File.Exists(fontFilePath)) - { - fonts.Add(new FontInfo { Name = familyName, FilePath = fontFilePath }); - } - } - return fonts; - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/MacOsFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/MacOsFontDiscovery.cs deleted file mode 100644 index aae467b64..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/MacOsFontDiscovery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal static class MacOsFontDiscovery - { - public static List DiscoverFontFiles() - { - var fontDirectories = new List - { - "/System/Library/Fonts", - "/Library/Fonts", - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Fonts") - }; - - return DirectoryFontDiscovery.DiscoverFontFilesInDirectories(fontDirectories); - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/WindowsFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/WindowsFontDiscovery.cs deleted file mode 100644 index 7885b6a54..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/WindowsFontDiscovery.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Microsoft.Win32; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Platform switching in calling class")] - internal static class WindowsFontDiscovery - { - public static List DiscoverFontInfos() - { - var fontInfos = new List(); - try - { - using (var machineRegistryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", false)) - using (var userRegistryKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", false)) - { - foreach (var registryKey in new[] { machineRegistryKey, userRegistryKey }) - { - if (registryKey == null) - { - continue; - } - - foreach (var fontName in registryKey.GetValueNames()) - { - var fontFilePath = registryKey.GetValue(fontName)?.ToString(); - - var fontFile = DiscoverFontFile(fontFilePath); - - if (fontFile != null) - { - fontInfos.Add(new FontInfo - { - Name = fontName, - FilePath = fontFile - }); - } - } - } - } - } - catch - { - // Silently continue on errors - } - - return fontInfos; - } - - private static string DiscoverFontFile(string fontFilePath) - { - if (fontFilePath == null || - !FontDiscoveryService.SupportedFontExtensions.Any(e => fontFilePath.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) - { - return null; - } - - // If the path is not absolute, the font is in the Windows Fonts folder - if (!Path.IsPathRooted(fontFilePath)) - { - fontFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), fontFilePath); - } - - if (!File.Exists(fontFilePath)) - { - return null; - } - - return fontFilePath; - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontAttributes.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontAttributes.cs deleted file mode 100644 index c1c2ccfb4..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontAttributes.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public enum FontWeight - { - Thin = 100, - ExtraLight = 200, - Light = 300, - Normal = 400, - Medium = 500, - SemiBold = 600, - Bold = 700, - ExtraBold = 800, - Black = 900, - } - - public enum FontWidth - { - UltraCondensed = 50, - ExtraCondensed = 62, - Condensed = 75, - SemiCondensed = 87, - Medium = 100, - SemiExpanded = 112, - Expanded = 125, - ExtraExpanded = 150, - UltraExpanded = 200, - } - - public enum FontStyle - { - Normal, - Italic, - Oblique, - } - - public class FontAttributes - { - public FontWeight Weight { get; set; } = FontWeight.Normal; - public FontStyle Style { get; set; } = FontStyle.Normal; - public FontWidth Width { get; set; } = FontWidth.Medium; - } -} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs new file mode 100644 index 000000000..f8af02288 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution +{ + /// + /// One registered face of a : its CSS Fonts Level 4 matching axes + /// (numeric weight, italic, stretch), the codepoint s it is restricted to + /// (from an @font-face unicode-range descriptor) or null when it has none - a face with + /// no explicit ranges is treated as covering everything (matching the same, deliberately simpler, + /// scope as the shared Core.Handlers.FontsHandler face registry - see its own doc comments for + /// why cmap-based coverage extraction isn't ported here: this codebase resolves one font per box, not + /// per glyph, so there is no per-glyph precision to exploit) - and the sniffed description + /// hands back once this face is + /// chosen; doubles as this face's storage + /// key into . + /// + /// Ported from PeachPDF's Fonts\FontFamilyModel.cs. + internal sealed class FontFaceEntry + { + public FontFaceEntry(int weight, bool italic, int stretch, IReadOnlyList explicitRanges, TtfFontDescription description) + { + Weight = weight; + Italic = italic; + Stretch = stretch; + ExplicitRanges = explicitRanges; + Description = description; + } + + public int Weight { get; } + public bool Italic { get; } + public int Stretch { get; } + public IReadOnlyList ExplicitRanges { get; } + public TtfFontDescription Description { get; } + } + + /// One CSS font-family: its registered faces. Ported from PeachPDF's Fonts\FontFamilyModel.cs. + internal sealed class FontFamilyModel + { + public string Name { get; set; } + + /// + /// Every registered face for this family - a list, not a dictionary keyed by (weight,italic, + /// stretch), because two faces can legitimately share the same matching axes yet differ by + /// unicode-range (e.g. a Latin subset and a Cyrillic subset of one webfont family, both + /// regular weight). + /// + public List Faces { get; } = new List(); + } +} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontInfo.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontInfo.cs deleted file mode 100644 index cceb0ae97..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public class FontInfo - { - public string Name { get; set; } - public string FilePath { get; set; } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontMetadata.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontMetadata.cs deleted file mode 100644 index d452b9f0e..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontMetadata.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public class FontMetadata - { - public string FilePath { get; set; } - - public string Family { get; set; } - public string Subfamily { get; set; } - - public string PreferredFamily { get; set; } - public string PreferredSubfamily { get; set; } - - public string FullName { get; set; } - public string PostScriptName { get; set; } - - public FontAttributes Attributes { get; set; } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontNameResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontNameResolver.cs deleted file mode 100644 index 7faced10b..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontNameResolver.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public static class FontNameResolver - { - public static List ResolveFontNames(string fontName, FontAttributes fontAttributes, FontResolveStrategy resolveStrategy) - { - var fontNames = new List(); - - if (resolveStrategy == FontResolveStrategy.Strict) - { - fontNames.Add(StylizeFontNameStrict(fontName, fontAttributes)); - } - else - { - fontNames.AddRange(StylizeFontNameClosest(fontName, fontAttributes)); - } - - return fontNames; - } - - public static string StylizeFontNameFamily(FontMetadata metadata) - { - if (metadata.Family is null || metadata.Attributes is null) - { - return string.Empty; - } - - return BuildStylizedFontName(metadata.Family, MatchWeightStrict(metadata.Attributes.Weight), MatchStyleStrict(metadata.Attributes.Style)); - } - - public static string StylizeFontNamePreferredFamily(FontMetadata metadata) - { - if (metadata.PreferredFamily is null || metadata.PreferredSubfamily is null) - { - return string.Empty; - } - - return string.IsNullOrEmpty(metadata.PreferredSubfamily) - ? metadata.PreferredFamily - : $"{metadata.PreferredFamily} {metadata.PreferredSubfamily}"; - } - - public static string StylizeFontNameStrict(string fontName, FontAttributes fontAttributes) - { - return BuildStylizedFontName(fontName, MatchWeightStrict(fontAttributes.Weight), MatchStyleStrict(fontAttributes.Style)); - } - - private static string[] StylizeFontNameClosest(string fontName, FontAttributes fontAttributes) - { - var fontNames = new List - { - StylizeFontNameStrict(fontName, fontAttributes), - BuildStylizedFontName(fontName, MatchWeightStrict(fontAttributes.Weight), MatchStyleObliqueAndItalic(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightMiddle(fontAttributes.Weight), MatchStyleStrict(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightMiddle(fontAttributes.Weight), MatchStyleObliqueAndItalic(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightOutward(fontAttributes.Weight), MatchStyleStrict(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightOutward(fontAttributes.Weight), MatchStyleObliqueAndItalic(fontAttributes.Style)), - fontName - }; - - return fontNames.Distinct().Where(x => !string.IsNullOrEmpty(x)).ToArray(); - } - - private static string MatchStyleStrict(FontStyle fontStyle) - { - if (fontStyle == FontStyle.Oblique || fontStyle == FontStyle.Italic) - { - return fontStyle.ToString(); - } - - return string.Empty; - } - - private static string MatchStyleObliqueAndItalic(FontStyle fontStyle) - { - if (fontStyle == FontStyle.Oblique || fontStyle == FontStyle.Italic) - { - return nameof(FontStyle.Italic); - } - - return string.Empty; - } - - private static string MatchWeightStrict(FontWeight fontWeight) - { - if (fontWeight == FontWeight.Normal) - { - return string.Empty; - } - - return fontWeight.ToString(); - } - - private static string MatchWeightMiddle(FontWeight fontWeight) - { - if (fontWeight == FontWeight.Light) - { - return nameof(FontWeight.Light); - } - - if (fontWeight == FontWeight.Medium) - { - return nameof(FontWeight.Medium); - } - - if (fontWeight < FontWeight.Light) - { - return $"{fontWeight + 100}"; - } - - if (fontWeight < FontWeight.Medium) - { - return $"{fontWeight - 100}"; - } - - return string.Empty; - } - - private static string MatchWeightOutward(FontWeight fontWeight) - { - if (fontWeight == FontWeight.Thin) - { - return nameof(FontWeight.Thin); - } - - if (fontWeight == FontWeight.Black) - { - return nameof(FontWeight.Black); - } - - if (fontWeight < FontWeight.Normal) - { - return $"{fontWeight - 100}"; - } - - if (fontWeight > FontWeight.Normal) - { - return $"{fontWeight + 100}"; - } - - return string.Empty; - } - - private static string BuildStylizedFontName(string fontName, string fontWeight, string fontStyle) - { - var stylizedFontName = new StringBuilder(fontName); - - if (!string.IsNullOrEmpty(fontWeight)) - { - stylizedFontName.Append($" {fontWeight}"); - } - - if (!string.IsNullOrEmpty(fontStyle)) - { - stylizedFontName.Append($" {fontStyle}"); - } - - return stylizedFontName.ToString(); - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolveStrategy.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolveStrategy.cs deleted file mode 100644 index ac7d9c126..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolveStrategy.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public enum FontResolveStrategy - { - Strict, - Closest - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs index 189acc045..45187f3a5 100644 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs +++ b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs @@ -1,152 +1,602 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using PdfSharp.Fonts; -using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery; -using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution { - public class FontResolver : IFontResolver - { - private readonly FontDiscoveryService _fontDiscoveryService = new FontDiscoveryService(); - private readonly Dictionary _fontCache = new Dictionary(); - - private List _fontMetadataCache = new List(); - - public static string FallbackFont => "Tuffy"; - public FontResolveStrategy ResolveStrategy { get; set; } = FontResolveStrategy.Strict; - - public FontResolverInfo ResolveTypeface(string familyName, bool bold, bool italic) - { - if (_fontMetadataCache.Count == 0) - { - DiscoverFonts(); - } - - var fontAttributes = new FontAttributes - { - Style = italic ? FontStyle.Italic : FontStyle.Normal, - Weight = bold ? FontWeight.Bold : FontWeight.Normal - }; - - var fontNames = FontNameResolver.ResolveFontNames(familyName, fontAttributes, ResolveStrategy); - - foreach (var name in fontNames) - { - foreach (var metadata in _fontMetadataCache) - { - var stylizedPreferredFamilyName = FontNameResolver.StylizeFontNamePreferredFamily(metadata); - var stylizedFamilyName = FontNameResolver.StylizeFontNameFamily(metadata); + /// + /// PDFsharp implementation backing the PdfSharp backend's font handling - + /// OS font discovery plus CSS Fonts Level 4 §5 nearest-match (slant→stretch→weight) face selection, + /// ported from PeachPDF's Fonts\FontResolver.cs. Unlike WinForms/WPF (which register an + /// @font-face face as an opaque platform font-family handle and let the OS/UI framework do + /// matching), PDFsharp's contract needs raw font bytes at PDF-generation + /// time for embedding, so this type does its own matching entirely and is called directly by + /// Adapters.PdfSharpAdapter's AddFontFace/GetFont overrides, bypassing the shared + /// Core.Handlers.FontsHandler registry other backends use. + /// + /// + /// Two deliberate scope departures from PeachPDF's own resolver, both already established for this + /// port's shared Core.Handlers.FontsHandler (see its own doc comments): + /// + /// No cmap-coverage extraction fallback for unicode-range-less faces - a face with no + /// explicit unicode-range is simply treated as covering everything. This codebase resolves one + /// font per box (not per glyph/run), so there's no per-glyph precision for cmap coverage to buy. + /// No per-FontResolver-instance glyph-typeface/descriptor caching. PeachPDF supports many + /// concurrent PdfGenerators each with their own FontResolver, so it isolates their custom + /// font caches from each other. This backend's PdfSharpAdapter (and this resolver with it) is a + /// single process-wide singleton via , matching this project's pre-existing + /// architecture - there's only ever one instance, so nothing to isolate. + /// + /// System font discovery itself is also adapted, not verbatim: no Android/iOS branches (this project's + /// netstandard2.0;net8.0 target framework list has no mobile leg to run them on), and Linux + /// discovery shells out to the fc-list CLI rather than P/Invoking libfontconfig.so.1 + /// directly (see 's own doc comment for why). + /// + public sealed class FontResolver : IFontResolver + { + public const string FallbackFont = "Tuffy"; - if (stylizedPreferredFamilyName == name || stylizedFamilyName == name) - { - if (!_fontCache.ContainsKey(name)) - { - _fontCache[name] = metadata; - } + private static readonly string[] FontExtensions = { "*.ttf", "*.otf" }; - return new FontResolverInfo(name); - } - } + private static readonly Dictionary _systemFontPaths; + private static readonly Dictionary _systemFamilies; + + private readonly Dictionary _customFonts = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _installedFonts; + private readonly List _customFontDirectories = new List(); + + /// + /// When true, a family/codepoint that can't be resolved returns null instead of falling back to + /// the embedded Tuffy font - lets a caller detect "nothing matched" instead of silently + /// substituting a visually different fallback face. + /// + public bool NullIfFontNotFound { get; set; } + + static FontResolver() + { + var supportedFonts = DiscoverSupportedFonts(); + var parsed = ParseSystemFonts(supportedFonts); + _systemFontPaths = parsed.Paths; + _systemFamilies = parsed.Families; + } + + public FontResolver() + { + _installedFonts = new Dictionary(_systemFamilies, StringComparer.Ordinal); + } + + /// Registers a new instance as PDFsharp's process-wide global resolver. + public static FontResolver Register() + { + var fontResolver = new FontResolver(); + GlobalFontSettings.FontResolver = fontResolver; + return fontResolver; + } + + #region OS font discovery + + private static string[] GetFontFiles(string dir) + { + if (!Directory.Exists(dir)) + return Array.Empty(); + + try + { + return FontExtensions + .SelectMany(pattern => Directory.GetFiles(dir, pattern, SearchOption.AllDirectories)) + .ToArray(); + } + catch (UnauthorizedAccessException) + { + // Some directories may exist but be unreadable depending on OS/permission configuration - + // treat that the same as "no fonts here" rather than failing discovery entirely. + return Array.Empty(); } - - // Return fallback if no matching font is found - var stylizedFallbackFontName = FontNameResolver.StylizeFontNameStrict(FallbackFont, fontAttributes); - return new FontResolverInfo(stylizedFallbackFontName); } - public byte[] GetFont(string faceName) + internal static string[] DiscoverSupportedFonts() { - if (_fontCache.TryGetValue(faceName, out var font)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - if (File.Exists(font.FilePath)) - { - return File.ReadAllBytes(font.FilePath); - } + var homeDir = Environment.GetEnvironmentVariable("HOME"); + var candidateDirs = new List { "/System/Library/Fonts", "/Library/Fonts" }; + if (!string.IsNullOrEmpty(homeDir)) + candidateDirs.Add(Path.Combine(homeDir, "Library", "Fonts")); + + return candidateDirs.SelectMany(GetFontFiles).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } - if (!faceName.StartsWith(FallbackFont)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return null; + return LinuxSystemFontResolver.Resolve(); } - // Try to load embedded fallback font - var assembly = Assembly.GetExecutingAssembly(); - - var resourceName = assembly.GetManifestResourceNames() - .FirstOrDefault(r => r.EndsWith($"{faceName}.ttf", StringComparison.OrdinalIgnoreCase)); - if (resourceName == null) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return null; + var fontDir = Environment.ExpandEnvironmentVariables(@"%SystemRoot%\Fonts"); + var fontPaths = new List(GetFontFiles(fontDir)); + + // Covers per-user-installed fonts (Windows 10+ lets a non-admin install fonts for just + // their own account), which the machine-wide Fonts folder above doesn't. + var appdataFontDir = Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\Windows\Fonts"); + fontPaths.AddRange(GetFontFiles(appdataFontDir)); + + return fontPaths.ToArray(); } - using (var stream = assembly.GetManifestResourceStream(resourceName)) + // No system font discovery on this platform - start with nothing and rely on fonts registered + // via AddFont/RegisterCustomFontDirectory. + return Array.Empty(); + } + + private static (Dictionary Paths, Dictionary Families) ParseSystemFonts(string[] supportedFonts) + { + var fontPaths = new Dictionary(StringComparer.Ordinal); + var descriptions = new List(); + + foreach (var fontPathFile in supportedFonts) { - if (stream == null) + try { - return null; + var description = TtfFontDescription.LoadDescription(fontPathFile); + descriptions.Add(description); + + if (!fontPaths.ContainsKey(description.FontNameInvariantCulture)) + fontPaths.Add(description.FontNameInvariantCulture, fontPathFile); } + catch (Exception e) + { + Debug.WriteLine(e); + } + } - using (var memoryStream = new MemoryStream()) + var families = new Dictionary(StringComparer.Ordinal); + + foreach (var familyGroup in descriptions.GroupBy(d => d.FontFamilyInvariantCulture)) + { + try { - stream.CopyTo(memoryStream); - return memoryStream.ToArray(); + var familyName = familyGroup.Key; + var family = DeserializeFontFamily(familyName, familyGroup); + families[familyName.ToLowerInvariant()] = family; + } + catch (Exception e) + { + Debug.WriteLine(e); } } + + return (fontPaths, families); + } + + private static FontFamilyModel DeserializeFontFamily(string fontFamilyName, IEnumerable descriptions) + { + var family = new FontFamilyModel { Name = fontFamilyName }; + + foreach (var description in descriptions) + { + var isItalic = (description.Style & RFontStyle.Italic) != 0; + // System fonts declare no explicit unicode-range - their effective coverage is + // "everything" (see this type's own doc comment). Keep the first face seen per + // (weight, italic, stretch). + if (!family.Faces.Any(f => f.Weight == description.Weight && f.Italic == isItalic && f.Stretch == description.Stretch)) + family.Faces.Add(new FontFaceEntry(description.Weight, isItalic, description.Stretch, null, description)); + } + + return family; + } + + #endregion + + #region Registration (@font-face src: url()/local(), custom directories) + + /// Registers a font under , using the values sniffed from the file itself. + public void AddFont(Stream stream, string fontFamilyName) + { + AddFont(stream, fontFamilyName, null, null, null, null); + } + + /// + /// Registers a font under , optionally overriding the face's own + /// sniffed weight/style/stretch with the values an @font-face rule declared for it - those + /// descriptors are authoritative for how that specific resource participates in matching, + /// independent of what the file's own internal tables say. Null means "use the value sniffed from + /// the file itself". restricts which codepoints this face is used + /// for; null means "covers whatever is asked of it" (see this type's own doc comment). + /// + public void AddFont(Stream stream, string fontFamilyName, int? weightOverride, bool? isItalicOverride, int? stretchOverride, IReadOnlyList unicodeRanges) + { + var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + var fontBytes = memoryStream.ToArray(); + memoryStream.Seek(0, SeekOrigin.Begin); + + var description = TtfFontDescription.LoadDescription(memoryStream); + + var weight = weightOverride ?? description.Weight; + var isItalic = isItalicOverride ?? (description.Style & RFontStyle.Italic) != 0; + var stretch = stretchOverride ?? description.Stretch; + + // The face name is the identity under which the bytes are stored and later fetched (GetFont) + // for embedding - normally the font's own internal name. But two DIFFERENT fonts can share one + // internal name (a common webfont-subset pattern, e.g. every "Roboto" subset file reports + // "Roboto"); those must not collide in _customFonts (the second would overwrite the first's + // bytes), so disambiguate with a content checksum when that happens. + var internalName = description.FontNameInvariantCulture; + var faceName = internalName; + if (_customFonts.TryGetValue(internalName, out var existingBytes) && !ByteArraysEqual(existingBytes, fontBytes)) + { + faceName = internalName + "#" + ComputeChecksum(fontBytes).ToString("x", CultureInfo.InvariantCulture); + } + + var faceDescription = new TtfFontDescription(description.FontFamilyInvariantCulture, faceName, description.Style, weight, stretch); + + RegisterFace(fontFamilyName, new FontFaceEntry(weight, isItalic, stretch, unicodeRanges, faceDescription)); + _customFonts[faceName] = fontBytes; } + /// + /// Satisfies an @font-face src: local(...) candidate: finds the nearest face already + /// registered under (a system font or an earlier registration) + /// for the given axes, and registers that same face's bytes as a face of + /// too - reusing the OS font's embedded bytes, not copying them. + /// + /// true if a local family by that name was found and registered, false otherwise (the caller tries the next src candidate) + public bool AddLocalFontFamily(string familyName, string localFamilyName, int? weightOverride, bool? isItalicOverride, int? stretchOverride, IReadOnlyList ranges) + { + if (!_installedFonts.TryGetValue(localFamilyName.ToLowerInvariant(), out var localFamily) || localFamily.Faces.Count == 0) + return false; + + var weight = weightOverride ?? TtfFontDescription.DefaultWeight; + var isItalic = isItalicOverride ?? false; + var stretch = stretchOverride ?? TtfFontDescription.DefaultStretch; + + if (!TryFindNearestFace(localFamily, weight, isItalic, stretch, null, out var sourceFace)) + return false; + + var entry = new FontFaceEntry( + weightOverride ?? sourceFace.Weight, + isItalicOverride ?? sourceFace.Italic, + stretchOverride ?? sourceFace.Stretch, + ranges, + sourceFace.Description); + + RegisterFace(familyName, entry); + return true; + } + + /// + /// Registers to be scanned for TTF/OTF files, each registered + /// under its own sniffed family name (unlike , which requires + /// the caller to already know the family name up front). + /// public void RegisterCustomFontDirectory(string fontDirectory) { - _fontDiscoveryService.RegisterCustomFontDirectory(fontDirectory); - - // Invalidate the cache to trigger discovery - _fontMetadataCache.Clear(); + if (_customFontDirectories.Contains(fontDirectory, StringComparer.OrdinalIgnoreCase)) + return; + + _customFontDirectories.Add(fontDirectory); + + foreach (var path in GetFontFiles(fontDirectory)) + { + try + { + var description = TtfFontDescription.LoadDescription(path); + using (var stream = File.OpenRead(path)) + { + AddFont(stream, description.FontFamilyInvariantCulture); + } + } + catch + { + // Not every *.ttf/*.otf found in a directory scan is necessarily a valid/readable font + // file - skip it and keep discovering the rest. + } + } } + /// Every family name currently registered (system-discovered, plus any added via /). public List DiscoverFontFamilies() { - if (_fontMetadataCache.Count == 0) + return _installedFonts.Values.Select(f => f.Name).ToList(); + } + + /// + /// Replaces any existing same-slot face (same weight/italic/stretch/unicode-range - a + /// re-registration) while letting a same-axes face with a different range set coexist (the + /// unicode-range subset case), then adds . Clones the family before + /// mutating: may currently resolve to the shared static + /// _systemFamilies snapshot (or an already-private clone from a prior call), and this must + /// never write into state shared across instances. + /// + private void RegisterFace(string familyName, FontFaceEntry entry) + { + var key = familyName.ToLowerInvariant(); + _installedFonts.TryGetValue(key, out var existingFamily); + + var clonedFamily = new FontFamilyModel { Name = existingFamily?.Name ?? familyName }; + if (existingFamily != null) + { + foreach (var face in existingFamily.Faces) + { + if (!IsSameFaceSlot(face, entry.Weight, entry.Italic, entry.Stretch, entry.ExplicitRanges)) + clonedFamily.Faces.Add(face); + } + } + + clonedFamily.Faces.Add(entry); + _installedFonts[key] = clonedFamily; + } + + private static bool IsSameFaceSlot(FontFaceEntry entry, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + return entry.Weight == weight && entry.Italic == isItalic && entry.Stretch == stretch && RangesEqual(entry.ExplicitRanges, ranges); + } + + private static bool RangesEqual(IReadOnlyList a, IReadOnlyList b) + { + if (a == null || b == null) + return a == null && b == null; + if (a.Count != b.Count) + return false; + for (var i = 0; i < a.Count; i++) { - DiscoverFonts(); + if (a[i].Start != b[i].Start || a[i].End != b[i].End) + return false; } - - var fontFamilies = new List(); - - foreach (var metadata in _fontMetadataCache) + return true; + } + + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + for (var i = 0; i < a.Length; i++) { - var stylizedPreferredFamilyName = FontNameResolver.StylizeFontNamePreferredFamily(metadata); - var stylizedFamilyName = FontNameResolver.StylizeFontNameFamily(metadata); + if (a[i] != b[i]) + return false; + } + return true; + } - if (!string.IsNullOrEmpty(stylizedPreferredFamilyName)) + // A cheap, non-cryptographic content hash - only used to disambiguate two different font files + // that happen to report the same internal name (see AddFont), not for any security purpose. + private static uint ComputeChecksum(byte[] bytes) + { + unchecked + { + const uint fnvPrime = 16777619; + var hash = 2166136261; + foreach (var b in bytes) { - fontFamilies.Add(stylizedFamilyName); + hash ^= b; + hash *= fnvPrime; } - - fontFamilies.Add(stylizedFamilyName); + return hash; } - - return fontFamilies; } - - public static FontResolver Register() + + #endregion + + #region IFontResolver + + public byte[] GetFont(string faceName) { - var fontResolver = new FontResolver(); - GlobalFontSettings.FontResolver = fontResolver; - return fontResolver; + if (_customFonts.TryGetValue(faceName, out var fontBytes)) + return fontBytes; + + if (_systemFontPaths.TryGetValue(faceName, out var fontPath) && File.Exists(fontPath)) + return File.ReadAllBytes(fontPath); + + if (faceName != null && faceName.StartsWith(FallbackFont, StringComparison.Ordinal)) + { + var embedded = LoadEmbeddedFallbackFont(faceName); + if (embedded != null) + return embedded; + } + + throw new ArgumentOutOfRangeException(nameof(faceName), faceName, "Unknown font face name."); } - private void DiscoverFonts() + public bool HasFont(string faceName) { - var fontInfos = _fontDiscoveryService.DiscoverFontInfos(); + return _customFonts.ContainsKey(faceName) || _systemFontPaths.ContainsKey(faceName); + } - _fontMetadataCache = fontInfos.Select(fontInfo => fontInfo.FilePath) - .Distinct() - .Select(FontParser.ExtractFontMetadata) - .Where(fontMetadata => fontMetadata != null) - .ToList(); + /// + /// Whether is registered - a system-discovered family, or one + /// registered via / + /// . Used by so + /// @font-face-only families (registered here, not in the shared FontsHandler - see + /// this type's own doc comment) are still recognized by . + /// + public bool HasFamily(string familyName) + { + return !string.IsNullOrEmpty(familyName) && _installedFonts.ContainsKey(familyName.ToLowerInvariant()); } + + /// Whether any face of declares an explicit unicode-range. + public bool HasExplicitRanges(string familyName) + { + return _installedFonts.TryGetValue(familyName.ToLowerInvariant(), out var family) + && family.Faces.Any(f => f.ExplicitRanges != null); + } + + public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic) => + ResolveTypeface(familyName, isBold ? 700 : 400, isItalic); + + public FontResolverInfo ResolveTypeface(string familyName, int weight, bool isItalic) => + ResolveTypeface(familyName, weight, isItalic, TtfFontDescription.DefaultStretch); + + public FontResolverInfo ResolveTypeface(string familyName, int weight, bool isItalic, int stretch) => + ResolveTypeface(familyName, weight, isItalic, stretch, null); + + /// + /// Resolves a face for at the requested axes, optionally restricted + /// to faces whose unicode-range covers . A codepoint-scoped + /// request that finds no covering face returns null, so per-codepoint matching can move on to the + /// next family instead of substituting a face that cannot render the character. A codepoint-less + /// request keeps the previous behavior (no coverage filter, plus a fallback rather than null/throw). + /// + public FontResolverInfo ResolveTypeface(string familyName, int weight, bool isItalic, int stretch, int? codepoint) + { + if (!string.IsNullOrEmpty(familyName) && _installedFonts.TryGetValue(familyName.ToLowerInvariant(), out var family)) + { + if (TryFindNearestFace(family, weight, isItalic, stretch, codepoint, out var face)) + { + // The chosen face may be a compromise (nearest-weight/slant match, not exact) - decide + // whether the gap is large enough that faux-bold/italic synthesis should kick in. + // Threshold mirrors the common UA convention that weights >=600 read as "bold". + var mustSimulateBold = weight >= 600 && face.Weight < 600; + var mustSimulateItalic = isItalic && !face.Italic; + return new FontResolverInfo(face.Description.FontNameInvariantCulture, mustSimulateBold, mustSimulateItalic); + } + } + + // A codepoint-scoped miss must not substitute an arbitrary non-covering face - report it so + // the caller tries the next family (and ultimately the box default). + if (codepoint.HasValue) + return null; + + if (NullIfFontNotFound) + return null; + + return new FontResolverInfo(StylizeTuffyFaceName(weight, isItalic)); + } + + /// + /// CSS Fonts Level 4 §5 face matching. When is supplied, only faces + /// whose unicode-range includes it (or which declare none at all - see this type's own doc + /// comment) are candidates; among equally-good matches the last-declared wins (CSS cascade order + /// for overlapping ranges). Otherwise every face is a candidate. Within the candidates it narrows + /// italic/slant first, then stretch, then weight; an exact axis match short-circuits. + /// + private static bool TryFindNearestFace(FontFamilyModel family, int weight, bool isItalic, int stretch, int? codepoint, out FontFaceEntry face) + { + face = null; + + var covering = codepoint.HasValue + ? family.Faces.Where(f => FaceCovers(f, codepoint.Value)).ToList() + : family.Faces; + + if (covering.Count == 0) + return false; + + var exact = covering.Where(f => f.Weight == weight && f.Italic == isItalic && f.Stretch == stretch).ToList(); + if (exact.Count > 0) + { + face = exact[exact.Count - 1]; + return true; + } + + var sameSlant = covering.Where(f => f.Italic == isItalic).ToList(); + var candidates = sameSlant.Count > 0 ? sameSlant : covering; + + var availableStretches = candidates.Select(f => f.Stretch).Distinct().ToList(); + var chosenStretch = PickNearestStretch(availableStretches, stretch); + var stretchCandidates = candidates.Where(f => f.Stretch == chosenStretch).ToList(); + + var availableWeights = stretchCandidates.Select(f => f.Weight).Distinct().ToList(); + var chosenWeight = PickNearestWeight(availableWeights, weight); + + face = stretchCandidates.Last(f => f.Weight == chosenWeight); + return true; + } + + private static bool FaceCovers(FontFaceEntry entry, int codepoint) => + entry.ExplicitRanges == null || UnicodeRangeParser.Covers(entry.ExplicitRanges, codepoint); + + /// + /// CSS Fonts Level 4 §5.2's nearest-stretch search order: a target at or narrower than normal (5) + /// searches narrower first (down to 1), then wider; a target wider than normal searches wider + /// first (up to 9), then narrower. must be non-empty. + /// + private static int PickNearestStretch(List availableStretches, int target) + { + if (availableStretches.Contains(target)) + return target; + + var candidates = target <= TtfFontDescription.DefaultStretch + ? availableStretches.Where(s => s < target).OrderByDescending(s => s) + .Concat(availableStretches.Where(s => s > target).OrderBy(s => s)) + : availableStretches.Where(s => s > target).OrderBy(s => s) + .Concat(availableStretches.Where(s => s < target).OrderByDescending(s => s)); + + return candidates.First(); + } + + /// + /// CSS Fonts Level 4 §5.2's nearest-weight search order (the standard browser algorithm): a target + /// in [400,500] searches upward to 500 first, then below the target, then above 500; a target + /// below 400 searches downward first, then upward; a target above 500 searches upward first, then + /// downward. must be non-empty and is assumed to NOT already + /// contain an exact match for (the caller checks that separately, since + /// an exact match also has to match the requested italic-ness, which this purely-numeric helper + /// doesn't know about). + /// + private static int PickNearestWeight(List availableWeights, int target) + { + IEnumerable candidates; + if (target >= 400 && target <= 500) + { + candidates = availableWeights.Where(w => w >= target && w <= 500).OrderBy(w => w) + .Concat(availableWeights.Where(w => w < target).OrderByDescending(w => w)) + .Concat(availableWeights.Where(w => w > 500).OrderBy(w => w)); + } + else if (target < 400) + { + candidates = availableWeights.Where(w => w < target).OrderByDescending(w => w) + .Concat(availableWeights.Where(w => w > target).OrderBy(w => w)); + } + else + { + candidates = availableWeights.Where(w => w > target).OrderBy(w => w) + .Concat(availableWeights.Where(w => w < target).OrderByDescending(w => w)); + } + + return candidates.First(); + } + + private static string StylizeTuffyFaceName(int weight, bool isItalic) + { + var isBold = weight >= 600; + if (isBold && isItalic) return "Tuffy Bold Italic"; + if (isBold) return "Tuffy Bold"; + if (isItalic) return "Tuffy Italic"; + return FallbackFont; + } + + private static byte[] LoadEmbeddedFallbackFont(string faceName) + { + var assembly = Assembly.GetExecutingAssembly(); + + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(r => r.EndsWith(faceName + ".ttf", StringComparison.OrdinalIgnoreCase)); + if (resourceName == null) + return null; + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + return null; + + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } + } + } + + #endregion } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/LinuxSystemFontResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/LinuxSystemFontResolver.cs new file mode 100644 index 000000000..d5e12ba03 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp/FontResolution/LinuxSystemFontResolver.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution +{ + /// + /// Linux system font-file discovery, used by . + /// + /// + /// Adapted, not verbatim, from PeachPDF's Fonts\LinuxSystemFontResolver.cs: PeachPDF + /// P/Invokes libfontconfig.so.1 directly via source-generated [LibraryImport] bindings, + /// which need .NET 7+ and don't exist on this project's netstandard2.0 target framework. + /// Shelling out to the fc-list CLI - fontconfig's own command-line front end, present wherever + /// libfontconfig itself is - returns the identical font-file listing with no native interop and + /// no TFM split; this project's own pre-existing Linux discovery already took this approach, so it's + /// kept here rather than replaced. When fc-list isn't available (or fails), falls back to + /// scanning fonts.conf's declared directories plus the conventional well-known ones directly - + /// matching PeachPDF's own fallback behavior. + /// + internal static class LinuxSystemFontResolver + { + public static string[] Resolve() + { + try + { + var files = ResolveViaFontConfig(); + if (files.Length > 0) + return files; + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + return ResolveFallback(); + } + + private static string[] ResolveViaFontConfig() + { + var startInfo = new ProcessStartInfo + { + FileName = "fc-list", + Arguments = ": file", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(startInfo)) + { + if (process == null) + return Array.Empty(); + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) + return Array.Empty(); + + // Each line looks like: "/path/to/font.ttf: file" + return output + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Split(':')[0].Trim()) + .Where(IsSupportedFontFile) + .Where(File.Exists) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + } + + private static bool IsSupportedFontFile(string path) => + path.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".otf", StringComparison.OrdinalIgnoreCase); + + private static string[] ResolveFallback() + { + var fontFiles = new List(); + + foreach (var path in GetSearchPaths().Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (!Directory.Exists(path)) + continue; + + try + { + fontFiles.AddRange(Directory.EnumerateFiles(path, "*.ttf", SearchOption.AllDirectories)); + fontFiles.AddRange(Directory.EnumerateFiles(path, "*.otf", SearchOption.AllDirectories)); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + + return fontFiles.ToArray(); + } + + private static IEnumerable GetSearchPaths() + { + var dirs = new List(); + + try + { + var confDirRegex = new Regex("(?.*)"); + if (File.Exists("/etc/fonts/fonts.conf")) + { + foreach (var line in File.ReadLines("/etc/fonts/fonts.conf")) + { + var match = confDirRegex.Match(line); + if (!match.Success) + continue; + + var path = match.Groups["dir"].Value.Trim(); + if (path.StartsWith("~")) + path = Environment.GetEnvironmentVariable("HOME") + path.Substring(1); + + dirs.Add(path); + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + dirs.Add("/usr/share/fonts"); + dirs.Add("/usr/local/share/fonts"); + var home = Environment.GetEnvironmentVariable("HOME"); + if (!string.IsNullOrEmpty(home)) + dirs.Add(Path.Combine(home, ".fonts")); + + return dirs; + } + } +} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/BinaryParser.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/BinaryParser.cs deleted file mode 100644 index b67434a40..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/BinaryParser.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Buffers; -using System.Text; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing -{ - internal static class BinaryParser - { - public static ushort ReadUint16BigEndian(Span data, int offset) - { - if (offset + 1 >= data.Length) - { - return 0; - } - - return (ushort)((data[offset] << 8) | data[offset + 1]); - } - - public static uint ReadUint32BigEndian(Span data, int offset) - { - if (offset + 3 >= data.Length) - { - return 0; - } - - return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | - ((uint)data[offset + 2] << 8) | data[offset + 3]; - } - - public static string ReadAsciiString(Span data, int offset, int length) - { - return Encoding.ASCII.GetString(data.Slice(offset, length).ToArray()); - } - - public static string ReadAsciiString(Span data) - { - return ReadAsciiString(data, 0, data.Length); - } - - public static string ReadUtf16StringBigEndian(Span data) - { - var chars = ArrayPool.Shared.Rent(data.Length); - - try - { - var index = 0; - - for (var i = 0; i < data.Length - 1; i += 2) - { - var c = (char)((data[i] << 8) | data[i + 1]); - - if (c == '\0') - { - continue; - } - - chars[index] = c; - ++index; - } - - return new string(chars, 0, index); - } - finally - { - ArrayPool.Shared.Return(chars); - } - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/FontParser.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/FontParser.cs deleted file mode 100644 index db01ae555..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/FontParser.cs +++ /dev/null @@ -1,467 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing -{ - internal static class FontParser - { - public static FontMetadata ExtractFontMetadata(string fontFilePath) - { - try - { - using (var fs = new FileStream(fontFilePath, FileMode.Open, FileAccess.Read)) - { - if (fs.Length > int.MaxValue) - { - return null; - } - - var pooledBytes = ArrayPool.Shared.Rent((int)fs.Length); - - try - { - var bytesRead = fs.Read(pooledBytes, 0, (int)fs.Length); - var bytes = pooledBytes.AsSpan(0, bytesRead); // Fixed size span view - - var metadata = ParseFontMetadata(bytes); - if (metadata != null) - { - metadata.FilePath = fontFilePath; - } - return metadata; - } - finally - { - ArrayPool.Shared.Return(pooledBytes); - } - } - } - catch - { - return null; - } - } - - private static FontMetadata ParseFontMetadata(Span bytes) - { - // Read the font header (big-endian) - var scalarType = BinaryParser.ReadUint32BigEndian(bytes, 0); - - // Verify it's a valid TTF/OTF file - if (scalarType != 0x00010000 && // TrueType version 1.0 - scalarType != 0x74727565 && // "true" - TrueType - scalarType != 0x4F54544F) // "OTTO" - OpenType with CFF outline - { - return null; - } - - var tableCount = BinaryParser.ReadUint16BigEndian(bytes, 4); - - // Find the "name" and "os/2" tables - long nameTableOffset = -1; - uint? os2TableOffset = null; - var tableRecordOffset = 12; - - for (var i = 0; i < tableCount; i++) - { - var tag = BinaryParser.ReadAsciiString(bytes, tableRecordOffset, 4); - var offset = BinaryParser.ReadUint32BigEndian(bytes, tableRecordOffset + 8); - - if (tag == "name") - { - nameTableOffset = offset; - } - else if (tag == "OS/2") - { - os2TableOffset = offset; - } - - tableRecordOffset += 16; - } - - if (nameTableOffset == -1) - { - return null; - } - - var metadata = new FontMetadata(); - - ParseNameTable(bytes, nameTableOffset, metadata); - - // Extract OS/2 table data if available for more accurate weight/width/style info - var hasOs2Data = ParseOs2Table(bytes, os2TableOffset, metadata); - - // Parse style from subfamily if not already set from OS/2 - if (!hasOs2Data) - { - ParseStyleFromSubfamily(metadata, metadata.Subfamily); - } - - // Return metadata if we at least have a family name - return !string.IsNullOrEmpty(metadata.Family) ? metadata : null; - } - - private static void ParseNameTable(Span bytes, long nameTableOffset, FontMetadata metadata) - { - var nameTablePos = (int)nameTableOffset; - var count = BinaryParser.ReadUint16BigEndian(bytes, nameTablePos + 2); - var stringDataOffset = BinaryParser.ReadUint16BigEndian(bytes, nameTablePos + 4); - - // Track which values have been set with English entries - var hasEnglishValue = new HashSet(); - - // First pass: Look for English entries - var nameRecordOffset = nameTablePos + 6; - for (var i = 0; i < count; i++, nameRecordOffset += 12) - { - var platformId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset); - var encodingId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 2); - var languageId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 4); - var nameId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 6); - var length = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 8); - var offset = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 10); - - // Filter for English language entries: - // Platform 3 (Windows): languageID 0x0409 (1033) = US English - // Platform 1 (Macintosh): languageID 0 = English - var isEnglish = (platformId == 3 && languageId == 0x0409) || - (platformId == 1 && languageId == 0); - - if ((platformId != 3 && platformId != 1) || !IsRelevantNameId(nameId)) - { - continue; - } - - var stringPos = (int)nameTableOffset + stringDataOffset + offset; - - if (stringPos + length > bytes.Length) - { - continue; - } - - var nameBytes = bytes.Slice(stringPos, length); - - var decodedString = DecodeNameTableString(nameBytes, platformId, encodingId); - - if (string.IsNullOrEmpty(decodedString) || decodedString == null) - { - continue; - } - - // If English, set the value and mark as having English - if (isEnglish) - { - if (SetMetadataValue(metadata, nameId, decodedString)) - { - hasEnglishValue.Add(nameId); - } - } - // If not English but we haven't found an English entry yet, use it as fallback - else if (!hasEnglishValue.Contains(nameId)) - { - SetMetadataValue(metadata, nameId, decodedString); - } - } - } - - private static bool IsRelevantNameId(ushort nameId) - { - return nameId is 1 || nameId is 2 || nameId is 4 || nameId is 6 || nameId is 16 || nameId is 17; - } - - private static bool SetMetadataValue(FontMetadata metadata, ushort nameId, string value) - { - switch (nameId) - { - case 1: // Legacy Family name - if (metadata.Family == null) - { - metadata.Family = value; - return true; - } - return false; - case 2: // Legacy Subfamily - if (metadata.Subfamily == null) - { - metadata.Subfamily = value; - ParseStyleFromSubfamily(metadata, value); - return true; - } - return false; - case 4: // Full font name - if (metadata.FullName == null) - { - metadata.FullName = value; - return true; - } - return false; - case 6: // PostScript name - if (metadata.PostScriptName == null) - { - metadata.PostScriptName = value; - return true; - } - return false; - case 16: // Preferred Family (Typographic Family) - if (metadata.PreferredFamily == null) - { - metadata.PreferredFamily = value; - return true; - } - return false; - case 17: // Preferred Subfamily (Typographic Subfamily) - if (metadata.PreferredSubfamily == null) - { - metadata.PreferredSubfamily = value; - return true; - } - return false; - default: - return false; - } - } - - private static void ParseStyleFromSubfamily(FontMetadata metadata, string subfamily) - { - if (subfamily is null) - { - return; - } - - var lower = subfamily.ToLowerInvariant(); - - // Parse weight, width, and style - var weight = ParseWeight(lower); - var width = ParseWidth(lower); - var style = ParseStyle(lower); - - // Update metadata with parsed attributes (note: FontAttributes order is Weight, Style, Width) - metadata.Attributes = new FontAttributes - { - Weight = weight, - Style = style, - Width = width - }; - } - - private static FontStyle ParseStyle(string lowerSubfamily) - { - if (lowerSubfamily.Contains("oblique")) - { - return FontStyle.Oblique; - } - - if (lowerSubfamily.Contains("italic")) - { - return FontStyle.Italic; - } - - return FontStyle.Normal; - } - - private static FontWeight ParseWeight(string lowerSubfamily) - { - // Map weight keywords to enum values - if (lowerSubfamily.Contains("thin") || lowerSubfamily.Contains("hairline")) - { - return FontWeight.Thin; - } - - if (lowerSubfamily.Contains("extralight") || lowerSubfamily.Contains("ultra light")) - { - return FontWeight.ExtraLight; - } - - if (lowerSubfamily.Contains("light")) - { - return FontWeight.Light; - } - - if (lowerSubfamily.Contains("medium")) - { - return FontWeight.Medium; - } - - if (lowerSubfamily.Contains("semibold") || lowerSubfamily.Contains("demibold")) - { - return FontWeight.SemiBold; - } - - if (lowerSubfamily.Contains("extrabold") || lowerSubfamily.Contains("ultra bold")) - { - return FontWeight.ExtraBold; - } - - if (lowerSubfamily.Contains("bold")) - { - return FontWeight.Bold; - } - - if (lowerSubfamily.Contains("black") || lowerSubfamily.Contains("heavy")) - { - return FontWeight.Black; - } - - return FontWeight.Normal; - } - - private static FontWidth ParseWidth(string lowerSubfamily) - { - if (lowerSubfamily.Contains("ultracondensed") || lowerSubfamily.Contains("ultra condensed")) - { - return FontWidth.UltraCondensed; - } - - if (lowerSubfamily.Contains("extracondensed") || lowerSubfamily.Contains("extra condensed")) - { - return FontWidth.ExtraCondensed; - } - - if (lowerSubfamily.Contains("condensed")) - { - return FontWidth.Condensed; - } - - if (lowerSubfamily.Contains("semicondensed")) - { - return FontWidth.SemiCondensed; - } - - if (lowerSubfamily.Contains("semiexpanded")) - { - return FontWidth.SemiExpanded; - } - - if (lowerSubfamily.Contains("extraexpanded") || lowerSubfamily.Contains("extra expanded")) - { - return FontWidth.ExtraExpanded; - } - - if (lowerSubfamily.Contains("ultraexpanded") || lowerSubfamily.Contains("ultra expanded")) - { - return FontWidth.UltraExpanded; - } - - if (lowerSubfamily.Contains("expanded")) - { - return FontWidth.Expanded; - } - - return FontWidth.Medium; - } - - private static bool ParseOs2Table(Span bytes, uint? os2TableOffset, FontMetadata metadata) - { - if (!os2TableOffset.HasValue) - { - return false; - } - - var offset = (int)os2TableOffset.Value; - - try - { - // OS/2 table structure (v0+): - // Offset 4: usWeightClass (USHORT) - font weight (100-900) - // Offset 6: usWidthClass (USHORT) - font width (1-9) - // Offset 8: fsType (USHORT) - embedding permissions - // Offset 62: fsSelection (USHORT) - contains style bits - - // Check if we have enough bytes for OS/2 table - if (offset + 64 > bytes.Length) - { - return false; - } - - // Read usWeightClass (offset 4 in OS/2) - var usWeightClass = BinaryParser.ReadUint16BigEndian(bytes, offset + 4); - var weight = FontWeight.Normal; - if (usWeightClass >= 100 && usWeightClass <= 900) - { - weight = (FontWeight)usWeightClass; - } - - // Read usWidthClass (offset 6 in OS/2) - var usWidthClass = BinaryParser.ReadUint16BigEndian(bytes, offset + 6); - var width = FontWidth.Medium; - if (usWidthClass >= 1 && usWidthClass <= 9) - { - width = WidthClassToEnum(usWidthClass); - } - - // Read fsSelection for style bits (offset 62 in OS/2) - var fsSelection = BinaryParser.ReadUint16BigEndian(bytes, offset + 62); - - // Bit 9: Oblique, Bit 6: Regular, Bit 0: Italic - var style = ((fsSelection & 0x0200) != 0) ? FontStyle.Oblique : - ((fsSelection & 0x0001) != 0) ? FontStyle.Italic : - FontStyle.Normal; - - // Update attributes with extracted OS/2 data (note: FontAttributes order is Weight, Style, Width) - metadata.Attributes = new FontAttributes - { - Weight = weight, - Style = style, - Width = width - }; - return true; - } - catch - { - // Silently continue if OS/2 parsing fails - return false; - } - } - - private static FontWidth WidthClassToEnum(int widthClass) - { - switch (widthClass) - { - case 1: return FontWidth.UltraCondensed; - case 2: return FontWidth.ExtraCondensed; - case 3: return FontWidth.Condensed; - case 4: return FontWidth.SemiCondensed; - case 5: return FontWidth.Medium; - case 6: return FontWidth.SemiExpanded; - case 7: return FontWidth.Expanded; - case 8: return FontWidth.ExtraExpanded; - case 9: return FontWidth.UltraExpanded; - default: return FontWidth.Medium; - } - } - - private static string DecodeNameTableString(Span data, ushort platformId, ushort encodingId) - { - try - { - // Windows platform - if (platformId == 3) - { - if (encodingId == 1) - { - return BinaryParser.ReadUtf16StringBigEndian(data); - } - - return null; - } - - // Macintosh platform - if (platformId == 1) - { - if (encodingId == 0) - { - return BinaryParser.ReadAsciiString(data).TrimEnd('\0'); - } - } - - return null; - } - catch - { - return null; - } - } - } -} diff --git a/Source/HtmlRenderer.PdfSharp/HtmlContainer.cs b/Source/HtmlRenderer.PdfSharp/HtmlContainer.cs index 9339fd562..6861a23eb 100644 --- a/Source/HtmlRenderer.PdfSharp/HtmlContainer.cs +++ b/Source/HtmlRenderer.PdfSharp/HtmlContainer.cs @@ -13,6 +13,7 @@ using PdfSharp.Drawing; using System; using System.Collections.Generic; +using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; @@ -256,9 +257,9 @@ public string SelectedHtml /// /// the html to init with, init empty if not given /// optional: the stylesheet to init with, init default if not given - public void SetHtml(string htmlSource, CssData baseCssData = null) + public Task SetHtml(string htmlSource, CssData baseCssData = null) { - _htmlContainerInt.SetHtml(htmlSource, baseCssData); + return _htmlContainerInt.SetHtml(htmlSource, baseCssData); } /// diff --git a/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs b/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs index 99e9b56ed..a78ea5259 100644 --- a/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs +++ b/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs @@ -14,6 +14,7 @@ using PdfSharp.Drawing; using PdfSharp.Pdf; using System; +using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; @@ -77,9 +78,13 @@ public static void RegisterCustomFontDirectory(string fontDirectory) /// the stylesheet source to parse /// true - combine the parsed CSS data with default CSS data, false - return only the parsed CSS data /// the parsed CSS data - public static CssData ParseStyleSheet(string stylesheet, bool combineWithDefault = true) + public static Task ParseStyleSheet(string stylesheet, bool combineWithDefault = true) { - return CssData.Parse(PdfSharpAdapter.Instance, stylesheet, combineWithDefault); + // CssData.Parse -> CssParser.ParseStyleSheet is not itself async yet (its @import resolution + // bridges into the async resource-loading pipeline synchronously for now - it's also called + // directly from several UI controls' public API, out of scope for this conversion) - wrapped + // in Task.FromResult here so this method's own shape matches the rest of this async-only API. + return Task.FromResult(CssData.Parse(PdfSharpAdapter.Instance, stylesheet, combineWithDefault)); } /// @@ -92,12 +97,12 @@ public static CssData ParseStyleSheet(string stylesheet, bool combineWithDefault /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the HTML - public static PdfDocument GeneratePdf(string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + public static async Task GeneratePdf(string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { var config = new PdfGenerateConfig(); config.PageSize = pageSize; config.SetMargins(margin); - return GeneratePdf(html, config, cssData, stylesheetLoad, imageLoad); + return await GeneratePdf(html, config, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false); } /// @@ -109,13 +114,13 @@ public static PdfDocument GeneratePdf(string html, PageSize pageSize, int margin /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the HTML - public static PdfDocument GeneratePdf(string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + public static async Task GeneratePdf(string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { // create PDF document to render the HTML into var document = new PdfDocument(); // add rendered PDF pages to document - AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad); + await AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false); return document; } @@ -131,12 +136,12 @@ public static PdfDocument GeneratePdf(string html, PdfGenerateConfig config, Css /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the HTML - public static void AddPdfPages(PdfDocument document, string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + public static Task AddPdfPages(PdfDocument document, string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { var config = new PdfGenerateConfig(); config.PageSize = pageSize; config.SetMargins(margin); - AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad); + return AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad); } /// @@ -149,7 +154,7 @@ public static void AddPdfPages(PdfDocument document, string html, PageSize pageS /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the HTML - public static void AddPdfPages(PdfDocument document, string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + public static async Task AddPdfPages(PdfDocument document, string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { XSize orgPageSize; // get the size of each page to layout the HTML in @@ -177,7 +182,7 @@ public static void AddPdfPages(PdfDocument document, string html, PdfGenerateCon container.Location = new XPoint(config.MarginLeft, config.MarginTop); container.MaxSize = new XSize(pageSize.Width, 0); - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); container.PageSize = pageSize; container.MarginBottom = config.MarginBottom; container.MarginLeft = config.MarginLeft; diff --git a/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs index a6ffb66bb..af19357b4 100644 --- a/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs +++ b/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs @@ -46,9 +46,9 @@ public override void LineTo(double x, double y) _geometryContext.LineTo(new Point(x, y), true, true); } - public override void ArcTo(double x, double y, double size, Corner corner) + public override void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner) { - _geometryContext.ArcTo(new Point(x, y), new Size(size, size), 0, false, SweepDirection.Clockwise, true, true); + _geometryContext.ArcTo(new Point(x, y), new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, true); } /// diff --git a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs index 3ea88d9c4..d0fda838e 100644 --- a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs +++ b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs @@ -13,12 +13,15 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net.Http; using System.Reflection; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Network; using TheArtOfDev.HtmlRenderer.WPF.Utilities; using Microsoft.Win32; @@ -31,6 +34,20 @@ internal sealed class WpfAdapter : RAdapter { #region Fields and Consts + // One HttpClient shared for the adapter's (process) lifetime, not one per request - `new + // HttpClient()` per call is a well-documented anti-pattern that exhausts sockets under load and + // never observes DNS changes. + // + // Declared BEFORE _instance deliberately: C# runs static field initializers in textual + // declaration order, and _instance's own initializer (`new WpfAdapter()`) runs the instance + // constructor immediately, which reads _sharedHttpClient on its very first line. If this field + // were declared after _instance, that read would observe _sharedHttpClient's still-default value + // (null - its own initializer hasn't run yet) and permanently capture a null HttpClient into + // NetworkLoader, since HttpClientNetworkLoader takes it as a constructor parameter, not a live + // reference to this field. (Confirmed by a real crash with this exact ordering, in the sibling + // WinFormsAdapter.) + private static readonly HttpClient _sharedHttpClient = new HttpClient(); + /// /// Singleton instance of global adapter. /// @@ -39,7 +56,20 @@ internal sealed class WpfAdapter : RAdapter /// /// List of valid predefined color names in lower-case /// - private static readonly List ValidColorNamesLc; + private static readonly List ValidColorNamesLc; + + // Backs LoadFontFaceFontInt's temp-file registration (see its own doc comment for why a real file + // is necessary - WPF's font-loading APIs are documented as file-URI-only, with no supported + // memory-only path) - one directory per process, cleaned up by the OS's normal temp-file + // housekeeping, not by this process. + private static readonly string _fontFaceTempDirectory = CreateFontFaceTempDirectory(); + + private static string CreateFontFaceTempDirectory() + { + var dir = Path.Combine(Path.GetTempPath(), "HtmlRenderer.FontFace." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + return dir; + } #endregion @@ -58,6 +88,14 @@ static WpfAdapter() /// private WpfAdapter() { + // Unlike the PdfSharp backend (which keeps the base RAdapter.NetworkLoader default of + // DataUriNetworkLoader-only - safer for unattended/server-side PDF generation, matching + // PeachPDF's own default), WPF is an interactive UI backend where fetching a real http(s): + // image or stylesheet out of the box is the expected behavior. data:/file: URIs still resolve + // the same way regardless (RAdapter.GetResourceStream intercepts both before ever consulting + // NetworkLoader), so only http(s): actually reaches this loader in practice. + NetworkLoader = new HttpClientNetworkLoader(_sharedHttpClient, (Uri)null); + AddFontFamilyMapping("monospace", "Courier New"); AddFontFamilyMapping("Helvetica", "Arial"); @@ -71,6 +109,19 @@ private WpfAdapter() { } } + + SystemEvents.UserPreferenceChanged += (sender, e) => + { + if (e.Category != UserPreferenceCategory.General) return; + + // The General category covers far more than the theme, so re-read and only report a + // change if the scheme really moved - otherwise every unrelated preference change + // would force a re-cascade and repaint. + var previous = _colorScheme; + _colorScheme = null; + if (previous.HasValue && previous.Value != SystemColorScheme) + OnColorSchemeChanged(); + }; } /// @@ -81,6 +132,27 @@ public static WpfAdapter Instance get { return _instance; } } + /// + /// Rendering onto a Windows surface, so the document should follow the user's app theme. + /// Cached and invalidated on a system preference change rather than read per query. + /// + public override RColorScheme SystemColorScheme + { + get + { + if (SystemColorSchemeOverride.HasValue) + return SystemColorSchemeOverride.Value; + if (!_colorScheme.HasValue) + _colorScheme = WindowsTheme.GetAppsColorScheme(); + return _colorScheme.Value; + } + } + + /// + /// Cached app theme; null when it needs to be re-read. + /// + private RColorScheme? _colorScheme; + protected override RColor GetColorInt(string colorName) { // check if color name is valid to avoid ColorConverter throwing an exception @@ -102,14 +174,19 @@ protected override RBrush CreateSolidBrush(RColor color) return new BrushAdapter(solidBrush); } - protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + protected override RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops) { - var startColor = angle <= 180 ? Utils.Convert(color1) : Utils.Convert(color2); - var endColor = angle <= 180 ? Utils.Convert(color2) : Utils.Convert(color1); - angle = angle <= 180 ? angle : angle - 180; - double x = angle < 135 ? Math.Max((angle - 45) / 90, 0) : 1; - double y = angle <= 45 ? Math.Max(0.5 - angle / 90, 0) : angle > 135 ? Math.Abs(1.5 - angle / 90) : 0; - return new BrushAdapter(new LinearGradientBrush(startColor, endColor, new Point(x, y), new Point(1 - x, 1 - y))); + var gradientStops = new GradientStopCollection(stops.Length); + foreach (var stop in stops) + gradientStops.Add(new GradientStop(Utils.Convert(stop.Color), stop.Position)); + + var brush = new LinearGradientBrush(gradientStops, 0) + { + MappingMode = BrushMappingMode.Absolute, + StartPoint = Utils.Convert(p1), + EndPoint = Utils.Convert(p2) + }; + return new BrushAdapter(brush); } protected override RImage ConvertImageInt(object image) @@ -140,6 +217,47 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty return new FontAdapter(new Typeface(((FontFamilyAdapter)family).FontFamily, GetFontStyle(style), GetFontWidth(style), FontStretches.Normal), size); } + /// + /// Loads one @font-face face's bytes as a WPF + /// via a temp file and - WPF's own documented way to load + /// a font from an arbitrary location. + /// + /// + /// Two earlier approaches were tried and rejected first: the Win32 AddFontMemResourceEx API + /// registers with GDI, but WPF's text stack (DirectWrite-based) never consults GDI's per-process + /// font table, so registered faces were silently invisible to WPF. A fully in-memory + /// -scheme trick (serving the bytes from a + /// for a synthetic URI, the same mechanism that historically let WPF/XBAP + /// apps reference fonts over plain http://) was also tried and confirmed NOT to work: WPF's + /// own source documents Fonts.GetFontFamilies's location parameter as "must be an absolute + /// file URI or path" - empirically, both the eager folder-scan API and the lazy + /// new FontFamily(baseUri, "./file#Name") reference came back empty against the synthetic + /// scheme even though the handler correctly served the bytes. There is no supported WPF API for + /// loading a font from memory alone, so - like WinForms' own PrivateFontCollection.AddFontFile + /// path, for its own different reason (GDI+'s AddMemoryFont being unreliable, not a + /// fundamental API gap) - this writes to a real, never-deleted temp file. + /// + /// + /// Each face gets its OWN, never-reused temp subdirectory - not one shared directory for every + /// face. WPF's font-family folder enumeration caches its scan per directory and does not notice + /// files added to that directory after the first scan (confirmed empirically: with a single shared + /// directory, every face after the first one silently resolved back to the first face's glyphs, + /// because 's first call had already cached "what's in this + /// folder" before the later faces' files existed). A fresh, single-file directory per face sidesteps + /// that cache entirely. + /// + protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath) + { + var faceDirectory = Path.Combine(_fontFaceTempDirectory, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(faceDirectory); + var tempFilePath = Path.Combine(faceDirectory, "face.ttf"); + File.WriteAllBytes(tempFilePath, fontBytes); + + var family = Fonts.GetFontFamilies(new Uri(tempFilePath)).FirstOrDefault(); + + return family != null ? new FontFamilyAdapter(family) : null; + } + protected override object GetClipboardDataObjectInt(string html, string plainText) { return ClipboardHelper.CreateDataObject(html, plainText); diff --git a/Source/HtmlRenderer.WPF/HtmlContainer.cs b/Source/HtmlRenderer.WPF/HtmlContainer.cs index d7d37d5be..1177465d4 100644 --- a/Source/HtmlRenderer.WPF/HtmlContainer.cs +++ b/Source/HtmlRenderer.WPF/HtmlContainer.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -271,9 +272,9 @@ public void ClearSelection() /// /// the html to init with, init empty if not given /// optional: the stylesheet to init with, init default if not given - public void SetHtml(string htmlSource, CssData baseCssData = null) + public Task SetHtml(string htmlSource, CssData baseCssData = null) { - _htmlContainerInt.SetHtml(htmlSource, baseCssData); + return _htmlContainerInt.SetHtml(htmlSource, baseCssData); } /// diff --git a/Source/HtmlRenderer.WPF/HtmlControl.cs b/Source/HtmlRenderer.WPF/HtmlControl.cs index be4e241fd..ca52940bd 100644 --- a/Source/HtmlRenderer.WPF/HtmlControl.cs +++ b/Source/HtmlRenderer.WPF/HtmlControl.cs @@ -12,6 +12,8 @@ using System; using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -70,6 +72,14 @@ public class HtmlControl : Control /// protected Point _lastScrollOffset; + /// + /// Tracks the in-flight call, if any, so a newer call can supersede an + /// older one still awaiting - the stale call's post-await side + /// effects (invalidate/error reporting) are skipped once it resumes, checked by reference equality + /// against this field. + /// + private CancellationTokenSource _pendingLoad; + #endregion @@ -462,6 +472,48 @@ protected virtual void InvokeMouseMove() _htmlContainer.HandleMouseMove(this, Mouse.GetPosition(this)); } + /// + /// Sets the html of this control and awaits the async load - the real entry point behind the + /// / dependency-property callbacks, + /// for callers that want to await completion or cancel an in-flight load. + /// + /// + /// A new call before a previous one finishes supersedes it: the previous call's + /// keeps running (it has no mid-flight cancellation point of + /// its own) but its post-completion side effects - invalidate, error reporting - are discarded + /// once it resumes, so only the newest call's results ever reach the control. + /// + /// the html to set + /// optional: cancel this specific call without affecting others + public async Task SetTextAsync(string html, CancellationToken cancellationToken = default) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _pendingLoad?.Cancel(); + _pendingLoad = cts; + + try + { + await _htmlContainer.SetHtml(html, _baseCssData); + } + catch (Exception ex) + { + if (cts == _pendingLoad) + { + OnRenderError(new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Failed to set html", ex)); + } + return; + } + + if (cts != _pendingLoad || cts.IsCancellationRequested) + { + return; + } + + InvalidateMeasure(); + InvalidateVisual(); + InvokeMouseMove(); + } + /// /// Handle when dependency property value changes to update the underline HtmlContainer with the new value. /// @@ -487,15 +539,12 @@ private static void OnDependencyProperty_valueChanged(DependencyObject dependenc { var baseCssData = HtmlRender.ParseStyleSheet((string)e.NewValue); control._baseCssData = baseCssData; - htmlContainer.SetHtml(control.Text, baseCssData); + _ = control.SetTextAsync(control.Text); } else if (e.Property == TextProperty) { htmlContainer.ScrollOffset = new Point(0, 0); - htmlContainer.SetHtml((string)e.NewValue, control._baseCssData); - control.InvalidateMeasure(); - control.InvalidateVisual(); - control.InvokeMouseMove(); + _ = control.SetTextAsync((string)e.NewValue); } } } diff --git a/Source/HtmlRenderer.WPF/HtmlRender.cs b/Source/HtmlRenderer.WPF/HtmlRender.cs index d9e6c65ad..76eac2606 100644 --- a/Source/HtmlRenderer.WPF/HtmlRender.cs +++ b/Source/HtmlRenderer.WPF/HtmlRender.cs @@ -6,11 +6,12 @@ // like the days and months; // they die and are reborn, // like the four seasons." -// +// // - Sun Tsu, // "The Art of War" using System; +using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -34,9 +35,9 @@ namespace TheArtOfDev.HtmlRenderer.WPF /// See https://codeplexarchive.org/ProjectTab/Wiki/HtmlRenderer/Documentation/Image%20generation
/// Because of GDI text rendering issue with alpha channel clear type text rendering rendering to image requires special handling.
/// Solid color background - generate an image where the background is filled with solid color and all the html is rendered on top - /// of the background color, GDI text rendering will be used. (RenderToImage method where the first argument is html string)
- /// Image background - render html on top of existing image with whatever currently exist but it cannot have transparent pixels, - /// GDI text rendering will be used. (RenderToImage method where the first argument is Image object)
+ /// of the background color, GDI text rendering will be used. (RenderToImageAsync method where the first argument is html string)
+ /// Image background - render html on top of existing image with whatever currently exist but it cannot have transparent pixels, + /// GDI text rendering will be used. (RenderToImageAsync method where the first argument is Image object)
/// Transparent background - render html to empty image using GDI+ text rendering, the generated image can be transparent. /// /// @@ -54,7 +55,7 @@ namespace TheArtOfDev.HtmlRenderer.WPF /// Allows to overwrite the loaded image by providing the image object manually, or different source (file or URL) to load from.
/// Example: image 'src' can be non-valid string that is interpreted in the overwrite delegate by custom logic to resource image object
/// Example: image 'src' in the html is relative - the overwrite intercepts the load and provide full source URL to load the image from
- /// Example: image download requires authentication - the overwrite intercepts the load, downloads the image to disk using custom code and provide + /// Example: image download requires authentication - the overwrite intercepts the load, downloads the image to disk using custom code and provide /// file path to load the image from.
/// If no alternative data is provided the original source will be used.
/// Note: Cannot use asynchronous scheme overwrite scheme.
@@ -63,14 +64,14 @@ namespace TheArtOfDev.HtmlRenderer.WPF /// /// /// Simple rendering
- /// HtmlRender.Render(g, "Hello World]]>");
- /// HtmlRender.Render(g, "Hello World]]>", 10, 10, 500, CssData.Parse("body {font-size: 20px}")");
+ /// await HtmlRender.RenderAsync(g, "Hello World]]>");
+ /// await HtmlRender.RenderAsync(g, "Hello World]]>", 10, 10, 500, CssData.Parse("body {font-size: 20px}")");
///
/// /// Image rendering
- /// HtmlRender.RenderToImage("Hello World]]>", new Size(600,400));
- /// HtmlRender.RenderToImage("Hello World]]>", 600);
- /// HtmlRender.RenderToImage(existingImage, "Hello World]]>");
+ /// await HtmlRender.RenderToImageAsync("Hello World]]>", new Size(600,400));
+ /// await HtmlRender.RenderToImageAsync("Hello World]]>", 600);
+ /// await HtmlRender.RenderToImageAsync(existingImage, "Hello World]]>");
///
///
public static class HtmlRender @@ -93,7 +94,7 @@ public static void AddFontFamily(FontFamily fontFamily) /// /// Adds a font mapping from to iff the is not found.
- /// When the font is used in rendered html and is not found in existing + /// When the font is used in rendered html and is not found in existing /// fonts (installed or added) it will be replaced by .
///
/// @@ -111,7 +112,7 @@ public static void AddFontFamilyMapping(string fromFamily, string toFamily) /// /// Parse the given stylesheet to object.
- /// If is true the parsed css blocks are added to the + /// If is true the parsed css blocks are added to the /// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned. ///
/// @@ -134,7 +135,7 @@ public static CssData ParseStyleSheet(string stylesheet, bool combineWithDefault /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the size required for the html - public static Size Measure(string html, double maxWidth = 0, CssData cssData = null, + public static async Task MeasureAsync(string html, double maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { Size actualSize = Size.Empty; @@ -151,7 +152,7 @@ public static Size Measure(string html, double maxWidth = 0, CssData cssData = n if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); container.PerformLayout(); actualSize = container.ActualSize; @@ -162,7 +163,7 @@ public static Size Measure(string html, double maxWidth = 0, CssData cssData = n /// /// Renders the specified HTML source on the specified location and max width restriction.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// Returned is the actual width and height of the rendered html.
///
@@ -175,7 +176,7 @@ public static Size Measure(string html, double maxWidth = 0, CssData cssData = n /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - public static Size Render(DrawingContext g, string html, double left = 0, double top = 0, double maxWidth = 0, CssData cssData = null, + public static Task RenderAsync(DrawingContext g, string html, double left = 0, double top = 0, double maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -184,7 +185,7 @@ public static Size Render(DrawingContext g, string html, double left = 0, double /// /// Renders the specified HTML source on the specified location and max size restriction.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -198,7 +199,7 @@ public static Size Render(DrawingContext g, string html, double left = 0, double /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - public static Size Render(DrawingContext g, string html, Point location, Size maxSize, CssData cssData = null, + public static Task RenderAsync(DrawingContext g, string html, Point location, Size maxSize, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -215,7 +216,7 @@ public static Size Render(DrawingContext g, string html, Point location, Size ma /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the html - public static BitmapFrame RenderToImage(string html, Size size, CssData cssData = null, + public static async Task RenderToImageAsync(string html, Size size, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { var renderTarget = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32); @@ -226,7 +227,7 @@ public static BitmapFrame RenderToImage(string html, Size size, CssData cssData DrawingVisual drawingVisual = new DrawingVisual(); using (DrawingContext g = drawingVisual.RenderOpen()) { - RenderHtml(g, html, new Point(), size, cssData, stylesheetLoad, imageLoad); + await RenderHtml(g, html, new Point(), size, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false); } // render visual into target bitmap @@ -238,7 +239,7 @@ public static BitmapFrame RenderToImage(string html, Size size, CssData cssData /// /// Renders the specified HTML into a new image of unknown size that will be determined by max width/height and HTML layout.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -255,15 +256,15 @@ public static BitmapFrame RenderToImage(string html, Size size, CssData cssData /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the html - public static BitmapFrame RenderToImage(string html, int maxWidth = 0, int maxHeight = 0, Color backgroundColor = new Color(), CssData cssData = null, + public static Task RenderToImageAsync(string html, int maxWidth = 0, int maxHeight = 0, Color backgroundColor = new Color(), CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { - return RenderToImage(html, Size.Empty, new Size(maxWidth, maxHeight), backgroundColor, cssData, stylesheetLoad, imageLoad); + return RenderToImageAsync(html, Size.Empty, new Size(maxWidth, maxHeight), backgroundColor, cssData, stylesheetLoad, imageLoad); } /// /// Renders the specified HTML into a new image of unknown size that will be determined by min/max width/height and HTML layout.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -281,7 +282,7 @@ public static BitmapFrame RenderToImage(string html, Size size, CssData cssData /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the html - public static BitmapFrame RenderToImage(string html, Size minSize, Size maxSize, Color backgroundColor = new Color(), CssData cssData = null, + public static async Task RenderToImageAsync(string html, Size minSize, Size maxSize, Color backgroundColor = new Color(), CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { RenderTargetBitmap renderTarget; @@ -296,7 +297,7 @@ public static BitmapFrame RenderToImage(string html, Size size, CssData cssData container.StylesheetLoad += stylesheetLoad; if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); var finalSize = MeasureHtmlByRestrictions(container, minSize, maxSize); container.MaxSize = finalSize; @@ -346,7 +347,7 @@ private static Size MeasureHtmlByRestrictions(HtmlContainer htmlContainer, Size /// /// Renders the specified HTML source on the specified location and max size restriction.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -361,12 +362,12 @@ private static Size MeasureHtmlByRestrictions(HtmlContainer htmlContainer, Size /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - private static Size RenderClip(DrawingContext g, string html, Point location, Size maxSize, CssData cssData, EventHandler stylesheetLoad, EventHandler imageLoad) + private static async Task RenderClip(DrawingContext g, string html, Point location, Size maxSize, CssData cssData, EventHandler stylesheetLoad, EventHandler imageLoad) { if (maxSize.Height > 0) g.PushClip(new RectangleGeometry(new Rect(location, maxSize))); - var actualSize = RenderHtml(g, html, location, maxSize, cssData, stylesheetLoad, imageLoad); + var actualSize = await RenderHtml(g, html, location, maxSize, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false); if (maxSize.Height > 0) g.Pop(); @@ -376,7 +377,7 @@ private static Size RenderClip(DrawingContext g, string html, Point location, Si /// /// Renders the specified HTML source on the specified location and max size restriction.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -390,7 +391,7 @@ private static Size RenderClip(DrawingContext g, string html, Point location, Si /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - private static Size RenderHtml(DrawingContext g, string html, Point location, Size maxSize, CssData cssData, EventHandler stylesheetLoad, EventHandler imageLoad) + private static async Task RenderHtml(DrawingContext g, string html, Point location, Size maxSize, CssData cssData, EventHandler stylesheetLoad, EventHandler imageLoad) { Size actualSize = Size.Empty; @@ -408,7 +409,7 @@ private static Size RenderHtml(DrawingContext g, string html, Point location, Si if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); container.PerformLayout(); container.PerformPaint(g, new Rect(0, 0, double.MaxValue, double.MaxValue)); @@ -421,4 +422,4 @@ private static Size RenderHtml(DrawingContext g, string html, Point location, Si #endregion } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs b/Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs new file mode 100644 index 000000000..b46984f8d --- /dev/null +++ b/Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs @@ -0,0 +1,54 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +using Microsoft.Win32; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Utilities +{ + /// + /// Reads the Windows app theme, which is what prefers-color-scheme reports for on-screen + /// rendering. + /// + internal static class WindowsTheme + { + private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightTheme = "AppsUseLightTheme"; + + /// + /// The user's app theme, or when it cannot be determined - + /// light is the Windows default and the safer assumption for a document that declares no dark + /// styling of its own. + /// + public static RColorScheme GetAppsColorScheme() + { + try + { + using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey)) + { + if (key != null) + { + var value = key.GetValue(AppsUseLightTheme); + if (value is int) + return (int)value == 0 ? RColorScheme.Dark : RColorScheme.Light; + } + } + } + catch + { + // A locked-down or missing key just means "no preference expressed". + } + + return RColorScheme.Light; + } + } +} diff --git a/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs index 6c4bbd061..d3a7b3f2c 100644 --- a/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs +++ b/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs @@ -51,11 +51,11 @@ public override void LineTo(double x, double y) _lastPoint = new RPoint(x, y); } - public override void ArcTo(double x, double y, double size, Corner corner) + public override void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner) { - float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? size : 0)); - float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? size : 0)); - _graphicsPath.AddArc(left, top, (float)size * 2, (float)size * 2, GetStartAngle(corner), 90); + float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? radiusX : 0)); + float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? radiusY : 0)); + _graphicsPath.AddArc(left, top, (float)radiusX * 2, (float)radiusY * 2, GetStartAngle(corner), 90); _lastPoint = new RPoint(x, y); } diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs index 16bc83db8..384f93983 100644 --- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs +++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs @@ -10,12 +10,17 @@ // - Sun Tsu, // "The Art of War" +using System; using System.Drawing; using System.Drawing.Drawing2D; +using System.Drawing.Text; using System.IO; +using System.Net.Http; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Core.Network; +using TheArtOfDev.HtmlRenderer.Core.Utils; using TheArtOfDev.HtmlRenderer.WinForms.Utilities; namespace TheArtOfDev.HtmlRenderer.WinForms.Adapters @@ -27,11 +32,40 @@ internal sealed class WinFormsAdapter : RAdapter { #region Fields and Consts + // One HttpClient shared for the adapter's (process) lifetime, not one per request - `new + // HttpClient()` per call is a well-documented anti-pattern that exhausts sockets under load and + // never observes DNS changes. + // + // Declared BEFORE _instance deliberately: C# runs static field initializers in textual + // declaration order, and _instance's own initializer (`new WinFormsAdapter()`) runs the instance + // constructor immediately, which reads _sharedHttpClient on its very first line. If this field + // were declared after _instance, that read would observe _sharedHttpClient's still-default value + // (null - its own initializer hasn't run yet) and permanently capture a null HttpClient into + // NetworkLoader, since HttpClientNetworkLoader takes it as a constructor parameter, not a live + // reference to this field. (Confirmed by a real crash with this exact ordering.) + private static readonly HttpClient _sharedHttpClient = new HttpClient(); + /// /// Singleton instance of global adapter. /// private static readonly WinFormsAdapter _instance = new WinFormsAdapter(); + // Adapter-level PrivateFontCollection for @font-face-loaded faces - one collection shared for the + // adapter's (process) lifetime, growing by one family per LoadFontFaceFontInt call. + private readonly PrivateFontCollection _fontFaceCollection = new PrivateFontCollection(); + + // Backs LoadFontFaceFontInt's temp-file registration (see its own doc comment for why a temp file + // is used instead of PrivateFontCollection.AddMemoryFont) - one directory per process, cleaned up + // by the OS's normal temp-file housekeeping, not by this process. + private static readonly string _fontFaceTempDirectory = CreateFontFaceTempDirectory(); + + private static string CreateFontFaceTempDirectory() + { + var dir = Path.Combine(Path.GetTempPath(), "HtmlRenderer.FontFace." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + return dir; + } + #endregion @@ -40,6 +74,14 @@ internal sealed class WinFormsAdapter : RAdapter ///
private WinFormsAdapter() { + // Unlike the PdfSharp backend (which keeps the base RAdapter.NetworkLoader default of + // DataUriNetworkLoader-only - safer for unattended/server-side PDF generation, matching + // PeachPDF's own default), WinForms is an interactive UI backend where fetching a real + // http(s): image or stylesheet out of the box is the expected behavior. data:/file: URIs + // still resolve the same way regardless (RAdapter.GetResourceStream intercepts both before + // ever consulting NetworkLoader), so only http(s): actually reaches this loader in practice. + NetworkLoader = new HttpClientNetworkLoader(_sharedHttpClient, (Uri)null); + AddFontFamilyMapping("monospace", "Courier New"); AddFontFamilyMapping("Helvetica", "Arial"); @@ -47,6 +89,19 @@ private WinFormsAdapter() { AddFontFamily(new FontFamilyAdapter(family)); } + + Microsoft.Win32.SystemEvents.UserPreferenceChanged += (sender, e) => + { + if (e.Category != Microsoft.Win32.UserPreferenceCategory.General) return; + + // The General category covers far more than the theme, so re-read and only report a + // change if the scheme really moved - otherwise every unrelated preference change + // would force a re-cascade and repaint. + var previous = _colorScheme; + _colorScheme = null; + if (previous.HasValue && previous.Value != SystemColorScheme) + OnColorSchemeChanged(); + }; } /// @@ -57,6 +112,28 @@ public static WinFormsAdapter Instance get { return _instance; } } + /// + /// Rendering onto a Windows control, so the document should follow the user's app theme. + /// Cached and invalidated on a system preference change rather than read per query. + /// + public override RColorScheme SystemColorScheme + { + get + { + if (SystemColorSchemeOverride.HasValue) + return SystemColorSchemeOverride.Value; + if (!_colorScheme.HasValue) + _colorScheme = WindowsTheme.GetAppsColorScheme(); + return _colorScheme.Value; + } + } + + /// + /// Cached app theme; null when it needs to be re-read. + /// + private RColorScheme? _colorScheme; + + protected override RColor GetColorInt(string colorName) { var color = Color.FromName(colorName); @@ -83,9 +160,32 @@ protected override RBrush CreateSolidBrush(RColor color) return new BrushAdapter(solidBrush, false); } - protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + protected override RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops) { - return new BrushAdapter(new LinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), (float)angle), true); + var brush = new LinearGradientBrush(Utils.Convert(p1), Utils.Convert(p2), Color.Black, Color.Black); + + var colors = new Color[stops.Length]; + var positions = new float[stops.Length]; + for (int i = 0; i < stops.Length; i++) + { + colors[i] = Utils.Convert(stops[i].Color); + var pos = (float)Math.Min(Math.Max(stops[i].Position, 0.0), 1.0); + // GDI+ requires strictly increasing positions - nudge any duplicate up by an epsilon. + positions[i] = i > 0 && pos <= positions[i - 1] ? positions[i - 1] + 0.0001f : pos; + } + // GDI+ requires the first/last position to be exactly 0/1 - forcing them here (rather than + // requiring the caller to pre-normalize) also matches spec behavior for a gradient whose + // outermost stops aren't at the very ends: the outermost color simply extends flat to the edge. + positions[0] = 0f; + positions[positions.Length - 1] = 1f; + + brush.InterpolationColors = new ColorBlend + { + Colors = colors, + Positions = positions + }; + + return new BrushAdapter(brush, true); } protected override RImage ConvertImageInt(object image) @@ -110,6 +210,67 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty return new FontAdapter(new Font(((FontFamilyAdapter)family).FontFamily, (float)size, fontStyle)); } + /// + /// Loads one @font-face face's bytes into the adapter's + /// via - through a temp file, matching this + /// adapter's own pre-existing DemoForm.LoadCustomFonts pattern (that path is untouched, + /// this is a new, separate mechanism specific to @font-face). + /// + /// + /// Deliberately NOT , despite it needing no temp + /// file: empirically (a throwaway repro project registering a dozen distinct families into one + /// ), AddMemoryFont is unreliable on this target + /// framework - permanently fails to reflect several of + /// the added families (not a timing race: polling for up to 300ms after the call never finds them + /// either), while the identical sequence of files through AddFontFile succeeds 100% of the + /// time across repeated runs. This is a known-flaky area of GDI+'s AddMemoryFont P/Invoke + /// path, not a bug in this port. The temp file is deliberately never deleted: GDI+ keeps it + /// memory-mapped for as long as this process-lifetime singleton's + /// references it, and the OS's own temp-directory housekeeping reclaims it afterward - the same + /// "small, bounded, process-lifetime" rationale the removed AddMemoryFont/AllocHGlobal + /// approach relied on. + /// + /// The returned is found by sniffing the font's own internal + /// family name via and matching it against + /// - not by comparing Families.Length before + /// and after, nor by taking the array's last entry. Two bugs made that approach unreliable: (1) + /// Families groups every face by family name (the whole point of + /// - it lets pick the right face via + /// automatically), so registering a second face of an *already-registered* + /// family (e.g. this face set's own Bold after its Regular) never changes the count at all; and + /// (2) even when the count does change, Families is returned in a GDI-defined (effectively + /// alphabetical) order, not insertion order, so "the last entry" is often a completely unrelated, + /// alphabetically-later family, not the one just added. + /// + /// + protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath) + { + string familyName; + using (var stream = new MemoryStream(fontBytes)) + { + familyName = TtfFontDescription.LoadDescription(stream).FontFamilyInvariantCulture; + } + + if (string.IsNullOrEmpty(familyName)) + { + return null; + } + + var tempFilePath = Path.Combine(_fontFaceTempDirectory, Guid.NewGuid().ToString("N") + ".ttf"); + File.WriteAllBytes(tempFilePath, fontBytes); + _fontFaceCollection.AddFontFile(tempFilePath); + + foreach (var family in _fontFaceCollection.Families) + { + if (string.Equals(family.Name, familyName, StringComparison.OrdinalIgnoreCase)) + { + return new FontFamilyAdapter(family); + } + } + + return null; + } + protected override object GetClipboardDataObjectInt(string html, string plainText) { return ClipboardHelper.CreateDataObject(html, plainText); diff --git a/Source/HtmlRenderer.WinForms/HtmlContainer.cs b/Source/HtmlRenderer.WinForms/HtmlContainer.cs index 5bc7c5cd0..af7fc9040 100644 --- a/Source/HtmlRenderer.WinForms/HtmlContainer.cs +++ b/Source/HtmlRenderer.WinForms/HtmlContainer.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Text; +using System.Threading.Tasks; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core; @@ -312,9 +313,9 @@ public void ClearSelection() /// /// the html to init with, init empty if not given /// optional: the stylesheet to init with, init default if not given - public void SetHtml(string htmlSource, CssData baseCssData = null) + public Task SetHtml(string htmlSource, CssData baseCssData = null) { - _htmlContainerInt.SetHtml(htmlSource, baseCssData); + return _htmlContainerInt.SetHtml(htmlSource, baseCssData); } /// diff --git a/Source/HtmlRenderer.WinForms/HtmlLabel.cs b/Source/HtmlRenderer.WinForms/HtmlLabel.cs index 283ef7360..254566541 100644 --- a/Source/HtmlRenderer.WinForms/HtmlLabel.cs +++ b/Source/HtmlRenderer.WinForms/HtmlLabel.cs @@ -15,6 +15,8 @@ using System.Diagnostics; using System.Drawing; using System.Drawing.Text; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core; @@ -112,6 +114,14 @@ public class HtmlLabel : Control /// protected TextRenderingHint _textRenderingHint = TextRenderingHint.SystemDefault; + /// + /// Tracks the in-flight call, if any, so a newer call can supersede an + /// older one still awaiting - the stale call's post-await side + /// effects (layout/invalidate/error reporting) are skipped once it resumes, checked by reference + /// equality against this field. + /// + private CancellationTokenSource _pendingLoad; + #endregion @@ -304,7 +314,7 @@ public virtual string BaseStylesheet { _baseRawCssData = value; _baseCssData = HtmlRender.ParseStyleSheet(value); - _htmlContainer.SetHtml(_text, _baseCssData); + _ = SetTextAsync(_text); } } @@ -397,11 +407,50 @@ public override string Text base.Text = value; if (!IsDisposed) { - _htmlContainer.SetHtml(_text, _baseCssData); - PerformLayout(); - Invalidate(); + _ = SetTextAsync(_text); + } + } + } + + /// + /// Sets the html of this control and awaits the async load - the real entry point behind the + /// / property setters, for callers that want to + /// await completion or cancel an in-flight load. + /// + /// + /// A new call before a previous one finishes supersedes it: the previous call's + /// keeps running (it has no mid-flight cancellation point of + /// its own) but its post-completion side effects - layout, invalidate, error reporting - are + /// discarded once it resumes, so only the newest call's results ever reach the control. + /// + /// the html to set + /// optional: cancel this specific call without affecting others + public async Task SetTextAsync(string html, CancellationToken cancellationToken = default) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _pendingLoad?.Cancel(); + _pendingLoad = cts; + + try + { + await _htmlContainer.SetHtml(html, _baseCssData); + } + catch (Exception ex) + { + if (cts == _pendingLoad) + { + OnRenderError(this, new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Failed to set html", ex)); } + return; + } + + if (cts != _pendingLoad || cts.IsCancellationRequested || IsDisposed) + { + return; } + + PerformLayout(); + Invalidate(); } /// @@ -677,6 +726,7 @@ protected override void WndProc(ref Message m) /// protected override void Dispose(bool disposing) { + _pendingLoad?.Cancel(); if (_htmlContainer != null) { _htmlContainer.LoadComplete -= OnLoadComplete; diff --git a/Source/HtmlRenderer.WinForms/HtmlPanel.cs b/Source/HtmlRenderer.WinForms/HtmlPanel.cs index a2f061874..9d150fd19 100644 --- a/Source/HtmlRenderer.WinForms/HtmlPanel.cs +++ b/Source/HtmlRenderer.WinForms/HtmlPanel.cs @@ -15,6 +15,8 @@ using System.Diagnostics; using System.Drawing; using System.Drawing.Text; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; @@ -104,6 +106,14 @@ public class HtmlPanel : ScrollableControl ///
protected Point _lastScrollOffset; + /// + /// Tracks the in-flight call, if any, so a newer call can supersede an + /// older one still awaiting - the stale call's post-await side + /// effects (layout/invalidate/error reporting) are skipped once it resumes, checked by reference + /// equality against this field. + /// + private CancellationTokenSource _pendingLoad; + #endregion @@ -312,7 +322,7 @@ public virtual string BaseStylesheet { _baseRawCssData = value; _baseCssData = HtmlRender.ParseStyleSheet(value); - _htmlContainer.SetHtml(_text, _baseCssData); + _ = SetTextAsync(_text); } } @@ -342,12 +352,53 @@ public override string Text if (!IsDisposed) { VerticalScroll.Value = VerticalScroll.Minimum; - _htmlContainer.SetHtml(_text, _baseCssData); - PerformLayout(); - Invalidate(); - InvokeMouseMove(); + _ = SetTextAsync(_text); + } + } + } + + /// + /// Sets the html of this control and awaits the async load - the real entry point behind the + /// / property setters, for callers that want to + /// await completion or cancel an in-flight load. + /// + /// + /// A new call before a previous one finishes supersedes it: the previous call's + /// keeps running (it has no mid-flight cancellation point of + /// its own) but its post-completion side effects - layout, invalidate, error reporting - are + /// discarded once it resumes, so only the newest call's results ever reach the control. + /// + /// the html to set + /// optional: cancel this specific call without affecting others + public async Task SetTextAsync(string html, CancellationToken cancellationToken = default) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _pendingLoad?.Cancel(); + _pendingLoad = cts; + + try + { + await _htmlContainer.SetHtml(html, _baseCssData); + } + catch (Exception ex) + { + if (cts == _pendingLoad) + { + OnRenderError(this, new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Failed to set html", ex)); } + return; + } + + // Superseded by a newer SetTextAsync/Text/BaseStylesheet call while this one was awaiting - + // that call's own results are what the control should reflect, not this stale one's. + if (cts != _pendingLoad || cts.IsCancellationRequested || IsDisposed) + { + return; } + + PerformLayout(); + Invalidate(); + InvokeMouseMove(); } /// @@ -764,6 +815,7 @@ protected override void WndProc(ref Message m) /// protected override void Dispose(bool disposing) { + _pendingLoad?.Cancel(); if (_htmlContainer != null) { _htmlContainer.LoadComplete -= OnLoadComplete; diff --git a/Source/HtmlRenderer.WinForms/HtmlRender.cs b/Source/HtmlRenderer.WinForms/HtmlRender.cs index ca0f44532..cd15e2f47 100644 --- a/Source/HtmlRenderer.WinForms/HtmlRender.cs +++ b/Source/HtmlRenderer.WinForms/HtmlRender.cs @@ -6,7 +6,7 @@ // like the days and months; // they die and are reborn, // like the four seasons." -// +// // - Sun Tsu, // "The Art of War" @@ -15,6 +15,7 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; +using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Utils; @@ -31,8 +32,8 @@ namespace TheArtOfDev.HtmlRenderer.WinForms /// /// /// GDI vs. GDI+ text rendering
- /// Windows supports two text rendering technologies: GDI and GDI+.
- /// GDI is older, has better performance and looks better on standard monitors but doesn't support alpha channel for transparency.
+ /// Windows supports two text rendering technologies: GDI and GDI+.
+ /// GDI is older, has better performance and looks better on standard monitors but doesn't support alpha channel for transparency.
/// GDI+ is newer, device independent so work better for printers but is slower and looks worse on monitors.
/// HtmlRender supports both GDI and GDI+ text rendering to accommodate different needs, GDI+ text rendering methods have "GdiPlus" suffix /// in their name where GDI do not.
@@ -42,12 +43,12 @@ namespace TheArtOfDev.HtmlRenderer.WinForms /// See https://codeplexarchive.org/ProjectTab/Wiki/HtmlRenderer/Documentation/Image%20generation
/// Because of GDI text rendering issue with alpha channel clear type text rendering rendering to image requires special handling.
/// Solid color background - generate an image where the background is filled with solid color and all the html is rendered on top - /// of the background color, GDI text rendering will be used. (RenderToImage method where the first argument is html string)
- /// Image background - render html on top of existing image with whatever currently exist but it cannot have transparent pixels, - /// GDI text rendering will be used. (RenderToImage method where the first argument is Image object)
+ /// of the background color, GDI text rendering will be used. (RenderToImageAsync method where the first argument is html string)
+ /// Image background - render html on top of existing image with whatever currently exist but it cannot have transparent pixels, + /// GDI text rendering will be used. (RenderToImageAsync method where the first argument is Image object)
/// Transparent background - render html to empty image using GDI+ text rendering, the generated image can be transparent. /// Text rendering can be controlled using , note that - /// doesn't render well on transparent background. (RenderToImageGdiPlus method)
+ /// doesn't render well on transparent background. (RenderToImageGdiPlusAsync method)
///
/// /// Overwrite stylesheet resolution
@@ -64,7 +65,7 @@ namespace TheArtOfDev.HtmlRenderer.WinForms /// Allows to overwrite the loaded image by providing the image object manually, or different source (file or URL) to load from.
/// Example: image 'src' can be non-valid string that is interpreted in the overwrite delegate by custom logic to resource image object
/// Example: image 'src' in the html is relative - the overwrite intercepts the load and provide full source URL to load the image from
- /// Example: image download requires authentication - the overwrite intercepts the load, downloads the image to disk using custom code and provide + /// Example: image download requires authentication - the overwrite intercepts the load, downloads the image to disk using custom code and provide /// file path to load the image from.
/// If no alternative data is provided the original source will be used.
/// Note: Cannot use asynchronous scheme overwrite scheme.
@@ -73,14 +74,14 @@ namespace TheArtOfDev.HtmlRenderer.WinForms /// /// /// Simple rendering
- /// HtmlRender.Render(g, "Hello World]]>");
- /// HtmlRender.Render(g, "Hello World]]>", 10, 10, 500, CssData.Parse("body {font-size: 20px}")");
+ /// await HtmlRender.RenderAsync(g, "Hello World]]>");
+ /// await HtmlRender.RenderAsync(g, "Hello World]]>", 10, 10, 500, CssData.Parse("body {font-size: 20px}")");
///
/// /// Image rendering
- /// HtmlRender.RenderToImage("Hello World]]>", new Size(600,400));
- /// HtmlRender.RenderToImage("Hello World]]>", 600);
- /// HtmlRender.RenderToImage(existingImage, "Hello World]]>");
+ /// await HtmlRender.RenderToImageAsync("Hello World]]>", new Size(600,400));
+ /// await HtmlRender.RenderToImageAsync("Hello World]]>", 600);
+ /// await HtmlRender.RenderToImageAsync(existingImage, "Hello World]]>");
///
///
public static class HtmlRender @@ -104,7 +105,7 @@ public static void AddFontFamily(FontFamily fontFamily) /// /// Adds a font mapping from to iff the is not found.
- /// When the font is used in rendered html and is not found in existing + /// When the font is used in rendered html and is not found in existing /// fonts (installed or added) it will be replaced by .
///
/// @@ -122,7 +123,7 @@ public static void AddFontFamilyMapping(string fromFamily, string toFamily) /// /// Parse the given stylesheet to object.
- /// If is true the parsed css blocks are added to the + /// If is true the parsed css blocks are added to the /// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned. ///
/// @@ -147,7 +148,7 @@ public static CssData ParseStyleSheet(string stylesheet, bool combineWithDefault /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the size required for the html - public static SizeF Measure(Graphics g, string html, float maxWidth = 0, CssData cssData = null, + public static Task MeasureAsync(Graphics g, string html, float maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -167,7 +168,7 @@ public static SizeF Measure(Graphics g, string html, float maxWidth = 0, CssData /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the size required for the html - public static SizeF MeasureGdiPlus(Graphics g, string html, float maxWidth = 0, CssData cssData = null, + public static Task MeasureGdiPlusAsync(Graphics g, string html, float maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -177,7 +178,7 @@ public static SizeF MeasureGdiPlus(Graphics g, string html, float maxWidth = 0, /// /// Renders the specified HTML source on the specified location and max width restriction.
/// Use GDI text rendering, note has no effect.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// Returned is the actual width and height of the rendered html.
///
@@ -190,7 +191,7 @@ public static SizeF MeasureGdiPlus(Graphics g, string html, float maxWidth = 0, /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - public static SizeF Render(Graphics g, string html, float left = 0, float top = 0, float maxWidth = 0, CssData cssData = null, + public static Task RenderAsync(Graphics g, string html, float left = 0, float top = 0, float maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -200,7 +201,7 @@ public static SizeF Render(Graphics g, string html, float left = 0, float top = /// /// Renders the specified HTML source on the specified location and max size restriction.
/// Use GDI text rendering, note has no effect.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -214,7 +215,7 @@ public static SizeF Render(Graphics g, string html, float left = 0, float top = /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - public static SizeF Render(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData = null, + public static Task RenderAsync(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -224,7 +225,7 @@ public static SizeF Render(Graphics g, string html, PointF location, SizeF maxSi /// /// Renders the specified HTML source on the specified location and max size restriction.
/// Use GDI+ text rending, use to control text rendering.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// Returned is the actual width and height of the rendered html.
///
@@ -237,7 +238,7 @@ public static SizeF Render(Graphics g, string html, PointF location, SizeF maxSi /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - public static SizeF RenderGdiPlus(Graphics g, string html, float left = 0, float top = 0, float maxWidth = 0, CssData cssData = null, + public static Task RenderGdiPlusAsync(Graphics g, string html, float left = 0, float top = 0, float maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); @@ -247,7 +248,7 @@ public static SizeF RenderGdiPlus(Graphics g, string html, float left = 0, float /// /// Renders the specified HTML source on the specified location and max size restriction.
/// Use GDI+ text rending, use to control text rendering.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -261,14 +262,14 @@ public static SizeF RenderGdiPlus(Graphics g, string html, float left = 0, float /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - public static SizeF RenderGdiPlus(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData = null, + public static Task RenderGdiPlusAsync(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(g, "g"); return RenderClip(g, html, location, maxSize, cssData, true, stylesheetLoad, imageLoad); } - public static Metafile RenderToMetafile(string html, float left = 0, float top = 0, float maxWidth = 0, CssData cssData = null, + public static async Task RenderToMetafileAsync(string html, float left = 0, float top = 0, float maxWidth = 0, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { Metafile image; @@ -280,7 +281,7 @@ public static Metafile RenderToMetafile(string html, float left = 0, float top = using (var g = Graphics.FromImage(image)) { - Render(g, html, left, top, maxWidth, cssData, stylesheetLoad, imageLoad); + await RenderAsync(g, html, left, top, maxWidth, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false); } } finally @@ -303,12 +304,12 @@ public static Metafile RenderToMetafile(string html, float left = 0, float top = /// optional: the style to use for html rendering (default - use W3 default style) /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic - public static void RenderToImage(Image image, string html, PointF location = new PointF(), CssData cssData = null, + public static Task RenderToImageAsync(Image image, string html, PointF location = new PointF(), CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(image, "image"); var maxSize = new SizeF(image.Size.Width - location.X, image.Size.Height - location.Y); - RenderToImage(image, html, location, maxSize, cssData, stylesheetLoad, imageLoad); + return RenderToImageAsync(image, html, location, maxSize, cssData, stylesheetLoad, imageLoad); } /// @@ -324,7 +325,7 @@ public static Metafile RenderToMetafile(string html, float left = 0, float top = /// optional: the style to use for html rendering (default - use W3 default style) /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic - public static void RenderToImage(Image image, string html, PointF location, SizeF maxSize, CssData cssData = null, + public static async Task RenderToImageAsync(Image image, string html, PointF location, SizeF maxSize, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { ArgChecker.AssertArgNotNull(image, "image"); @@ -343,7 +344,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size memoryGraphics.DrawImageUnscaled(image, 0, 0); // render HTML into the memory buffer - RenderHtml(memoryGraphics, html, location, maxSize, cssData, false, stylesheetLoad, imageLoad); + await RenderHtml(memoryGraphics, html, location, maxSize, cssData, false, stylesheetLoad, imageLoad).ConfigureAwait(false); } // copy from memory buffer to image @@ -372,7 +373,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size /// optional: can be used to overwrite image resolution logic /// the generated image of the html /// if is . - public static Image RenderToImage(string html, Size size, Color backgroundColor = new Color(), CssData cssData = null, + public static async Task RenderToImageAsync(string html, Size size, Color backgroundColor = new Color(), CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { if (backgroundColor == Color.Transparent) @@ -394,7 +395,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size memoryGraphics.Clear(backgroundColor != Color.Empty ? backgroundColor : Color.White); // render HTML into the memory buffer - RenderHtml(memoryGraphics, html, PointF.Empty, size, cssData, true, stylesheetLoad, imageLoad); + await RenderHtml(memoryGraphics, html, PointF.Empty, size, cssData, true, stylesheetLoad, imageLoad).ConfigureAwait(false); } // copy from memory buffer to image @@ -411,7 +412,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size /// /// Renders the specified HTML into a new image of unknown size that will be determined by max width/height and HTML layout.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -429,15 +430,15 @@ public static void RenderToImage(Image image, string html, PointF location, Size /// optional: can be used to overwrite image resolution logic /// the generated image of the html /// if is . - public static Image RenderToImage(string html, int maxWidth = 0, int maxHeight = 0, Color backgroundColor = new Color(), CssData cssData = null, + public static Task RenderToImageAsync(string html, int maxWidth = 0, int maxHeight = 0, Color backgroundColor = new Color(), CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { - return RenderToImage(html, Size.Empty, new Size(maxWidth, maxHeight), backgroundColor, cssData, stylesheetLoad, imageLoad); + return RenderToImageAsync(html, Size.Empty, new Size(maxWidth, maxHeight), backgroundColor, cssData, stylesheetLoad, imageLoad); } /// /// Renders the specified HTML into a new image of unknown size that will be determined by min/max width/height and HTML layout.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -456,7 +457,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size /// optional: can be used to overwrite image resolution logic /// the generated image of the html /// if is . - public static Image RenderToImage(string html, Size minSize, Size maxSize, Color backgroundColor = new Color(), CssData cssData = null, + public static async Task RenderToImageAsync(string html, Size minSize, Size maxSize, Color backgroundColor = new Color(), CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { if (backgroundColor == Color.Transparent) @@ -474,7 +475,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size container.StylesheetLoad += stylesheetLoad; if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); var finalSize = MeasureHtmlByRestrictions(container, minSize, maxSize); container.MaxSize = finalSize; @@ -520,7 +521,7 @@ public static void RenderToImage(Image image, string html, PointF location, Size /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the html - public static Image RenderToImageGdiPlus(string html, Size size, TextRenderingHint textRenderingHint = TextRenderingHint.AntiAlias, CssData cssData = null, + public static async Task RenderToImageGdiPlusAsync(string html, Size size, TextRenderingHint textRenderingHint = TextRenderingHint.AntiAlias, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { var image = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppArgb); @@ -528,7 +529,7 @@ public static Image RenderToImageGdiPlus(string html, Size size, TextRenderingHi using (var g = Graphics.FromImage(image)) { g.TextRenderingHint = textRenderingHint; - RenderHtml(g, html, PointF.Empty, size, cssData, true, stylesheetLoad, imageLoad); + await RenderHtml(g, html, PointF.Empty, size, cssData, true, stylesheetLoad, imageLoad).ConfigureAwait(false); } return image; @@ -536,7 +537,7 @@ public static Image RenderToImageGdiPlus(string html, Size size, TextRenderingHi /// /// Renders the specified HTML into a new image of unknown size that will be determined by max width/height and HTML layout.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -552,15 +553,15 @@ public static Image RenderToImageGdiPlus(string html, Size size, TextRenderingHi /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the html - public static Image RenderToImageGdiPlus(string html, int maxWidth = 0, int maxHeight = 0, TextRenderingHint textRenderingHint = TextRenderingHint.AntiAlias, CssData cssData = null, + public static Task RenderToImageGdiPlusAsync(string html, int maxWidth = 0, int maxHeight = 0, TextRenderingHint textRenderingHint = TextRenderingHint.AntiAlias, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { - return RenderToImageGdiPlus(html, Size.Empty, new Size(maxWidth, maxHeight), textRenderingHint, cssData, stylesheetLoad, imageLoad); + return RenderToImageGdiPlusAsync(html, Size.Empty, new Size(maxWidth, maxHeight), textRenderingHint, cssData, stylesheetLoad, imageLoad); } /// /// Renders the specified HTML into a new image of unknown size that will be determined by min/max width/height and HTML layout.
- /// If is zero the html will use all the required width, otherwise it will perform line + /// If is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -577,7 +578,7 @@ public static Image RenderToImageGdiPlus(string html, int maxWidth = 0, int maxH /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the generated image of the html - public static Image RenderToImageGdiPlus(string html, Size minSize, Size maxSize, TextRenderingHint textRenderingHint = TextRenderingHint.AntiAlias, CssData cssData = null, + public static async Task RenderToImageGdiPlusAsync(string html, Size minSize, Size maxSize, TextRenderingHint textRenderingHint = TextRenderingHint.AntiAlias, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) { if (string.IsNullOrEmpty(html)) @@ -593,7 +594,7 @@ public static Image RenderToImageGdiPlus(string html, Size minSize, Size maxSize container.StylesheetLoad += stylesheetLoad; if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); var finalSize = MeasureHtmlByRestrictions(container, minSize, maxSize); container.MaxSize = finalSize; @@ -626,7 +627,7 @@ public static Image RenderToImageGdiPlus(string html, Size minSize, Size maxSize /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the size required for the html - private static SizeF Measure(Graphics g, string html, float maxWidth, CssData cssData, bool useGdiPlusTextRendering, + private static async Task Measure(Graphics g, string html, float maxWidth, CssData cssData, bool useGdiPlusTextRendering, EventHandler stylesheetLoad, EventHandler imageLoad) { SizeF actualSize = SizeF.Empty; @@ -644,7 +645,7 @@ private static SizeF Measure(Graphics g, string html, float maxWidth, CssData cs if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); container.PerformLayout(g); actualSize = container.ActualSize; @@ -675,7 +676,7 @@ private static Size MeasureHtmlByRestrictions(HtmlContainer htmlContainer, Size /// /// Renders the specified HTML source on the specified location and max size restriction.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -691,7 +692,7 @@ private static Size MeasureHtmlByRestrictions(HtmlContainer htmlContainer, Size /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - private static SizeF RenderClip(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData, bool useGdiPlusTextRendering, EventHandler stylesheetLoad, EventHandler imageLoad) + private static async Task RenderClip(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData, bool useGdiPlusTextRendering, EventHandler stylesheetLoad, EventHandler imageLoad) { Region prevClip = null; if (maxSize.Height > 0) @@ -700,7 +701,7 @@ private static SizeF RenderClip(Graphics g, string html, PointF location, SizeF g.SetClip(new RectangleF(location, maxSize)); } - var actualSize = RenderHtml(g, html, location, maxSize, cssData, useGdiPlusTextRendering, stylesheetLoad, imageLoad); + var actualSize = await RenderHtml(g, html, location, maxSize, cssData, useGdiPlusTextRendering, stylesheetLoad, imageLoad).ConfigureAwait(false); if (prevClip != null) { @@ -712,7 +713,7 @@ private static SizeF RenderClip(Graphics g, string html, PointF location, SizeF /// /// Renders the specified HTML source on the specified location and max size restriction.
- /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// If .Width is zero the html will use all the required width, otherwise it will perform line /// wrap as specified in the html
/// If .Height is zero the html will use all the required height, otherwise it will clip at the /// given max height not rendering the html below it.
@@ -727,7 +728,7 @@ private static SizeF RenderClip(Graphics g, string html, PointF location, SizeF /// optional: can be used to overwrite stylesheet resolution logic /// optional: can be used to overwrite image resolution logic /// the actual size of the rendered html - private static SizeF RenderHtml(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData, bool useGdiPlusTextRendering, EventHandler stylesheetLoad, EventHandler imageLoad) + private static async Task RenderHtml(Graphics g, string html, PointF location, SizeF maxSize, CssData cssData, bool useGdiPlusTextRendering, EventHandler stylesheetLoad, EventHandler imageLoad) { SizeF actualSize = SizeF.Empty; @@ -746,7 +747,7 @@ private static SizeF RenderHtml(Graphics g, string html, PointF location, SizeF if (imageLoad != null) container.ImageLoad += imageLoad; - container.SetHtml(html, cssData); + await container.SetHtml(html, cssData).ConfigureAwait(false); container.PerformLayout(g); container.PerformPaint(g); @@ -774,4 +775,4 @@ private static void CopyBufferToImage(IntPtr memoryHdc, Image image) #endregion } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer.WinForms/HtmlToolTip.cs b/Source/HtmlRenderer.WinForms/HtmlToolTip.cs index 2a82209bd..9b6bb1eb8 100644 --- a/Source/HtmlRenderer.WinForms/HtmlToolTip.cs +++ b/Source/HtmlRenderer.WinForms/HtmlToolTip.cs @@ -241,7 +241,11 @@ protected virtual void OnToolTipPopup(PopupEventArgs e) //Create fragment container var cssClass = string.IsNullOrEmpty(_tooltipCssClass) ? null : string.Format(" class=\"{0}\"", _tooltipCssClass); var toolipHtml = string.Format("{1}", cssClass, GetToolTip(e.AssociatedControl)); - _htmlContainer.SetHtml(toolipHtml, _baseCssData); + // Bridges into the async HtmlContainer.SetHtml from this synchronous event handler. + // Deliberately, permanently blocking: WinForms' ToolTip.Popup is a synchronous framework + // callback with no async variant - e.ToolTipSize must be set before this method returns or + // the tooltip won't position correctly, so there is no way to make this fire-and-forget. + _htmlContainer.SetHtml(toolipHtml, _baseCssData).GetAwaiter().GetResult(); _htmlContainer.MaxSize = MaximumSize; //Measure size of the container diff --git a/Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs b/Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs new file mode 100644 index 000000000..e3892153c --- /dev/null +++ b/Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs @@ -0,0 +1,54 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +using Microsoft.Win32; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WinForms.Utilities +{ + /// + /// Reads the Windows app theme, which is what prefers-color-scheme reports for on-screen + /// rendering. + /// + internal static class WindowsTheme + { + private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightTheme = "AppsUseLightTheme"; + + /// + /// The user's app theme, or when it cannot be determined - + /// light is the Windows default and the safer assumption for a document that declares no dark + /// styling of its own. + /// + public static RColorScheme GetAppsColorScheme() + { + try + { + using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey)) + { + if (key != null) + { + var value = key.GetValue(AppsUseLightTheme); + if (value is int) + return (int)value == 0 ? RColorScheme.Dark : RColorScheme.Light; + } + } + } + catch + { + // A locked-down or missing key just means "no preference expressed". + } + + return RColorScheme.Light; + } + } +} diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs index 6b13459c1..6632f6942 100644 --- a/Source/HtmlRenderer/Adapters/RAdapter.cs +++ b/Source/HtmlRenderer/Adapters/RAdapter.cs @@ -14,10 +14,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Handlers; +using TheArtOfDev.HtmlRenderer.Core.Network; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Adapters @@ -54,6 +57,16 @@ public abstract class RAdapter ///
private readonly FontsHandler _fontsHandler; + /// + /// Dedup cache for , keyed by the resolved resource's absolute URI - since + /// the orchestrator re-runs @font-face registration on every SetHtml and this adapter + /// is a process-wide singleton, re-registering the same face's bytes with the platform text engine + /// on every call would leak native font handles (WinForms' PrivateFontCollection.AddMemoryFont/ + /// WPF's AddFontMemResourceEx both register a *new* resource each call, even for identical + /// bytes - neither is idempotent). + /// + private readonly Dictionary _fontFaceCache = new Dictionary(); + /// /// default CSS parsed data singleton /// @@ -80,12 +93,158 @@ protected RAdapter() _fontsHandler = new FontsHandler(this); } + /// + /// The CSS media type this adapter renders for, used to evaluate @media queries - + /// "screen" for on-screen adapters (the default), "print" for paged output. + /// + public virtual string DefaultMediaType + { + get { return "screen"; } + } + + /// + /// The colour scheme the rendering surface presents, answering the prefers-color-scheme + /// media feature. Defaults to ; an adapter that renders onto a + /// themed surface should report the system setting instead, and raise + /// when it changes. + /// + public virtual RColorScheme SystemColorScheme + { + get { return RColorScheme.Light; } + } + + /// + /// Test-only seam: when set, an adapter that would otherwise report a live system theme (WinForms/ + /// WPF) returns this value from instead of querying the OS. Image- + /// regression tests render against a fixed baseline and must not depend on the host machine's own + /// theme setting - without this, a CI runner whose theme differs from whatever machine captured the + /// baseline fails a prefers-color-scheme sample for reasons unrelated to any real rendering + /// regression. Production code must never set this. + /// + internal static RColorScheme? SystemColorSchemeOverride { get; set; } + + /// + /// Raised when has changed, so anything rendered against it can + /// re-evaluate its prefers-color-scheme rules and repaint. Never raised by an adapter + /// whose scheme is fixed. + /// + /// + /// Handlers are held for the lifetime of the adapter, which is typically a process-wide + /// singleton, so a subscriber must unsubscribe when it is disposed. + /// + public event EventHandler ColorSchemeChanged; + + /// + /// Raises . For adapters that track a system theme. + /// + protected void OnColorSchemeChanged() + { + var handler = ColorSchemeChanged; + if (handler != null) + handler(this, EventArgs.Empty); + } + + /// + /// Controls how the root HTML document and every external resource it references (stylesheets, + /// images, @font-face fonts) is loaded. Defaults to , + /// which only resolves data: URIs — set this to a or + /// (or a custom ) to + /// enable loading from local files or over HTTP(S). + /// + public RNetworkLoader NetworkLoader { get; set; } = new DataUriNetworkLoader(); + + /// + /// Whether file: resource requests are honored. Checked ahead of, and independently from, + /// whether happens to be a — a + /// false value refuses local file access even then. Defaults to true. + /// + public bool AllowLocalFileAccess { get; set; } = true; + + // Serves file: URIs (and supplies the default working-directory base URI) whenever the configured + // NetworkLoader isn't itself a FileUriNetworkLoader - mirroring how data: URIs are always handled + // internally regardless of which loader is configured. Lazily created so its + // Directory.GetCurrentDirectory() snapshot isn't taken until a file: resource (or the fallback base + // URI) is actually needed. + private FileUriNetworkLoader _internalFileLoader; + private FileUriNetworkLoader InternalFileLoader => _internalFileLoader ?? (_internalFileLoader = new FileUriNetworkLoader()); + + // Serves embedded: URIs unconditionally, the same way data:/file: are always handled internally + // regardless of which loader is configured - stateless (the target assembly is named in the URI + // itself, see EmbeddedResourceNetworkLoader), so a single shared instance needs no lazy init. + private static readonly EmbeddedResourceNetworkLoader InternalEmbeddedResourceLoader = new EmbeddedResourceNetworkLoader(); + + /// + /// The document's base URL, used to resolve relative href/src/CSS url() + /// references that have no closer <base href> element to resolve against. Sourced from + /// 's own base URI when it has one; otherwise (e.g. the default + /// , which has none) falls back to the current working directory + /// as a file: URI when local file access is allowed, or null when it is not. + /// + public RUri BaseUri => NetworkLoader.BaseUri ?? (AllowLocalFileAccess ? InternalFileLoader.BaseUri : null); + + /// + /// Resolve an external resource (a stylesheet, image, or @font-face font) referenced by the + /// document, dispatching by URI scheme: data:, file:, and embedded: always + /// resolve internally (file: refused outright when is + /// false), every other scheme goes to the configured . + /// + /// the resource URI, already resolved to absolute against or a <base href> element + /// the resolved resource, or null if it could not be resolved + public Task GetResourceStream(RUri uri) + { + // BaseUri is normally never null, so every reference resolves to an absolute URI and loaders + // only ever see those - RUri.Scheme throws on a relative URI, and neither DataUriNetworkLoader + // nor HttpClientNetworkLoader checks. Denying local file access is what makes BaseUri nullable, + // so a relative reference can now survive resolution; answer "unresolved" for it here rather + // than handing a loader a URI it cannot inspect. + if (!uri.IsAbsoluteUri) + { + return Task.FromResult(null); + } + + if (uri.Scheme == "data") + { + var dataLoader = NetworkLoader as DataUriNetworkLoader ?? new DataUriNetworkLoader(); + return dataLoader.GetResourceStream(uri); + } + + if (uri.Scheme == "file") + { + // Checked ahead of the configured loader, so a deny holds even when that loader is itself a + // FileUriNetworkLoader - the two settings contradict each other, and refusing is the safe read. + if (!AllowLocalFileAccess) + { + return Task.FromResult(null); + } + + var fileLoader = NetworkLoader as FileUriNetworkLoader ?? InternalFileLoader; + return fileLoader.GetResourceStream(uri); + } + + if (uri.Scheme == EmbeddedResourceNetworkLoader.Scheme) + { + var embeddedLoader = NetworkLoader as EmbeddedResourceNetworkLoader ?? InternalEmbeddedResourceLoader; + return embeddedLoader.GetResourceStream(uri); + } + + return NetworkLoader.GetResourceStream(uri); + } + /// /// Get the default CSS stylesheet data. /// public CssData DefaultCssData { - get { return _defaultCssData ?? (_defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false)); } + get + { + if (_defaultCssData == null) + { + _defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false); + foreach (var stylesheet in _defaultCssData.Stylesheets) + stylesheet.IsUserAgent = true; + } + return _defaultCssData; + } } /// @@ -130,16 +289,15 @@ public RBrush GetSolidBrush(RColor color) } /// - /// Get linear gradient color brush from to . + /// Get a multi-stop linear gradient brush along the line from to . /// - /// the rectangle to get the brush for - /// the start color of the gradient - /// the end color of the gradient - /// the angle to move the gradient from start color to end color in the rectangle + /// the gradient line's start point + /// the gradient line's end point + /// color stops, each with a position in [0,1] along the gradient line /// linear gradient color brush instance - public RBrush GetLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + public RBrush GetLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops) { - return CreateLinearGradientBrush(rect, color1, color2, angle); + return CreateLinearGradientBrush(p1, p2, stops); } /// @@ -164,11 +322,20 @@ public RImage ImageFromStream(Stream memoryStream) } /// - /// Check if the given font exists in the system by font family name. + /// Check if the given font exists in the system by font family name. Consulted by + /// to decide whether a font-family + /// candidate should be kept as-is or the next fallback (ultimately ) + /// tried instead - so this must recognize a family the moment it's usable, including one + /// registered via , not just /system fonts. /// /// the font name to check /// true - font exists by given family name, false - otherwise - public bool IsFontExists(string font) + /// + /// Virtual so the PdfSharp backend can override it - its @font-face registrations live + /// entirely in its own FontResolver (see 's doc comment for why), + /// invisible to the shared FontsHandler this base implementation checks. + /// + public virtual bool IsFontExists(string font) { return _fontsHandler.IsFontExists(font); } @@ -194,6 +361,91 @@ public void AddFontFamilyMapping(string fromFamily, string toFamily) _fontsHandler.AddFontFamilyMapping(fromFamily, toFamily); } + /// + /// Loads one @font-face src: url(...) candidate and registers it as a face of + /// for CSS Fonts Level 4 matching. Resolves the resource through + /// - the same funnel used for images/stylesheets, so this supports + /// file:/data:/http(s): uniformly - loads the platform-specific face via + /// , and registers it with . + /// + /// the CSS family name declared by the @font-face rule + /// the resolved, absolute src URI to fetch (already resolved against the document base/a stylesheet's own location by the caller) + /// CSS Fonts Level 4 numeric weight (1-1000) this face matches for + /// whether this face matches an italic/oblique request + /// CSS Fonts Level 3 stretch (1-9) this face matches for + /// the face's unicode-range restriction, or null for "covers whatever is asked of it" + /// true if the face was loaded and registered, false if the resource could not be resolved/loaded (the caller tries the next src candidate) + /// + /// Virtual so the PdfSharp backend can override it to bypass 's shared + /// registry entirely and register directly with its own FontResolver instead - PDFsharp + /// needs raw font bytes at PDF-generation time for embedding (via its own richer + /// IFontResolver-based CSS-Fonts-L4 matching), unlike WinForms/WPF, which just need an + /// opaque platform font-family handle. See the base implementation's own doc comment for the + /// shared-registry path this overrides. + /// + public virtual async Task AddFontFace(string familyName, RUri uri, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + RFontFamily fontFamily; + if (!_fontFaceCache.TryGetValue(uri.AbsoluteUri, out fontFamily)) + { + var networkResponse = await GetResourceStream(uri).ConfigureAwait(false); + if (networkResponse == null || networkResponse.ResourceStream == null) + { + return false; + } + + byte[] fontBytes; + using (var memoryStream = new MemoryStream()) + { + using (networkResponse.ResourceStream) + { + networkResponse.ResourceStream.CopyTo(memoryStream); + } + fontBytes = memoryStream.ToArray(); + } + + try + { + fontFamily = LoadFontFaceFontInt(fontBytes, uri.AbsoluteUri); + } + catch + { + return false; + } + + if (fontFamily == null) + { + return false; + } + + _fontFaceCache[uri.AbsoluteUri] = fontFamily; + } + + _fontsHandler.AddFontFace(familyName, fontFamily, weight, isItalic, stretch, ranges); + return true; + } + + /// + /// Satisfies an @font-face src: local(...) candidate: rather than fetching a resource + /// at all, this looks for a family already registered under (a + /// system font, or an earlier / registration) + /// and, if found, registers *that same* as a face of + /// too. + /// + /// true if a local family by that name was found and registered, false otherwise (the caller tries the next src candidate) + /// Virtual for the same reason as - see its doc comment. + public virtual bool AddFontFaceFromLocalFamily(string familyName, string localFamilyName, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + var localFamily = _fontsHandler.TryGetExistingFamily(localFamilyName); + if (localFamily == null) + { + return false; + } + + _fontsHandler.AddFontFace(familyName, localFamily, weight, isItalic, stretch, ranges); + return true; + } + /// /// Get font instance by given font family name, size and style. /// @@ -206,6 +458,28 @@ public RFont GetFont(string family, double size, RFontStyle style) return _fontsHandler.GetCachedFont(family, size, style); } + /// + /// Get font instance by given font family name, size, style, and CSS Fonts Level 4 numeric + /// weight/stretch - matches against any @font-face faces registered for + /// (see ), falling + /// back to the legacy family-name lookup when none are registered. + /// + /// the font family name + /// font size + /// font style (Italic/Underline/Strikeout are honored regardless of which face is chosen; Bold is superseded by ) + /// CSS Fonts Level 4 numeric weight (1-1000) + /// CSS Fonts Level 3 stretch (1-9) + /// the box's first non-whitespace character's codepoint, for unicode-range face disambiguation, or null to skip it + /// font instance, or null when is given and no registered face covers it + /// + /// Virtual so the PdfSharp backend can override it to bypass 's shared + /// registry entirely, for the same reason as - see its doc comment. + /// + public virtual RFont GetFont(string family, double size, RFontStyle style, int weight, int stretch, int? codepoint) + { + return _fontsHandler.GetCachedFont(family, size, style, weight, stretch, codepoint); + } + /// /// Get image to be used while HTML image is loading. /// @@ -351,14 +625,13 @@ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style) protected abstract RBrush CreateSolidBrush(RColor color); /// - /// Get linear gradient color brush from to . + /// Get a multi-stop linear gradient brush along the line from to . /// - /// the rectangle to get the brush for - /// the start color of the gradient - /// the end color of the gradient - /// the angle to move the gradient from start color to end color in the rectangle + /// the gradient line's start point + /// the gradient line's end point + /// color stops, each with a position in [0,1] along the gradient line /// linear gradient color brush instance - protected abstract RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle); + protected abstract RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops); /// /// Convert image object returned from to . @@ -393,6 +666,19 @@ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style) /// font instance protected abstract RFont CreateFontInt(RFontFamily family, double size, RFontStyle style); + /// + /// Registers (a loaded @font-face src candidate's raw + /// font file bytes) with the platform text engine and returns an opaque + /// handle for it - one file's bytes in, one face's family handle out (a WinForms/WPF + /// implementation registers with the OS/GDI text engine and returns a handle to that one face; + /// this is not called for backends - like PdfSharp - that override to + /// bypass this entirely). + /// + /// the font file's raw bytes (TTF/OTF) + /// the resolved source this was loaded from, for diagnostics/error messages only + /// the registered face's family handle, or null if the bytes could not be loaded as a font (the caller tries the next src candidate) + protected abstract RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath); + /// /// Get data object for the given html and plain text data.
/// The data object can be used for clipboard or drag-drop operation. diff --git a/Source/HtmlRenderer/Adapters/RColorScheme.cs b/Source/HtmlRenderer/Adapters/RColorScheme.cs new file mode 100644 index 000000000..b4fd68303 --- /dev/null +++ b/Source/HtmlRenderer/Adapters/RColorScheme.cs @@ -0,0 +1,32 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +namespace TheArtOfDev.HtmlRenderer.Adapters +{ + /// + /// The colour scheme the rendering surface presents, as reported by the platform adapter and + /// queried by the prefers-color-scheme media feature (Media Queries 5 §5.1). + /// + public enum RColorScheme + { + /// + /// A light surface - dark content on a light background. The default for any adapter that has + /// no system theme to follow. + /// + Light, + + /// + /// A dark surface - light content on a dark background. + /// + Dark + } +} diff --git a/Source/HtmlRenderer/Adapters/RGraphics.cs b/Source/HtmlRenderer/Adapters/RGraphics.cs index af54f2ae4..22ba0df77 100644 --- a/Source/HtmlRenderer/Adapters/RGraphics.cs +++ b/Source/HtmlRenderer/Adapters/RGraphics.cs @@ -76,16 +76,15 @@ public RBrush GetSolidBrush(RColor color) } /// - /// Get linear gradient color brush from to . + /// Get a multi-stop linear gradient brush along the line from to . /// - /// the rectangle to get the brush for - /// the start color of the gradient - /// the end color of the gradient - /// the angle to move the gradient from start color to end color in the rectangle + /// the gradient line's start point + /// the gradient line's end point + /// color stops, each with a position in [0,1] along the gradient line /// linear gradient color brush instance - public RBrush GetLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + public RBrush GetLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops) { - return _adapter.GetLinearGradientBrush(rect, color1, color2, angle); + return _adapter.GetLinearGradientBrush(p1, p2, stops); } /// diff --git a/Source/HtmlRenderer/Adapters/RGraphicsPath.cs b/Source/HtmlRenderer/Adapters/RGraphicsPath.cs index 21c86bc1a..474781a5e 100644 --- a/Source/HtmlRenderer/Adapters/RGraphicsPath.cs +++ b/Source/HtmlRenderer/Adapters/RGraphicsPath.cs @@ -30,10 +30,19 @@ public abstract class RGraphicsPath : IDisposable public abstract void LineTo(double x, double y); /// - /// Add circular arc of the given size to the given point from the last point. + /// Add an elliptical arc with independent horizontal (X) and vertical (Y) radii to the given + /// point from the last point - supports elliptical border-radius corners. /// - public abstract void ArcTo(double x, double y, double size, Corner corner); - + public abstract void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner); + + /// + /// Add a circular arc of the given size to the given point from the last point. + /// + public void ArcTo(double x, double y, double size, Corner corner) + { + ArcTo(x, y, size, size, corner); + } + /// /// Release path resources. /// diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs index f0e4a8106..d7ae1ca52 100644 --- a/Source/HtmlRenderer/Core/CssData.cs +++ b/Source/HtmlRenderer/Core/CssData.cs @@ -6,54 +6,51 @@ // like the days and months; // they die and are reborn, // like the four seasons." -// +// // - Sun Tsu, // "The Art of War" using System; using System.Collections.Generic; +using System.Linq; using TheArtOfDev.HtmlRenderer.Adapters; -using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Dom; using TheArtOfDev.HtmlRenderer.Core.Parse; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core { /// - /// Holds parsed stylesheet css blocks arranged by media and classes.
- /// + /// Holds parsed stylesheets (UA default + author + inline), and provides origin-aware, + /// specificity-ordered selector matching against the box tree.
///
/// - /// To learn more about CSS blocks visit CSS spec: http://www.w3.org/TR/CSS21/syndata.html#block + /// Derived from the same ExCSS lineage as the vendored CSS-OM (originally Tyler Brinks' ExCSS), + /// adapted to HTML-Renderer's CssBox/HtmlTag shape. One deliberate omission from the source: the + /// ::marker pseudo-element box-synthesis path is removed - ::before/::after + /// synthesis is kept, but this codebase has no CssBox.IsMarkerPseudoElement concept and no + /// consumer for it (list markers are painted procedurally, not via a synthesized box). /// public sealed class CssData { - #region Fields and Consts - - /// - /// used to return empty array - /// - private static readonly List _emptyArray = new List(); - /// - /// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data. + /// The parsed stylesheets that make up this CssData, in the order they were added (UA default + /// first, then author/`<link>`/`<style>` stylesheets in document order). Each carries its own + /// flag so origin-aware cascade phases can be resolved. /// - private readonly Dictionary>> _mediaBlocks = new Dictionary>>(StringComparer.InvariantCultureIgnoreCase); - - #endregion - + internal List Stylesheets { get; } = new List(); /// /// Init. /// internal CssData() { - _mediaBlocks.Add("all", new Dictionary>(StringComparer.InvariantCultureIgnoreCase)); } /// /// Parse the given stylesheet to object.
- /// If is true the parsed css blocks are added to the + /// If is true the parsed css blocks are added to the /// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned. ///
/// @@ -63,152 +60,966 @@ internal CssData() /// the parsed css data public static CssData Parse(RAdapter adapter, string stylesheet, bool combineWithDefault = true) { - CssParser parser = new CssParser(adapter); + var parser = new CssParser(adapter); return parser.ParseStyleSheet(stylesheet, combineWithDefault); } /// - /// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data + /// Combine this CSS data's stylesheets with 's. + /// + /// the CSS data to combine with + public void Combine(CssData other) + { + ArgChecker.AssertArgNotNull(other, "other"); + Stylesheets.AddRange(other.Stylesheets); + } + + /// + /// Create a shallow copy of this css data - the returned instance has its own + /// list (so appending a new stylesheet to the clone doesn't affect the original), but the + /// instances themselves are shared (they're never mutated after parsing). /// - internal IDictionary>> MediaBlocks + /// cloned object + public CssData Clone() + { + var clone = new CssData(); + clone.Stylesheets.AddRange(Stylesheets); + return clone; + } + + // --- Rule index ----------------------------------------------------------------------- + // + // Matching every stylesheet rule against every CssBox in the document (the naive approach) + // is O(rules x boxes) and dominates cascade cost on large documents. Real browser engines + // avoid this by bucketing rules by the "subject" simple selector (the one that must match the + // box itself, e.g. the tag/class/id) so a box only needs to test the handful of rules that + // could plausibly match it, instead of the whole stylesheet. DoesSelectorMatch remains the + // source of truth for whether a rule actually matches - the index only narrows the candidates. + // + // Built lazily, once, the first time this CssData's rules are queried. Safe because by the + // time CascadeApplyStyles starts querying rules for the box tree, DomParser has already + // finished building/cloning CssData from "); } diff --git a/Source/HtmlRenderer/Core/Utils/FontFaceDescriptorResolver.cs b/Source/HtmlRenderer/Core/Utils/FontFaceDescriptorResolver.cs new file mode 100644 index 000000000..17552658a --- /dev/null +++ b/Source/HtmlRenderer/Core/Utils/FontFaceDescriptorResolver.cs @@ -0,0 +1,95 @@ +#nullable enable + +using System; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Resolves an @font-face rule's own font-weight/font-style/font-stretch + /// descriptor strings () into the override values a registered + /// face's matching entry takes - these are authoritative for how a specific registered face + /// participates in matching, independent of what the font file's own internal tables say. Returns null + /// for any descriptor this can't confidently resolve to a single concrete value (absent, or a + /// variable-font weight/stretch *range* like 100 900/50% 200% - real interpolated + /// variable fonts are out of scope), so the caller falls back to the value sniffed from the file itself + /// instead of silently forcing a wrong/arbitrary one. + /// + internal static class FontFaceDescriptorResolver + { + /// + /// Resolves a font-weight descriptor (normal/bold/a number/absent) to a + /// concrete CSS Fonts numeric weight. A two-token range (variable-font syntax) resolves to its + /// lower bound as a reasonable single-value approximation, since there is no variable-font + /// interpolation here. Any other multi-token or unparseable value returns null. + /// + internal static int? ResolveWeight(string? weightDescriptor) + { + if (string.IsNullOrWhiteSpace(weightDescriptor)) + return null; + + // See UnicodeRangeParser.Parse for why the ! is needed here on netstandard2.0. + var tokens = weightDescriptor!.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length == 1 && int.TryParse(tokens[0], out var numeric)) + return numeric; + if (tokens.Length == 1 && tokens[0] == CssConstants.Bold) + return 700; + if (tokens.Length == 1 && tokens[0] == CssConstants.Normal) + return 400; + if (tokens.Length == 2 && int.TryParse(tokens[0], out var lowerBound)) + return lowerBound; + + return null; + } + + /// + /// Resolves a font-style descriptor (normal/italic/oblique/ + /// oblique <angle>/absent) to whether the face should be treated as italic for + /// matching purposes. Returns null for absent/unrecognized values. + /// + internal static bool? ResolveIsItalic(string? styleDescriptor) + { + if (string.IsNullOrWhiteSpace(styleDescriptor)) + return null; + + var firstToken = styleDescriptor!.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)[0]; + + if (firstToken == CssConstants.Italic || firstToken == CssConstants.Oblique) + return true; + if (firstToken == CssConstants.Normal) + return false; + + return null; + } + + /// + /// Resolves a font-stretch descriptor (one of the 9 CSS Fonts keywords, or absent) to the + /// matching 1-9 numeric scale via . Percentage values/ranges + /// (variable-font syntax) and any other unrecognized value return null rather than being silently + /// coerced to normal. + /// + internal static int? ResolveStretch(string? stretchDescriptor) + { + if (string.IsNullOrWhiteSpace(stretchDescriptor)) + return null; + + var trimmed = stretchDescriptor!.Trim(); + + switch (trimmed) + { + case CssConstants.UltraCondensed: + case CssConstants.ExtraCondensed: + case CssConstants.Condensed: + case CssConstants.SemiCondensed: + case CssConstants.Normal: + case CssConstants.SemiExpanded: + case CssConstants.Expanded: + case CssConstants.ExtraExpanded: + case CssConstants.UltraExpanded: + return FontStretchResolver.Resolve(trimmed); + default: + return null; + } + } + } +} diff --git a/Source/HtmlRenderer/Core/Utils/FontStretchResolver.cs b/Source/HtmlRenderer/Core/Utils/FontStretchResolver.cs new file mode 100644 index 000000000..a795d8496 --- /dev/null +++ b/Source/HtmlRenderer/Core/Utils/FontStretchResolver.cs @@ -0,0 +1,34 @@ +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Resolves a CSS font-stretch keyword to the 1-9 numeric scale matching the OpenType OS/2 + /// table's usWidthClass field directly (1=ultra-condensed ... 5=normal ... 9=ultra-expanded) - + /// the same scale a ported TtfFontDescription's Stretch reads from that field for each + /// registered face, so the two are directly comparable without any extra translation. + /// + internal static class FontStretchResolver + { + internal const int Normal = 5; + + /// + /// Resolves a raw font-stretch keyword (a box's cascaded value, or an @font-face + /// descriptor - see ) to the 1-9 scale. + /// Unlike PeachPDF's own version of this resolver, there's only this one string-keyword overload - + /// HTML-Renderer's CSS engine works with raw cascaded strings throughout (no separate strongly-typed + /// CSS-OM enum for font-stretch the way PeachPDF's does), so a second typed-enum overload would have + /// no caller. + /// + internal static int Resolve(string fontStretchValue) => fontStretchValue switch + { + CssConstants.UltraCondensed => 1, + CssConstants.ExtraCondensed => 2, + CssConstants.Condensed => 3, + CssConstants.SemiCondensed => 4, + CssConstants.SemiExpanded => 6, + CssConstants.Expanded => 7, + CssConstants.ExtraExpanded => 8, + CssConstants.UltraExpanded => 9, + _ => Normal + }; + } +} diff --git a/Source/HtmlRenderer/Core/Utils/IsExternalInit.cs b/Source/HtmlRenderer/Core/Utils/IsExternalInit.cs new file mode 100644 index 000000000..c358f7441 --- /dev/null +++ b/Source/HtmlRenderer/Core/Utils/IsExternalInit.cs @@ -0,0 +1,12 @@ +#if NETSTANDARD2_0 +// C# 9 records/init-only setters need this marker type, which only ships in the BCL from +// netstandard2.1/.NET 5 onward. LangVersion 12 (set project-wide in Directory.Build.props) makes the +// record/init-accessor *syntax* available on netstandard2.0 too, but the compiler still needs the type to +// exist somewhere in the compilation - this is the standard, widely-used polyfill for that gap. +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit + { + } +} +#endif diff --git a/Source/HtmlRenderer/Core/Utils/RenderUtils.cs b/Source/HtmlRenderer/Core/Utils/RenderUtils.cs index 2726b5fb2..091c75191 100644 --- a/Source/HtmlRenderer/Core/Utils/RenderUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/RenderUtils.cs @@ -95,44 +95,42 @@ public static void DrawImageErrorIcon(RGraphics g, HtmlContainerInt htmlContaine } /// - /// Creates a rounded rectangle using the specified corner radius
- /// NW-----NE + /// Creates a rounded rectangle path. Each corner has independent horizontal (X) and vertical (Y) + /// radii, supporting elliptical corners per the CSS border-radius spec.
+ /// TL-----TR /// | | /// | | - /// SW-----SE + /// BL-----BR ///
/// the device to draw into /// Rectangle to round - /// Radius of the north east corner - /// Radius of the north west corner - /// Radius of the south east corner - /// Radius of the south west corner /// GraphicsPath with the lines of the rounded rectangle ready to be painted - public static RGraphicsPath GetRoundRect(RGraphics g, RRect rect, double nwRadius, double neRadius, double seRadius, double swRadius) + public static RGraphicsPath GetRoundRect(RGraphics g, RRect rect, + double tlX, double tlY, double trX, double trY, + double brX, double brY, double blX, double blY) { var path = g.GetGraphicsPath(); - path.Start(rect.Left + nwRadius, rect.Top); - - path.LineTo(rect.Right - neRadius, rect.Y); - - if (neRadius > 0f) - path.ArcTo(rect.Right, rect.Top + neRadius, neRadius, RGraphicsPath.Corner.TopRight); - - path.LineTo(rect.Right, rect.Bottom - seRadius); - - if (seRadius > 0f) - path.ArcTo(rect.Right - seRadius, rect.Bottom, seRadius, RGraphicsPath.Corner.BottomRight); - - path.LineTo(rect.Left + swRadius, rect.Bottom); - - if (swRadius > 0f) - path.ArcTo(rect.Left, rect.Bottom - swRadius, swRadius, RGraphicsPath.Corner.BottomLeft); - - path.LineTo(rect.Left, rect.Top + nwRadius); - - if (nwRadius > 0f) - path.ArcTo(rect.Left + nwRadius, rect.Top, nwRadius, RGraphicsPath.Corner.TopLeft); + // Top edge: start after TL corner, end before TR corner. + path.Start(rect.Left + tlX, rect.Top); + path.LineTo(rect.Right - trX, rect.Top); + if (trX > 0 || trY > 0) + path.ArcTo(rect.Right, rect.Top + trY, trX, trY, RGraphicsPath.Corner.TopRight); + + // Right edge. + path.LineTo(rect.Right, rect.Bottom - brY); + if (brX > 0 || brY > 0) + path.ArcTo(rect.Right - brX, rect.Bottom, brX, brY, RGraphicsPath.Corner.BottomRight); + + // Bottom edge. + path.LineTo(rect.Left + blX, rect.Bottom); + if (blX > 0 || blY > 0) + path.ArcTo(rect.Left, rect.Bottom - blY, blX, blY, RGraphicsPath.Corner.BottomLeft); + + // Left edge. + path.LineTo(rect.Left, rect.Top + tlY); + if (tlX > 0 || tlY > 0) + path.ArcTo(rect.Left + tlX, rect.Top, tlX, tlY, RGraphicsPath.Corner.TopLeft); return path; } diff --git a/Source/HtmlRenderer/Core/Utils/TtfFontDescription.cs b/Source/HtmlRenderer/Core/Utils/TtfFontDescription.cs new file mode 100644 index 000000000..9ecb09a05 --- /dev/null +++ b/Source/HtmlRenderer/Core/Utils/TtfFontDescription.cs @@ -0,0 +1,264 @@ +#nullable enable + +using System; +using System.IO; +using System.Text; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Sniffs a TTF/OTF font file's family/subfamily name, style, and CSS Fonts numeric weight/stretch + /// directly from its name/OS/2 tables - a single self-contained reader, ported from + /// PeachPDF's Fonts\TtfFontDescription.cs (adapted to this project's own + /// instead of PeachPDF's PdfSharpCore-specific XFontStyle, since this type lives in the shared + /// Core project used by every backend, not just PdfSharp - both WPF's @font-face loading, which + /// needs the font's own internal family name to look it up via System.Windows.Media.FontFamily + /// after registering it with AddFontMemResourceEx, and the PdfSharp FontResolver port, + /// which needs the numeric weight/stretch for CSS Fonts Level 4 matching, need this). Public: it's + /// consumed from the WPF and PdfSharp backend assemblies, not just Core itself. + /// + /// + /// Replaces this project's older HtmlRenderer.PdfSharp\FontResolution\FontMetadata.cs/ + /// Parsing\FontParser.cs (PdfSharp-backend-only, name-table-only, no numeric weight/stretch at + /// all - it could not supply what CSS Fonts Level 4 matching needs). + /// + public readonly struct TtfFontDescription + { + /// Default CSS Fonts numeric weight (400 = "normal") used when a font has no OS/2 table, or its field is out of the valid 1-1000 range. + public const int DefaultWeight = 400; + + /// Default CSS Fonts stretch value (5 = "normal" on the 1-9 usWidthClass scale) used when a font has no OS/2 table, or its value is out of the valid 1-9 range. + public const int DefaultStretch = 5; + + public TtfFontDescription(string fontFamilyInvariantCulture, string fontNameInvariantCulture, RFontStyle style, int weight, int stretch) + { + FontFamilyInvariantCulture = fontFamilyInvariantCulture; + FontNameInvariantCulture = fontNameInvariantCulture; + Style = style; + Weight = weight; + Stretch = stretch; + } + + public string FontFamilyInvariantCulture { get; } + public string FontNameInvariantCulture { get; } + public RFontStyle Style { get; } + + /// + /// CSS Fonts Level 4 numeric weight (1-1000), read from the OS/2 table's usWeightClass + /// field when present and valid; falls back to a value derived from 's + /// name-table-subfamily-sniffed Bold bit (700 if bold, else ) when + /// OS/2 is absent or its usWeightClass is 0 (a real font can legitimately omit/zero this + /// field even though the spec range is 1-1000). + /// + public int Weight { get; } + + /// + /// CSS Fonts Level 3 font-stretch classification (1-9, matching the OS/2 usWidthClass + /// scale directly: 1=ultra-condensed ... 5=normal ... 9=ultra-expanded), read from the OS/2 table + /// when present and valid; (normal) otherwise. + /// + public int Stretch { get; } + + public static TtfFontDescription LoadDescription(string path) + { + using (var stream = File.OpenRead(path)) + { + return LoadDescription(stream); + } + } + + public static TtfFontDescription LoadDescription(Stream stream) + { + // TTF/OTF files are big-endian. Read the offset table to locate the name/OS2 tables. + var buf4 = new byte[4]; + var buf2 = new byte[2]; + + ReadExactly(stream, buf4, 4); // sfVersion - skip + ReadExactly(stream, buf2, 2); + int numTables = ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); // searchRange + ReadExactly(stream, buf2, 2); // entrySelector + ReadExactly(stream, buf2, 2); // rangeShift + + long nameTableOffset = -1; + long os2TableOffset = -1; + for (int i = 0; i < numTables; i++) + { + ReadExactly(stream, buf4, 4); + var tag = Encoding.ASCII.GetString(buf4); + ReadExactly(stream, buf4, 4); // checkSum + ReadExactly(stream, buf4, 4); + uint tableOffset = ReadUInt32BE(buf4); + ReadExactly(stream, buf4, 4); // length + + if (tag == "name") + nameTableOffset = tableOffset; + else if (tag == "OS/2") + os2TableOffset = tableOffset; + } + + if (nameTableOffset < 0) + throw new InvalidOperationException("Font file does not contain a name table."); + + stream.Seek(nameTableOffset, SeekOrigin.Begin); + ReadExactly(stream, buf2, 2); // format + ReadExactly(stream, buf2, 2); + int count = ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); + int stringOffset = ReadUInt16BE(buf2); + long storageBase = nameTableOffset + stringOffset; + + // Read all name records (6 uint16 fields each) + var platformIDs = new ushort[count]; + var encodingIDs = new ushort[count]; + var languageIDs = new ushort[count]; + var nameIDs = new ushort[count]; + var lengths = new ushort[count]; + var offsets = new ushort[count]; + + for (int i = 0; i < count; i++) + { + ReadExactly(stream, buf2, 2); platformIDs[i] = (ushort)ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); encodingIDs[i] = (ushort)ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); languageIDs[i] = (ushort)ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); nameIDs[i] = (ushort)ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); lengths[i] = (ushort)ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); offsets[i] = (ushort)ReadUInt16BE(buf2); + } + + string? familyName = ReadBestNameRecord(stream, platformIDs, encodingIDs, languageIDs, nameIDs, lengths, offsets, count, storageBase, 1); + string? subfamilyName = ReadBestNameRecord(stream, platformIDs, encodingIDs, languageIDs, nameIDs, lengths, offsets, count, storageBase, 2); + string? fullName = ReadBestNameRecord(stream, platformIDs, encodingIDs, languageIDs, nameIDs, lengths, offsets, count, storageBase, 4); + + RFontStyle style; + switch (subfamilyName != null ? subfamilyName.ToLowerInvariant() : null) + { + case "bold italic": + case "bold oblique": + style = RFontStyle.Bold | RFontStyle.Italic; + break; + case "bold": + style = RFontStyle.Bold; + break; + case "italic": + case "oblique": + style = RFontStyle.Italic; + break; + default: + style = RFontStyle.Regular; + break; + } + + int weight, stretch; + ReadOs2WeightAndStretch(stream, os2TableOffset, out weight, out stretch); + if (weight == 0) + weight = (style & RFontStyle.Bold) != 0 ? 700 : DefaultWeight; + + return new TtfFontDescription( + familyName ?? fullName ?? string.Empty, + fullName ?? familyName ?? string.Empty, + style, + weight, + stretch); + } + + /// + /// Reads usWeightClass (offset 4) and usWidthClass (offset 6) from the OS/2 table, + /// per the OpenType spec's OS/2 table layout (both fields are present in every OS/2 table + /// version, including the oldest version 0). Yields (0, ) - a + /// sentinel the caller substitutes a Style-derived default for - when there's no OS/2 table at + /// all, or a value is outside its spec-valid range (weight: 1-1000, stretch: 1-9). + /// + private static void ReadOs2WeightAndStretch(Stream stream, long os2TableOffset, out int weight, out int stretch) + { + if (os2TableOffset < 0) + { + weight = 0; + stretch = DefaultStretch; + return; + } + + var buf2 = new byte[2]; + stream.Seek(os2TableOffset + 4, SeekOrigin.Begin); + ReadExactly(stream, buf2, 2); + var weightClass = ReadUInt16BE(buf2); + ReadExactly(stream, buf2, 2); + var widthClass = ReadUInt16BE(buf2); + + weight = weightClass >= 1 && weightClass <= 1000 ? weightClass : 0; + stretch = widthClass >= 1 && widthClass <= 9 ? widthClass : DefaultStretch; + } + + // Prefers platformID=3/encodingID=1 (Windows Unicode) with en-US, then any language, + // then platformID=1 (Mac Roman), then whatever is available. + private static string? ReadBestNameRecord( + Stream stream, + ushort[] platformIDs, ushort[] encodingIDs, ushort[] languageIDs, + ushort[] nameIDs, ushort[] lengths, ushort[] offsets, + int count, long storageBase, ushort targetNameID) + { + int best = -1; + int bestPriority = int.MaxValue; + + for (int i = 0; i < count; i++) + { + if (nameIDs[i] != targetNameID) continue; + + int priority; + if (platformIDs[i] == 3 && encodingIDs[i] == 1 && languageIDs[i] == 0x0409) + priority = 0; + else if (platformIDs[i] == 3 && encodingIDs[i] == 1) + priority = 1; + else if (platformIDs[i] == 1) + priority = 2; + else + priority = 3; + + if (priority < bestPriority) + { + bestPriority = priority; + best = i; + } + } + + if (best < 0) return null; + + stream.Seek(storageBase + offsets[best], SeekOrigin.Begin); + var bytes = new byte[lengths[best]]; + ReadExactly(stream, bytes, bytes.Length); + + return platformIDs[best] == 1 + ? GetLatin1().GetString(bytes, 0, bytes.Length) + : Encoding.BigEndianUnicode.GetString(bytes, 0, bytes.Length); + } + + // Encoding.Latin1 is a .NET 5+ convenience shortcut - GetEncoding("iso-8859-1") is the portable + // equivalent available on every TFM this project targets (including netstandard2.0/net462). + private static Encoding GetLatin1() => Encoding.GetEncoding("iso-8859-1"); + + /// + /// Fills 's first bytes from , + /// looping until satisfied (a plain is not guaranteed to fill the buffer + /// in one call). The portable equivalent of Stream.ReadExactly (.NET 7+ only, not available + /// on this project's netstandard2.0/net462 legs). + /// + private static void ReadExactly(Stream stream, byte[] buffer, int count) + { + var offset = 0; + while (offset < count) + { + var read = stream.Read(buffer, offset, count - offset); + if (read <= 0) + throw new EndOfStreamException(); + offset += read; + } + } + + private static int ReadUInt16BE(byte[] b) => + (b[0] << 8) | b[1]; + + private static uint ReadUInt32BE(byte[] b) => + ((uint)b[0] << 24) | ((uint)b[1] << 16) | ((uint)b[2] << 8) | b[3]; + } +} diff --git a/Source/HtmlRenderer/Core/Utils/UnicodeRangeParser.cs b/Source/HtmlRenderer/Core/Utils/UnicodeRangeParser.cs new file mode 100644 index 000000000..26ed6fe0e --- /dev/null +++ b/Source/HtmlRenderer/Core/Utils/UnicodeRangeParser.cs @@ -0,0 +1,97 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Parse; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Parses a CSS @font-face unicode-range descriptor into a compact set of inclusive + /// codepoint s, reusing the existing CSS tokenizer's U+ grammar + /// (, already fully implemented - + /// see 's UnicodeRange method) + /// rather than re-implementing it. + /// + public static class UnicodeRangeParser + { + /// + /// Parses a unicode-range descriptor into inclusive codepoint ranges, or returns null when + /// the descriptor is absent/blank or contains no valid range (meaning "no explicit subset - the + /// face applies to whatever its font actually covers"). + /// + public static IReadOnlyList? Parse(string? descriptor) + { + if (string.IsNullOrWhiteSpace(descriptor)) + return null; + + List? ranges = null; + + // The value can arrive either in its CSS source form ("U+41-5A, U+61-7A") or, once round- + // tripped through the CSS-OM, with the "U+" prefix dropped ("41-5A, 61-7A"). Split on the + // top-level commas and re-tokenize each segment with the "U+" prefix the lexer's range + // grammar expects, so both forms parse identically through the one shared tokenizer. + // The netstandard2.0 reference assembly's string.IsNullOrWhiteSpace has no [NotNullWhen] + // annotation, so the compiler can't narrow descriptor to non-null past the check above on + // that TFM alone (net8.0 already narrows it) - the ! makes both legs agree. + foreach (var segment in descriptor!.Split(',')) + { + var normalized = segment.Trim(); + + if (normalized.Length == 0) + continue; + + if (!normalized.StartsWith("U+", StringComparison.OrdinalIgnoreCase)) + normalized = "U+" + normalized; + + var rangeToken = CssValueParser.GetCssTokens(normalized).OfType().FirstOrDefault(); + + if (rangeToken == null) + continue; + + if (!int.TryParse(rangeToken.Start, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var start) || + !int.TryParse(rangeToken.End, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var end)) + continue; + + if (start > Symbols.MaximumCodepoint) + continue; + + if (end > Symbols.MaximumCodepoint) + end = Symbols.MaximumCodepoint; + + if (end < start) + continue; + + // A declared range whose bound lands on a surrogate can't be a real Unicode scalar value + // (surrogates aren't valid codepoints on their own); nudge inward, and skip a range that + // is nothing but surrogates. + if (start is >= 0xD800 and <= 0xDFFF) + start = 0xE000; + if (end is >= 0xD800 and <= 0xDFFF) + end = 0xD7FF; + if (start > end) + continue; + + (ranges ??= new List()).Add(new CodepointRange(start, end)); + } + + return ranges; + } + + /// + /// Whether falls inside any of . + /// + public static bool Covers(IReadOnlyList ranges, int codepoint) + { + for (var i = 0; i < ranges.Count; i++) + { + if (ranges[i].Contains(codepoint)) + return true; + } + return false; + } + } +} diff --git a/Source/HtmlRenderer/HtmlRenderer.csproj b/Source/HtmlRenderer/HtmlRenderer.csproj index 7212373ed..e21f42282 100644 --- a/Source/HtmlRenderer/HtmlRenderer.csproj +++ b/Source/HtmlRenderer/HtmlRenderer.csproj @@ -4,6 +4,7 @@ Library TheArtOfDev.HtmlRenderer true + true @@ -24,6 +25,15 @@ For existing implementations see: HtmlRenderer.WinForms, HtmlRenderer.WPF and Ht + + + + + + diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png index c5686cf06..0ba3367e5 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png index ac8c207c4..835a5369b 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png index 38f7feb91..8f6d24715 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png index bb110ba10..0541b326f 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png index 43db1710c..e298a696c 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png index 75ed6e306..4a443a631 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png index 264b7413f..a3c8753b1 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png index ec8ef692a..aa3b19e3e 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png index 041208fc6..1e2c8b6de 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png index 0b7b7103b..f46283eaf 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Float wrap.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Float wrap.png new file mode 100644 index 000000000..0a07beb38 Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Float wrap.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png index adc6aff76..d0f6979f1 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png index 3f1fcbd38..fa778ea81 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png index 0a1e0fd89..89ab0ac35 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png index 48bdccf4a..63793b07d 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png index 8ed9d3f91..e3e8c72a1 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png index 569e1547c..7b87e4ca1 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png index 5ba9ca0ae..20bbb594e 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png index e63b3ca00..ab353cf9e 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png index f0dd3072b..c4a9aac47 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png index 85ba90cef..48522a053 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Media queries and color scheme.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Media queries and color scheme.png new file mode 100644 index 000000000..190c61052 Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Media queries and color scheme.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png index a16bbfd36..ef95f22c5 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png index a49ee1c13..64a988737 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png index 8d55fdb56..14c35ccf2 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png index 5c89192db..e676ca361 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png index 47d955000..74d8db18f 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png index 901825887..83d2fc417 100644 Binary files a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png differ diff --git a/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs b/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs index 1b1f68f80..842c5dc1b 100644 --- a/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs +++ b/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Reflection; using System.Text; +using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Demo.Common; using TheArtOfDev.HtmlRenderer.WinForms; @@ -24,7 +25,7 @@ public sealed class HtmlRenderingRegressionTests [DoNotParallelize] [TestMethod] [DynamicData(nameof(GetSamples), DynamicDataDisplayName = nameof(GetSampleDisplayName))] - public void Render_DemoTestSample_MatchesBaselineImage(string sampleName, string sampleHtml) + public async Task Render_DemoTestSample_MatchesBaselineImage(string sampleName, string sampleHtml) { EnsureSamplesLoaded(); @@ -42,7 +43,7 @@ public void Render_DemoTestSample_MatchesBaselineImage(string sampleName, string var actualPath = Path.Combine(outputDirectory, sampleFileName + ".actual.png"); var diffPath = Path.Combine(outputDirectory, sampleFileName + ".diff.png"); - using var rendered = RenderSample(sampleHtml); + using var rendered = await RenderSample(sampleHtml); if (!File.Exists(baselinePath)) { if (approveBaselines) @@ -119,13 +120,19 @@ private static void EnsureSamplesLoaded() } SamplesLoader.Init("Regression", typeof(HtmlRender).Assembly.GetName().Version.ToString()); + + // Baselines are captured once on some machine and compared everywhere after; a sample driven + // by prefers-color-scheme must not depend on whichever machine happens to run the test having + // the same Windows theme as that one did. Pin it so every render sees the same reported scheme. + RAdapter.SystemColorSchemeOverride = RColorScheme.Light; + _samplesLoaded = true; } } - private static Bitmap RenderSample(string html) + private static async Task RenderSample(string html) { - return (Bitmap)HtmlRender.RenderToImage( + return (Bitmap)await HtmlRender.RenderToImageAsync( html, minSize: Size.Empty, maxSize: new Size(MaxWidth, MaxHeight), diff --git a/Source/Test/HtmlRenderer.PdfSharp.Test/PdfGeneratorTests.cs b/Source/Test/HtmlRenderer.PdfSharp.Test/PdfGeneratorTests.cs index 0fd72ab1e..f98e20235 100644 --- a/Source/Test/HtmlRenderer.PdfSharp.Test/PdfGeneratorTests.cs +++ b/Source/Test/HtmlRenderer.PdfSharp.Test/PdfGeneratorTests.cs @@ -11,7 +11,7 @@ namespace HtmlRenderer.PdfSharp.Test; public sealed class PdfGeneratorTests { [TestMethod] - public void GeneratePdf_FromHtml_CreatesPdfDocument() + public async Task GeneratePdf_FromHtml_CreatesPdfDocument() { // Arrange var config = new PdfGenerateConfig @@ -33,7 +33,7 @@ public void GeneratePdf_FromHtml_CreatesPdfDocument() """; // Act - using var document = PdfGenerator.GeneratePdf(html, config, null, DemoUtils.OnStylesheetLoad, OnImageLoadPdfSharp); + using var document = await PdfGenerator.GeneratePdf(html, config, null, DemoUtils.OnStylesheetLoad, OnImageLoadPdfSharp); // Assert Assert.AreEqual(1, document.Pages.Count); @@ -60,7 +60,7 @@ public void GeneratePdf_FromHtml_CreatesPdfDocument() } [TestMethod] - public void GeneratePdf_FromHtml_WithMultipleFonts_CreatesPdfDocument() + public async Task GeneratePdf_FromHtml_WithMultipleFonts_CreatesPdfDocument() { // Arrange var config = new PdfGenerateConfig @@ -86,7 +86,7 @@ public void GeneratePdf_FromHtml_WithMultipleFonts_CreatesPdfDocument() """; // Act - using var document = PdfGenerator.GeneratePdf(html, config, null, DemoUtils.OnStylesheetLoad, null); + using var document = await PdfGenerator.GeneratePdf(html, config, null, DemoUtils.OnStylesheetLoad, null); // Assert Assert.AreEqual(1, document.Pages.Count);