├── .gitignore ├── LICENSE ├── README.md ├── dist ├── adapter │ ├── paint.js │ ├── paragraph.js │ ├── paragraph_builder.js │ └── skia.js ├── impl │ ├── drawer.js │ ├── layout.js │ └── span.js ├── index.js ├── logger.js ├── polyfill.js ├── polyfill.types.js ├── target.js └── util.js ├── minitex.js ├── minitex.min.js ├── package-lock.json ├── package.json ├── src ├── adapter │ ├── paint.ts │ ├── paragraph.ts │ ├── paragraph_builder.ts │ └── skia.ts ├── impl │ ├── drawer.ts │ ├── layout.ts │ └── span.ts ├── index.ts ├── logger.ts ├── polyfill.ts ├── polyfill.types.ts ├── target.ts └── util.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2024] [MPFlutter] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniTex 2 | 3 | MiniTex is a text typesetting and rendering library that can be used on the Web and WeChat mini programs. 4 | 5 | MiniTex should be used in conjunction with Skia CanvasKit, which can use the Text Renderer of Web Canvas2D to achieve text typesetting and rendering capabilities without loading additional font files. 6 | 7 | For Skia CanvasKit, the benefits are very obvious. Users no longer need to download multi-language fonts. 8 | 9 | MiniTex is designed for Skia CanvasKit, and its interface is fully compatible with Skia Paragraph. 10 | 11 | MPFlutter uses MiniTex to render text. -------------------------------------------------------------------------------- /dist/adapter/paint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.Paint = void 0; 7 | const util_1 = require("../util"); 8 | const skia_1 = require("./skia"); 9 | class Paint extends skia_1.SkEmbindObject { 10 | constructor() { 11 | super(...arguments); 12 | this._type = "SkPaint"; 13 | this._color = (0, util_1.valueOfRGBAInt)(0, 0, 0, 255); 14 | this._strokeCap = skia_1.StrokeCap.Butt; 15 | this._strokeJoin = skia_1.StrokeJoin.Bevel; 16 | this._strokeMiter = 0; 17 | this._strokeWidth = 0; 18 | this._alpha = 1.0; 19 | this._antiAlias = true; 20 | } 21 | /** 22 | * Returns a copy of this paint. 23 | */ 24 | copy() { 25 | const newValue = new Paint(); 26 | Object.assign(newValue, this); 27 | return newValue; 28 | } 29 | /** 30 | * Retrieves the alpha and RGB unpremultiplied. RGB are extended sRGB values 31 | * (sRGB gamut, and encoded with the sRGB transfer function). 32 | */ 33 | getColor() { 34 | return this._color; 35 | } 36 | /** 37 | * Returns the geometry drawn at the beginning and end of strokes. 38 | */ 39 | getStrokeCap() { 40 | return this._strokeCap; 41 | } 42 | /** 43 | * Returns the geometry drawn at the corners of strokes. 44 | */ 45 | getStrokeJoin() { 46 | return this._strokeJoin; 47 | } 48 | /** 49 | * Returns the limit at which a sharp corner is drawn beveled. 50 | */ 51 | getStrokeMiter() { 52 | return this._strokeMiter; 53 | } 54 | /** 55 | * Returns the thickness of the pen used to outline the shape. 56 | */ 57 | getStrokeWidth() { 58 | return this._strokeWidth; 59 | } 60 | /** 61 | * Replaces alpha, leaving RGBA unchanged. 0 means fully transparent, 1.0 means opaque. 62 | * @param alpha 63 | */ 64 | setAlphaf(alpha) { 65 | this._alpha = alpha; 66 | } 67 | /** 68 | * Requests, but does not require, that edge pixels draw opaque or with 69 | * partial transparency. 70 | * @param aa 71 | */ 72 | setAntiAlias(aa) { 73 | this._antiAlias = aa; 74 | } 75 | /** 76 | * Sets the blend mode that is, the mode used to combine source color 77 | * with destination color. 78 | * @param mode 79 | */ 80 | setBlendMode(mode) { } 81 | /** 82 | * Sets the current blender, increasing its refcnt, and if a blender is already 83 | * present, decreasing that object's refcnt. 84 | * 85 | * * A nullptr blender signifies the default SrcOver behavior. 86 | * 87 | * * For convenience, you can call setBlendMode() if the blend effect can be expressed 88 | * as one of those values. 89 | * @param blender 90 | */ 91 | setBlender(blender) { } 92 | /** 93 | * Sets alpha and RGB used when stroking and filling. The color is four floating 94 | * point values, unpremultiplied. The color values are interpreted as being in 95 | * the provided colorSpace. 96 | * @param color 97 | * @param colorSpace - defaults to sRGB 98 | */ 99 | setColor(color) { 100 | this._color = color; 101 | } 102 | /** 103 | * Sets alpha and RGB used when stroking and filling. The color is four floating 104 | * point values, unpremultiplied. The color values are interpreted as being in 105 | * the provided colorSpace. 106 | * @param r 107 | * @param g 108 | * @param b 109 | * @param a 110 | * @param colorSpace - defaults to sRGB 111 | */ 112 | setColorComponents(r, g, b, a) { 113 | this.setColor((0, util_1.valueOfRGBAInt)(r, g, b, a)); 114 | } 115 | /** 116 | * Sets the current color filter, replacing the existing one if there was one. 117 | * @param filter 118 | */ 119 | setColorFilter(filter) { } 120 | /** 121 | * Sets the color used when stroking and filling. The color values are interpreted as being in 122 | * the provided colorSpace. 123 | * @param color 124 | * @param colorSpace - defaults to sRGB. 125 | */ 126 | setColorInt(color, colorSpace) { } 127 | /** 128 | * Requests, but does not require, to distribute color error. 129 | * @param shouldDither 130 | */ 131 | setDither(shouldDither) { } 132 | /** 133 | * Sets the current image filter, replacing the existing one if there was one. 134 | * @param filter 135 | */ 136 | setImageFilter(filter) { } 137 | /** 138 | * Sets the current mask filter, replacing the existing one if there was one. 139 | * @param filter 140 | */ 141 | setMaskFilter(filter) { } 142 | /** 143 | * Sets the current path effect, replacing the existing one if there was one. 144 | * @param effect 145 | */ 146 | setPathEffect(effect) { } 147 | /** 148 | * Sets the current shader, replacing the existing one if there was one. 149 | * @param shader 150 | */ 151 | setShader(shader) { } 152 | /** 153 | * Sets the geometry drawn at the beginning and end of strokes. 154 | * @param cap 155 | */ 156 | setStrokeCap(cap) { 157 | this._strokeCap = cap; 158 | } 159 | /** 160 | * Sets the geometry drawn at the corners of strokes. 161 | * @param join 162 | */ 163 | setStrokeJoin(join) { 164 | this._strokeJoin = join; 165 | } 166 | /** 167 | * Sets the limit at which a sharp corner is drawn beveled. 168 | * @param limit 169 | */ 170 | setStrokeMiter(limit) { 171 | this._strokeMiter = limit; 172 | } 173 | /** 174 | * Sets the thickness of the pen used to outline the shape. 175 | * @param width 176 | */ 177 | setStrokeWidth(width) { 178 | this._strokeWidth = width; 179 | } 180 | /** 181 | * Sets whether the geometry is filled or stroked. 182 | * @param style 183 | */ 184 | setStyle(style) { } 185 | } 186 | exports.Paint = Paint; 187 | -------------------------------------------------------------------------------- /dist/adapter/paragraph.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.Paragraph = exports.drawParagraph = void 0; 7 | const drawer_1 = require("../impl/drawer"); 8 | const layout_1 = require("../impl/layout"); 9 | const span_1 = require("../impl/span"); 10 | const logger_1 = require("../logger"); 11 | const util_1 = require("../util"); 12 | const skia_1 = require("./skia"); 13 | let drawParagraphSharedPaint; 14 | const drawParagraph = function (CanvasKit, skCanvas, paragraph, dx, dy) { 15 | let drawStartTime; 16 | if (logger_1.logger.profileMode) { 17 | drawStartTime = new Date().getTime(); 18 | } 19 | let canvasImg = paragraph.skImageCache; 20 | if (!canvasImg) { 21 | const drawer = new drawer_1.Drawer(paragraph); 22 | const imageData = drawer.draw(); 23 | canvasImg = CanvasKit.MakeImage({ 24 | width: imageData.width, 25 | height: imageData.height, 26 | alphaType: CanvasKit.AlphaType.Unpremul, 27 | colorType: CanvasKit.ColorType.RGBA_8888, 28 | colorSpace: CanvasKit.ColorSpace.SRGB, 29 | }, imageData.data, 4 * imageData.width); 30 | paragraph.skImageCache = canvasImg; 31 | paragraph.skImageWidth = imageData.width; 32 | paragraph.skImageHeight = imageData.height; 33 | } 34 | const srcRect = CanvasKit.XYWHRect(0, 0, paragraph.skImageWidth, paragraph.skImageHeight); 35 | const dstRect = CanvasKit.XYWHRect(Math.ceil(dx), Math.ceil(dy), paragraph.skImageWidth / drawer_1.Drawer.pixelRatio, paragraph.skImageHeight / drawer_1.Drawer.pixelRatio); 36 | const skPaint = drawParagraphSharedPaint !== null && drawParagraphSharedPaint !== void 0 ? drawParagraphSharedPaint : new CanvasKit.Paint(); 37 | drawParagraphSharedPaint = skPaint; 38 | skCanvas.drawImageRect(canvasImg, srcRect, dstRect, skPaint); 39 | if (logger_1.logger.profileMode) { 40 | const drawCostTime = new Date().getTime() - drawStartTime; 41 | logger_1.logger.profile("drawParagraph cost", drawCostTime); 42 | } 43 | }; 44 | exports.drawParagraph = drawParagraph; 45 | class Paragraph extends skia_1.SkEmbindObject { 46 | constructor(spans, paragraphStyle, iconFontData) { 47 | super(); 48 | this.spans = spans; 49 | this.paragraphStyle = paragraphStyle; 50 | this.iconFontData = iconFontData; 51 | this._type = "SkParagraph"; 52 | this.isMiniTex = true; 53 | this._textLayout = new layout_1.TextLayout(this); 54 | if (this.iconFontData) { 55 | this.iconFontMap = JSON.parse(this.iconFontData); 56 | } 57 | } 58 | delete() { 59 | if (this.skImageCache) { 60 | this.skImageCache.delete(); 61 | this.skImageCache = undefined; 62 | } 63 | super.delete(); 64 | } 65 | didExceedMaxLines() { 66 | return this._textLayout.didExceedMaxLines; 67 | } 68 | getAlphabeticBaseline() { 69 | return 0; 70 | } 71 | /** 72 | * Returns the index of the glyph that corresponds to the provided coordinate, 73 | * with the top left corner as the origin, and +y direction as down. 74 | */ 75 | getGlyphPositionAtCoordinate(dx, dy) { 76 | this._textLayout.measureGlyphIfNeeded(); 77 | for (let index = 0; index < this._textLayout.glyphInfos.length; index++) { 78 | const glyphInfo = this._textLayout.glyphInfos[index]; 79 | const left = glyphInfo.graphemeLayoutBounds[0]; 80 | const top = glyphInfo.graphemeLayoutBounds[1]; 81 | const width = glyphInfo.graphemeLayoutBounds[2] - left; 82 | const height = glyphInfo.graphemeLayoutBounds[3] - top; 83 | if (dx >= left && dx <= left + width && dy >= top && dy <= top + height) { 84 | return { pos: index, affinity: { value: skia_1.Affinity.Downstream } }; 85 | } 86 | } 87 | for (let index = 0; index < this._textLayout.lineMetrics.length; index++) { 88 | const lineMetrics = this._textLayout.lineMetrics[index]; 89 | const isLastLine = index === this._textLayout.lineMetrics.length - 1; 90 | const left = 0; 91 | const top = lineMetrics.yOffset; 92 | const width = lineMetrics.width; 93 | const height = lineMetrics.height; 94 | if (dy >= top && dy <= top + height) { 95 | if (dx <= 0) { 96 | return { 97 | pos: lineMetrics.startIndex, 98 | affinity: { value: skia_1.Affinity.Downstream }, 99 | }; 100 | } 101 | else if (dx >= width) { 102 | return { 103 | pos: lineMetrics.endIndex, 104 | affinity: { value: skia_1.Affinity.Downstream }, 105 | }; 106 | } 107 | } 108 | if (dy >= top + height && isLastLine) { 109 | return { 110 | pos: lineMetrics.endIndex, 111 | affinity: { value: skia_1.Affinity.Downstream }, 112 | }; 113 | } 114 | } 115 | return { pos: 0, affinity: { value: skia_1.Affinity.Upstream } }; 116 | } 117 | /** 118 | * Returns the information associated with the closest glyph at the specified 119 | * paragraph coordinate, or null if the paragraph is empty. 120 | */ 121 | getClosestGlyphInfoAtCoordinate(dx, dy) { 122 | return this.getGlyphInfoAt(this.getGlyphPositionAtCoordinate(dx, dy).pos); 123 | } 124 | /** 125 | * Returns the information associated with the glyph at the specified UTF-16 126 | * offset within the paragraph's visible lines, or null if the index is out 127 | * of bounds, or points to a codepoint that is logically after the last 128 | * visible codepoint. 129 | */ 130 | getGlyphInfoAt(index) { 131 | var _a; 132 | this._textLayout.measureGlyphIfNeeded(); 133 | return (_a = this._textLayout.glyphInfos[index]) !== null && _a !== void 0 ? _a : null; 134 | } 135 | getHeight() { 136 | const lineMetrics = this.getLineMetrics(); 137 | let height = 0; 138 | for (let i = 0; i < lineMetrics.length; i++) { 139 | height += lineMetrics[i].height * lineMetrics[i].heightMultiplier; 140 | if (i > 0 && i < lineMetrics.length) { 141 | height += lineMetrics[i].height * 0.15; 142 | } 143 | } 144 | // console.log("getHeight", height); 145 | return height; 146 | } 147 | getIdeographicBaseline() { 148 | return 0; 149 | } 150 | /** 151 | * Returns the line number of the line that contains the specified UTF-16 152 | * offset within the paragraph, or -1 if the index is out of bounds, or 153 | * points to a codepoint that is logically after the last visible codepoint. 154 | */ 155 | getLineNumberAt(index) { 156 | var _a, _b; 157 | return (_b = (_a = this.getLineMetricsOfRange(index, index)[0]) === null || _a === void 0 ? void 0 : _a.lineNumber) !== null && _b !== void 0 ? _b : 0; 158 | } 159 | getLineMetrics() { 160 | return this._textLayout.lineMetrics; 161 | } 162 | /** 163 | * Returns the LineMetrics of the line at the specified line number, or null 164 | * if the line number is out of bounds, or is larger than or equal to the 165 | * specified max line number. 166 | */ 167 | getLineMetricsAt(lineNumber) { 168 | var _a; 169 | return (_a = this._textLayout.lineMetrics[lineNumber]) !== null && _a !== void 0 ? _a : null; 170 | } 171 | getLineMetricsOfRange(start, end) { 172 | let lineMetrics = []; 173 | this._textLayout.lineMetrics.forEach((it) => { 174 | const range0 = [start, end]; 175 | const range1 = [it.startIndex, it.endIndex]; 176 | const hasIntersection = range0[1] >= range1[0] && range1[1] >= range0[0]; 177 | if (hasIntersection) { 178 | lineMetrics.push(it); 179 | } 180 | }); 181 | return lineMetrics; 182 | } 183 | getLongestLine() { 184 | return 0; 185 | } 186 | getMaxIntrinsicWidth() { 187 | var _a; 188 | const lineMetrics = this.getLineMetrics(); 189 | let maxWidth = 0; 190 | for (let i = 0; i < lineMetrics.length; i++) { 191 | maxWidth = Math.max(maxWidth, (_a = lineMetrics[i].justifyWidth) !== null && _a !== void 0 ? _a : lineMetrics[i].width); 192 | } 193 | // console.log("getMaxIntrinsicWidth", maxWidth); 194 | return maxWidth; 195 | } 196 | getMaxWidth() { 197 | var _a; 198 | const lineMetrics = this.getLineMetrics(); 199 | let maxWidth = 0; 200 | for (let i = 0; i < lineMetrics.length; i++) { 201 | maxWidth = Math.max(maxWidth, (_a = lineMetrics[i].justifyWidth) !== null && _a !== void 0 ? _a : lineMetrics[i].width); 202 | } 203 | // console.log("getMaxWidth", maxWidth); 204 | return maxWidth; 205 | } 206 | getMinIntrinsicWidth() { 207 | const lineMetrics = this.getLineMetrics(); 208 | let width = 0; 209 | for (let i = 0; i < lineMetrics.length; i++) { 210 | width = Math.max(width, lineMetrics[i].width); 211 | } 212 | // console.log("getMinIntrinsicWidth", width); 213 | return width; 214 | } 215 | /** 216 | * Returns the total number of visible lines in the paragraph. 217 | */ 218 | getNumberOfLines() { 219 | return this._textLayout.lineMetrics.length; 220 | } 221 | getRectsForPlaceholders() { 222 | return []; 223 | } 224 | /** 225 | * Returns bounding boxes that enclose all text in the range of glpyh indexes [start, end). 226 | * @param start 227 | * @param end 228 | * @param hStyle 229 | * @param wStyle 230 | */ 231 | getRectsForRange(start, end, hStyle, wStyle) { 232 | this._textLayout.measureGlyphIfNeeded(); 233 | let result = []; 234 | this._textLayout.lineMetrics.forEach((it) => { 235 | const range0 = [start, end]; 236 | const range1 = [it.startIndex, it.endIndex]; 237 | const hasIntersection = range0[1] > range1[0] && range1[1] > range0[0]; 238 | if (hasIntersection) { 239 | const intersecRange = [ 240 | Math.max(range0[0], range1[0]), 241 | Math.min(range0[1], range1[1]), 242 | ]; 243 | let currentLineLeft = -1; 244 | let currentLineTop = -1; 245 | let currentLineWidth = 0; 246 | let currentLineHeight = 0; 247 | for (let index = intersecRange[0]; index < intersecRange[1]; index++) { 248 | const glyphInfo = this._textLayout.glyphInfos[index]; 249 | if (glyphInfo) { 250 | if (currentLineLeft < 0) { 251 | currentLineLeft = glyphInfo.graphemeLayoutBounds[0]; 252 | } 253 | if (currentLineTop < 0) { 254 | currentLineTop = glyphInfo.graphemeLayoutBounds[1]; 255 | } 256 | currentLineTop = Math.min(currentLineTop, glyphInfo.graphemeLayoutBounds[1]); 257 | currentLineWidth = 258 | glyphInfo.graphemeLayoutBounds[2] - currentLineLeft; 259 | currentLineHeight = Math.max(currentLineHeight, glyphInfo.graphemeLayoutBounds[3] - currentLineTop); 260 | } 261 | } 262 | result.push({ 263 | rect: (0, util_1.makeFloat32Array)([ 264 | currentLineLeft, 265 | currentLineTop, 266 | currentLineLeft + currentLineWidth, 267 | currentLineTop + currentLineHeight, 268 | ]), 269 | dir: { value: skia_1.TextDirection.LTR }, 270 | }); 271 | } 272 | }); 273 | if (result.length === 0) { 274 | const lastSpan = this.spans[this.spans.length - 1]; 275 | const lastLine = this._textLayout.lineMetrics[this._textLayout.lineMetrics.length - 1]; 276 | if (end > lastLine.endIndex && 277 | lastSpan instanceof span_1.TextSpan && 278 | lastSpan.originText.endsWith("\n")) { 279 | return [ 280 | { 281 | rect: (0, util_1.makeFloat32Array)([ 282 | 0, 283 | lastLine.yOffset, 284 | 0, 285 | lastLine.yOffset + lastLine.height, 286 | ]), 287 | dir: { value: skia_1.TextDirection.LTR }, 288 | }, 289 | ]; 290 | } 291 | } 292 | return result; 293 | } 294 | /** 295 | * Finds the first and last glyphs that define a word containing the glyph at index offset. 296 | * @param offset 297 | */ 298 | getWordBoundary(offset) { 299 | return { start: offset, end: offset }; 300 | } 301 | /** 302 | * Returns an array of ShapedLine objects, describing the paragraph. 303 | */ 304 | getShapedLines() { 305 | return []; 306 | } 307 | /** 308 | * Lays out the text in the paragraph so it is wrapped to the given width. 309 | * @param width 310 | */ 311 | layout(width) { 312 | if (this.skImageCache) { 313 | this.skImageCache.delete(); 314 | } 315 | this.skImageCache = undefined; 316 | this._textLayout.layout(width); 317 | } 318 | /** 319 | * When called after shaping, returns the glyph IDs which were not matched 320 | * by any of the provided fonts. 321 | */ 322 | unresolvedCodepoints() { 323 | return []; 324 | } 325 | } 326 | exports.Paragraph = Paragraph; 327 | -------------------------------------------------------------------------------- /dist/adapter/paragraph_builder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.ParagraphBuilder = void 0; 7 | const span_1 = require("../impl/span"); 8 | const logger_1 = require("../logger"); 9 | const paragraph_1 = require("./paragraph"); 10 | const skia_1 = require("./skia"); 11 | class ParagraphBuilder extends skia_1.SkEmbindObject { 12 | static MakeFromFontCollection(originMakeFromFontCollectionMethod, style, fontCollection, embeddingFonts, iconFonts) { 13 | var _a; 14 | const fontFamilies = (_a = style.textStyle) === null || _a === void 0 ? void 0 : _a.fontFamilies; 15 | if (fontFamilies && fontFamilies[0] === "MiniTex") { 16 | logger_1.logger.info("use minitex paragraph builder.", fontFamilies); 17 | return new ParagraphBuilder(style); 18 | } 19 | else if (fontFamilies && iconFonts && iconFonts[fontFamilies[0]]) { 20 | logger_1.logger.info("use fontPaths paragraph builder.", fontFamilies); 21 | return new ParagraphBuilder(style, iconFonts[fontFamilies[0]]); 22 | } 23 | else if (ParagraphBuilder.usingPolyfill) { 24 | logger_1.logger.info("usingPolyfill, so use minitex paragraph builder.", fontFamilies); 25 | return new ParagraphBuilder(style); 26 | } 27 | else { 28 | if (fontFamilies) { 29 | if (fontFamilies.filter((it) => { 30 | return embeddingFonts.indexOf(it) >= 0; 31 | }).length === 0) { 32 | logger_1.logger.info("use minitex paragraph builder.", fontFamilies); 33 | return new ParagraphBuilder(style); 34 | } 35 | } 36 | logger_1.logger.info("use skia paragraph builder.", fontFamilies); 37 | return originMakeFromFontCollectionMethod(style, fontCollection); 38 | } 39 | } 40 | constructor(style, iconFontData) { 41 | super(); 42 | this.style = style; 43 | this.iconFontData = iconFontData; 44 | this.isMiniTex = true; 45 | this.spans = []; 46 | this.styles = []; 47 | } 48 | /** 49 | * Pushes the information required to leave an open space. 50 | * @param width 51 | * @param height 52 | * @param alignment 53 | * @param baseline 54 | * @param offset 55 | */ 56 | addPlaceholder(width, height, alignment, baseline, offset) { } 57 | /** 58 | * Adds text to the builder. Forms the proper runs to use the upper-most style 59 | * on the style_stack. 60 | * @param str 61 | */ 62 | addText(str) { 63 | logger_1.logger.debug("ParagraphBuilder.addText", str); 64 | let mergedStyle = {}; 65 | this.styles.forEach((it) => { 66 | Object.assign(mergedStyle, it); 67 | }); 68 | const span = new span_1.TextSpan(str, mergedStyle); 69 | this.spans.push(span); 70 | } 71 | /** 72 | * Returns a Paragraph object that can be used to be layout and paint the text to an 73 | * Canvas. 74 | */ 75 | build() { 76 | return new paragraph_1.Paragraph(this.spans, this.style, this.iconFontData); 77 | } 78 | /** 79 | * @param words is an array of word edges (starting or ending). You can 80 | * pass 2 elements (0 as a start of the entire text and text.size as the 81 | * end). This information is only needed for a specific API method getWords. 82 | * 83 | * The indices are expected to be relative to the UTF-8 representation of 84 | * the text. 85 | */ 86 | setWordsUtf8(words) { } 87 | /** 88 | * @param words is an array of word edges (starting or ending). You can 89 | * pass 2 elements (0 as a start of the entire text and text.size as the 90 | * end). This information is only needed for a specific API method getWords. 91 | * 92 | * The indices are expected to be relative to the UTF-16 representation of 93 | * the text. 94 | * 95 | * The `Intl.Segmenter` API can be used as a source for this data. 96 | */ 97 | setWordsUtf16(words) { } 98 | /** 99 | * @param graphemes is an array of indexes in the input text that point 100 | * to the start of each grapheme. 101 | * 102 | * The indices are expected to be relative to the UTF-8 representation of 103 | * the text. 104 | */ 105 | setGraphemeBreaksUtf8(graphemes) { } 106 | /** 107 | * @param graphemes is an array of indexes in the input text that point 108 | * to the start of each grapheme. 109 | * 110 | * The indices are expected to be relative to the UTF-16 representation of 111 | * the text. 112 | * 113 | * The `Intl.Segmenter` API can be used as a source for this data. 114 | */ 115 | setGraphemeBreaksUtf16(graphemes) { } 116 | /** 117 | * @param lineBreaks is an array of unsigned integers that should be 118 | * treated as pairs (index, break type) that point to the places of possible 119 | * line breaking if needed. It should include 0 as the first element. 120 | * Break type == 0 means soft break, break type == 1 is a hard break. 121 | * 122 | * The indices are expected to be relative to the UTF-8 representation of 123 | * the text. 124 | */ 125 | setLineBreaksUtf8(lineBreaks) { } 126 | /** 127 | * @param lineBreaks is an array of unsigned integers that should be 128 | * treated as pairs (index, break type) that point to the places of possible 129 | * line breaking if needed. It should include 0 as the first element. 130 | * Break type == 0 means soft break, break type == 1 is a hard break. 131 | * 132 | * The indices are expected to be relative to the UTF-16 representation of 133 | * the text. 134 | * 135 | * Chrome's `v8BreakIterator` API can be used as a source for this data. 136 | */ 137 | setLineBreaksUtf16(lineBreaks) { } 138 | /** 139 | * Returns the entire Paragraph text (which is useful in case that text 140 | * was produced as a set of addText calls). 141 | */ 142 | getText() { 143 | let text = ""; 144 | this.spans.forEach((it) => { 145 | if (it instanceof span_1.TextSpan) { 146 | text += it.originText; 147 | } 148 | }); 149 | if (typeof window === "object" && window.TextEncoder) { 150 | const encoder = new window.TextEncoder(); 151 | const view = encoder.encode(text); 152 | return String.fromCharCode(...Array.from(view)); 153 | } 154 | return text; 155 | } 156 | /** 157 | * Remove a style from the stack. Useful to apply different styles to chunks 158 | * of text such as bolding. 159 | */ 160 | pop() { 161 | logger_1.logger.debug("ParagraphBuilder.pop"); 162 | this.styles.pop(); 163 | } 164 | /** 165 | * Push a style to the stack. The corresponding text added with addText will 166 | * use the top-most style. 167 | * @param textStyle 168 | */ 169 | pushStyle(textStyle) { 170 | logger_1.logger.debug("ParagraphBuilder.pushStyle", textStyle); 171 | this.styles.push(textStyle); 172 | } 173 | /** 174 | * Pushes a TextStyle using paints instead of colors for foreground and background. 175 | * @param textStyle 176 | * @param fg 177 | * @param bg 178 | */ 179 | pushPaintStyle(textStyle, fg, bg) { 180 | logger_1.logger.debug("ParagraphBuilder.pushPaintStyle", textStyle, fg, bg); 181 | this.styles.push(textStyle); 182 | } 183 | /** 184 | * Resets this builder to its initial state, discarding any text, styles, placeholders that have 185 | * been added, but keeping the initial ParagraphStyle. 186 | */ 187 | reset() { 188 | logger_1.logger.debug("ParagraphBuilder.reset"); 189 | this.spans = []; 190 | this.styles = []; 191 | } 192 | } 193 | exports.ParagraphBuilder = ParagraphBuilder; 194 | ParagraphBuilder.usingPolyfill = false; 195 | -------------------------------------------------------------------------------- /dist/adapter/skia.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.TextHeightBehavior = exports.DecorationStyle = exports.FontSlant = exports.FontWidth = exports.FontWeight = exports.LineThroughDecoration = exports.OverlineDecoration = exports.UnderlineDecoration = exports.NoDecoration = exports.TextAlign = exports.Affinity = exports.RectWidthStyle = exports.RectHeightStyle = exports.TextDirection = exports.TextBaseline = exports.StrokeJoin = exports.StrokeCap = exports.PlaceholderAlignment = exports.SkEmbindObject = void 0; 7 | class SkEmbindObject { 8 | constructor() { 9 | this._type = ""; 10 | this._deleted = false; 11 | } 12 | delete() { 13 | this._deleted = true; 14 | } 15 | deleteLater() { 16 | this._deleted = true; 17 | } 18 | isAliasOf(other) { 19 | return other._type === this._type; 20 | } 21 | isDeleted() { 22 | return this._deleted; 23 | } 24 | } 25 | exports.SkEmbindObject = SkEmbindObject; 26 | var PlaceholderAlignment; 27 | (function (PlaceholderAlignment) { 28 | PlaceholderAlignment["Baseline"] = "Baseline"; 29 | PlaceholderAlignment["AboveBaseline"] = "AboveBaseline"; 30 | PlaceholderAlignment["BelowBaseline"] = "BelowBaseline"; 31 | PlaceholderAlignment["Top"] = "Top"; 32 | PlaceholderAlignment["Bottom"] = "Bottom"; 33 | PlaceholderAlignment["Middle"] = "Middle"; 34 | })(PlaceholderAlignment || (exports.PlaceholderAlignment = PlaceholderAlignment = {})); 35 | var StrokeCap; 36 | (function (StrokeCap) { 37 | StrokeCap["Butt"] = "Butt"; 38 | StrokeCap["Round"] = "Round"; 39 | StrokeCap["Square"] = "Square"; 40 | })(StrokeCap || (exports.StrokeCap = StrokeCap = {})); 41 | var StrokeJoin; 42 | (function (StrokeJoin) { 43 | StrokeJoin["Bevel"] = "Bevel"; 44 | StrokeJoin["Miter"] = "Miter"; 45 | StrokeJoin["Round"] = "Round"; 46 | })(StrokeJoin || (exports.StrokeJoin = StrokeJoin = {})); 47 | var TextBaseline; 48 | (function (TextBaseline) { 49 | TextBaseline[TextBaseline["Alphabetic"] = 0] = "Alphabetic"; 50 | TextBaseline[TextBaseline["Ideographic"] = 1] = "Ideographic"; 51 | })(TextBaseline || (exports.TextBaseline = TextBaseline = {})); 52 | var TextDirection; 53 | (function (TextDirection) { 54 | TextDirection[TextDirection["RTL"] = 0] = "RTL"; 55 | TextDirection[TextDirection["LTR"] = 1] = "LTR"; 56 | })(TextDirection || (exports.TextDirection = TextDirection = {})); 57 | var RectHeightStyle; 58 | (function (RectHeightStyle) { 59 | RectHeightStyle[RectHeightStyle["Tight"] = 0] = "Tight"; 60 | RectHeightStyle[RectHeightStyle["Max"] = 1] = "Max"; 61 | RectHeightStyle[RectHeightStyle["IncludeLineSpacingMiddle"] = 2] = "IncludeLineSpacingMiddle"; 62 | RectHeightStyle[RectHeightStyle["IncludeLineSpacingTop"] = 3] = "IncludeLineSpacingTop"; 63 | RectHeightStyle[RectHeightStyle["IncludeLineSpacingBottom"] = 4] = "IncludeLineSpacingBottom"; 64 | RectHeightStyle[RectHeightStyle["Strut"] = 5] = "Strut"; 65 | })(RectHeightStyle || (exports.RectHeightStyle = RectHeightStyle = {})); 66 | var RectWidthStyle; 67 | (function (RectWidthStyle) { 68 | RectWidthStyle[RectWidthStyle["Tight"] = 0] = "Tight"; 69 | RectWidthStyle[RectWidthStyle["Max"] = 1] = "Max"; 70 | })(RectWidthStyle || (exports.RectWidthStyle = RectWidthStyle = {})); 71 | var Affinity; 72 | (function (Affinity) { 73 | Affinity[Affinity["Upstream"] = 0] = "Upstream"; 74 | Affinity[Affinity["Downstream"] = 1] = "Downstream"; 75 | })(Affinity || (exports.Affinity = Affinity = {})); 76 | var TextAlign; 77 | (function (TextAlign) { 78 | TextAlign[TextAlign["Left"] = 0] = "Left"; 79 | TextAlign[TextAlign["Right"] = 1] = "Right"; 80 | TextAlign[TextAlign["Center"] = 2] = "Center"; 81 | TextAlign[TextAlign["Justify"] = 3] = "Justify"; 82 | TextAlign[TextAlign["Start"] = 4] = "Start"; 83 | TextAlign[TextAlign["End"] = 5] = "End"; 84 | })(TextAlign || (exports.TextAlign = TextAlign = {})); 85 | exports.NoDecoration = 0; 86 | exports.UnderlineDecoration = 1; 87 | exports.OverlineDecoration = 2; 88 | exports.LineThroughDecoration = 4; 89 | var FontWeight; 90 | (function (FontWeight) { 91 | FontWeight[FontWeight["Invisible"] = 0] = "Invisible"; 92 | FontWeight[FontWeight["Thin"] = 100] = "Thin"; 93 | FontWeight[FontWeight["ExtraLight"] = 200] = "ExtraLight"; 94 | FontWeight[FontWeight["Light"] = 300] = "Light"; 95 | FontWeight[FontWeight["Normal"] = 400] = "Normal"; 96 | FontWeight[FontWeight["Medium"] = 500] = "Medium"; 97 | FontWeight[FontWeight["SemiBold"] = 600] = "SemiBold"; 98 | FontWeight[FontWeight["Bold"] = 700] = "Bold"; 99 | FontWeight[FontWeight["ExtraBold"] = 800] = "ExtraBold"; 100 | FontWeight[FontWeight["Black"] = 900] = "Black"; 101 | FontWeight[FontWeight["ExtraBlack"] = 1000] = "ExtraBlack"; 102 | })(FontWeight || (exports.FontWeight = FontWeight = {})); 103 | var FontWidth; 104 | (function (FontWidth) { 105 | FontWidth[FontWidth["UltraCondensed"] = 0] = "UltraCondensed"; 106 | FontWidth[FontWidth["ExtraCondensed"] = 1] = "ExtraCondensed"; 107 | FontWidth[FontWidth["Condensed"] = 2] = "Condensed"; 108 | FontWidth[FontWidth["SemiCondensed"] = 3] = "SemiCondensed"; 109 | FontWidth[FontWidth["Normal"] = 4] = "Normal"; 110 | FontWidth[FontWidth["SemiExpanded"] = 5] = "SemiExpanded"; 111 | FontWidth[FontWidth["Expanded"] = 6] = "Expanded"; 112 | FontWidth[FontWidth["ExtraExpanded"] = 7] = "ExtraExpanded"; 113 | FontWidth[FontWidth["UltraExpanded"] = 8] = "UltraExpanded"; 114 | })(FontWidth || (exports.FontWidth = FontWidth = {})); 115 | var FontSlant; 116 | (function (FontSlant) { 117 | FontSlant[FontSlant["Upright"] = 0] = "Upright"; 118 | FontSlant[FontSlant["Italic"] = 1] = "Italic"; 119 | FontSlant[FontSlant["Oblique"] = 2] = "Oblique"; 120 | })(FontSlant || (exports.FontSlant = FontSlant = {})); 121 | var DecorationStyle; 122 | (function (DecorationStyle) { 123 | DecorationStyle[DecorationStyle["Solid"] = 0] = "Solid"; 124 | DecorationStyle[DecorationStyle["Double"] = 1] = "Double"; 125 | DecorationStyle[DecorationStyle["Dotted"] = 2] = "Dotted"; 126 | DecorationStyle[DecorationStyle["Dashed"] = 3] = "Dashed"; 127 | DecorationStyle[DecorationStyle["Wavy"] = 4] = "Wavy"; 128 | })(DecorationStyle || (exports.DecorationStyle = DecorationStyle = {})); 129 | var TextHeightBehavior; 130 | (function (TextHeightBehavior) { 131 | TextHeightBehavior[TextHeightBehavior["All"] = 0] = "All"; 132 | TextHeightBehavior[TextHeightBehavior["DisableFirstAscent"] = 1] = "DisableFirstAscent"; 133 | TextHeightBehavior[TextHeightBehavior["DisableLastDescent"] = 2] = "DisableLastDescent"; 134 | TextHeightBehavior[TextHeightBehavior["DisableAll"] = 3] = "DisableAll"; 135 | })(TextHeightBehavior || (exports.TextHeightBehavior = TextHeightBehavior = {})); 136 | -------------------------------------------------------------------------------- /dist/impl/drawer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.Drawer = void 0; 7 | const skia_1 = require("../adapter/skia"); 8 | const skia_2 = require("../adapter/skia"); 9 | const span_1 = require("./span"); 10 | const util_1 = require("../util"); 11 | const logger_1 = require("../logger"); 12 | class Drawer { 13 | constructor(paragraph) { 14 | this.paragraph = paragraph; 15 | } 16 | initCanvas() { 17 | if (!Drawer.sharedRenderCanvas) { 18 | Drawer.sharedRenderCanvas = (0, util_1.createCanvas)(Math.min(4000, 1000 * Drawer.pixelRatio), Math.min(4000, 1000 * Drawer.pixelRatio)); 19 | Drawer.sharedRenderContext = Drawer.sharedRenderCanvas.getContext("2d"); 20 | } 21 | } 22 | draw() { 23 | this.initCanvas(); 24 | const width = (0, util_1.convertToUpwardToPixelRatio)(this.paragraph.getMaxWidth() * Drawer.pixelRatio, Drawer.pixelRatio); 25 | const height = (0, util_1.convertToUpwardToPixelRatio)(this.paragraph.getHeight() * Drawer.pixelRatio, Drawer.pixelRatio); 26 | if (width <= 0 || height <= 0) { 27 | const context = Drawer.sharedRenderContext; 28 | context.clearRect(0, 0, 1, 1); 29 | return context.getImageData(0, 0, 1, 1); 30 | } 31 | const context = Drawer.sharedRenderContext; 32 | context.clearRect(0, 0, width, height); 33 | context.save(); 34 | context.scale(Drawer.pixelRatio, Drawer.pixelRatio); 35 | let didExceedMaxLines = false; 36 | let spanLetterStartIndex = 0; 37 | let linesDrawingRightBounds = {}; 38 | const spans = (0, span_1.spanWithNewline)(this.paragraph.spans); 39 | let linesUndrawed = {}; 40 | this.paragraph.getLineMetrics().forEach((it) => { 41 | linesUndrawed[it.lineNumber] = it.endIndex - it.startIndex; 42 | }); 43 | spans.forEach((span) => { 44 | var _a, _b, _c, _d, _e, _f, _g; 45 | if (didExceedMaxLines) 46 | return; 47 | if (span instanceof span_1.TextSpan) { 48 | if (span instanceof span_1.NewlineSpan) { 49 | spanLetterStartIndex++; 50 | return; 51 | } 52 | let spanUndrawLength = span.charSequence.length; 53 | let spanLetterEndIndex = spanLetterStartIndex + span.charSequence.length; 54 | const lineMetrics = this.paragraph.getLineMetricsOfRange(spanLetterStartIndex, spanLetterEndIndex); 55 | context.font = span.toCanvasFont(); 56 | while (spanUndrawLength > 0) { 57 | let currentDrawText = []; 58 | let currentDrawLine; 59 | for (let index = 0; index < lineMetrics.length; index++) { 60 | const line = lineMetrics[index]; 61 | if (linesUndrawed[line.lineNumber] > 0) { 62 | const currentDrawLength = Math.min(linesUndrawed[line.lineNumber], spanUndrawLength); 63 | currentDrawText = span.charSequence.slice(span.charSequence.length - spanUndrawLength, span.charSequence.length - spanUndrawLength + currentDrawLength); 64 | spanUndrawLength -= currentDrawLength; 65 | linesUndrawed[line.lineNumber] -= currentDrawLength; 66 | currentDrawLine = line; 67 | break; 68 | } 69 | } 70 | if (!currentDrawLine) 71 | break; 72 | if (this.paragraph.didExceedMaxLines() && 73 | this.paragraph.paragraphStyle.maxLines === 74 | currentDrawLine.lineNumber + 1 && 75 | linesUndrawed[currentDrawLine.lineNumber] <= 0) { 76 | const trimLength = (0, util_1.isSquareCharacter)(currentDrawText[currentDrawText.length - 1]) 77 | ? 1 78 | : 3; 79 | currentDrawText = currentDrawText.slice(0, currentDrawText.length - trimLength); 80 | currentDrawText.push(...Array.from((_a = this.paragraph.paragraphStyle.ellipsis) !== null && _a !== void 0 ? _a : "...")); 81 | didExceedMaxLines = true; 82 | } 83 | let drawingLeft = (() => { 84 | var _a, _b; 85 | if (linesDrawingRightBounds[currentDrawLine.lineNumber] === undefined) { 86 | const textAlign = (_a = this.paragraph.paragraphStyle.textAlign) === null || _a === void 0 ? void 0 : _a.value; 87 | const textDirection = (_b = this.paragraph.paragraphStyle.textDirection) === null || _b === void 0 ? void 0 : _b.value; 88 | if (textAlign === skia_1.TextAlign.Center) { 89 | linesDrawingRightBounds[currentDrawLine.lineNumber] = 90 | (this.paragraph.getMaxWidth() - currentDrawLine.width) / 2.0; 91 | } 92 | else if (textAlign === skia_1.TextAlign.Right || 93 | (textAlign === skia_1.TextAlign.End && 94 | textDirection !== skia_1.TextDirection.RTL) || 95 | (textAlign === skia_1.TextAlign.Start && 96 | textDirection === skia_1.TextDirection.RTL)) { 97 | linesDrawingRightBounds[currentDrawLine.lineNumber] = 98 | this.paragraph.getMaxWidth() - currentDrawLine.width; 99 | } 100 | else { 101 | linesDrawingRightBounds[currentDrawLine.lineNumber] = 0; 102 | } 103 | } 104 | return linesDrawingRightBounds[currentDrawLine.lineNumber]; 105 | })(); 106 | const drawingRight = drawingLeft + 107 | (() => { 108 | if (currentDrawText.length === 1 && currentDrawText[0] === "\n") { 109 | return 0; 110 | } 111 | const extraLetterSpacing = span.hasLetterSpacing() 112 | ? currentDrawText.length * span.style.letterSpacing 113 | : 0; 114 | return (context.measureText(currentDrawText.join("")).width + 115 | extraLetterSpacing); 116 | })(); 117 | linesDrawingRightBounds[currentDrawLine.lineNumber] = drawingRight; 118 | const textTop = currentDrawLine.baseline * currentDrawLine.heightMultiplier - 119 | span.letterBaseline; 120 | const textBaseline = currentDrawLine.baseline * currentDrawLine.heightMultiplier; 121 | const textHeight = span.letterHeight; 122 | this.drawBackground(span, context, { 123 | currentDrawLine, 124 | drawingLeft, 125 | drawingRight, 126 | textBaseline, 127 | textTop, 128 | textHeight, 129 | }); 130 | context.save(); 131 | if (span.style.shadows && span.style.shadows.length > 0) { 132 | context.shadowColor = span.style.shadows[0].color 133 | ? (0, util_1.colorToHex)(span.style.shadows[0].color) 134 | : "transparent"; 135 | context.shadowOffsetX = (_c = (_b = span.style.shadows[0].offset) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : 0; 136 | context.shadowOffsetY = (_e = (_d = span.style.shadows[0].offset) === null || _d === void 0 ? void 0 : _d[1]) !== null && _e !== void 0 ? _e : 0; 137 | context.shadowBlur = (_f = span.style.shadows[0].blurRadius) !== null && _f !== void 0 ? _f : 0; 138 | } 139 | context.fillStyle = span.toTextFillStyle(); 140 | if (this.paragraph.iconFontData) { 141 | for (let index = 0; index < currentDrawText.length; index++) { 142 | const currentDrawLetter = currentDrawText[index]; 143 | const letterWidth = (_g = span.style.fontSize) !== null && _g !== void 0 ? _g : 14; 144 | this.fillIcon(context, currentDrawLetter, letterWidth, drawingLeft, textBaseline + currentDrawLine.yOffset); 145 | drawingLeft += letterWidth; 146 | } 147 | } 148 | else if (span.hasLetterSpacing() || 149 | span.hasWordSpacing() || 150 | span.hasJustifySpacing(this.paragraph.paragraphStyle)) { 151 | const letterSpacing = span.hasLetterSpacing() 152 | ? span.style.letterSpacing 153 | : 0; 154 | const justifySpacing = span.hasJustifySpacing(this.paragraph.paragraphStyle) && 155 | !currentDrawLine.isLastLine 156 | ? this.computeJustifySpacing(currentDrawText, currentDrawLine.width, currentDrawLine.justifyWidth) 157 | : 0; 158 | for (let index = 0; index < currentDrawText.length; index++) { 159 | const currentDrawLetter = currentDrawText[index]; 160 | context.fillText(currentDrawLetter, drawingLeft, textBaseline + currentDrawLine.yOffset); 161 | const letterWidth = context.measureText(currentDrawLetter).width; 162 | if (span.hasWordSpacing() && 163 | currentDrawLetter === " " && 164 | (0, util_1.isEnglishWord)(currentDrawText[index - 1])) { 165 | drawingLeft += span.style.wordSpacing; 166 | } 167 | else { 168 | drawingLeft += letterWidth + letterSpacing; 169 | } 170 | if (!(0, util_1.isEnglishWord)(currentDrawText[index])) { 171 | drawingLeft += justifySpacing; 172 | } 173 | } 174 | } 175 | else { 176 | context.fillText(currentDrawText.join(""), drawingLeft, textBaseline + currentDrawLine.yOffset); 177 | } 178 | context.restore(); 179 | logger_1.logger.debug("Drawer.draw.fillText", currentDrawText, drawingLeft, textBaseline + currentDrawLine.yOffset); 180 | this.drawDecoration(span, context, { 181 | currentDrawLine, 182 | drawingLeft, 183 | drawingRight, 184 | textBaseline, 185 | textTop, 186 | textHeight, 187 | }); 188 | if (didExceedMaxLines) { 189 | break; 190 | } 191 | } 192 | spanLetterStartIndex = spanLetterEndIndex; 193 | } 194 | }); 195 | context.restore(); 196 | return context.getImageData(0, 0, width, height); 197 | } 198 | fillIcon(context, text, fontSize, x, y) { 199 | var _a; 200 | const svgPath = (_a = this.paragraph.iconFontMap) === null || _a === void 0 ? void 0 : _a[text]; 201 | if (!svgPath) { 202 | console.log("fill icon not found", text.charCodeAt(0).toString(16)); 203 | return; 204 | } 205 | const pathCommands = svgPath.match(/[A-Za-z]\d+([\.\d,]+)?/g); 206 | if (!pathCommands) 207 | return; 208 | context.save(); 209 | context.beginPath(); 210 | let lastControlPoint = null; 211 | pathCommands.forEach((command) => { 212 | const type = command.charAt(0); 213 | const args = command 214 | .substring(1) 215 | .split(",") 216 | .map(parseFloat) 217 | .map((it, index) => { 218 | let value = it; 219 | if (index % 2 === 1) { 220 | value = 150 - value + 150; 221 | } 222 | return value * (fontSize / 300); 223 | }); 224 | if (type === "M") { 225 | context.moveTo(args[0], args[1]); 226 | } 227 | else if (type === "L") { 228 | context.lineTo(args[0], args[1]); 229 | } 230 | else if (type === "C") { 231 | context.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]); 232 | lastControlPoint = [args[2], args[3]]; 233 | } 234 | else if (type === "Q") { 235 | context.quadraticCurveTo(args[0], args[1], args[2], args[3]); 236 | lastControlPoint = [args[0], args[1]]; 237 | } 238 | else if (type === "A") { 239 | // no need A 240 | } 241 | else if (type === "Z") { 242 | context.closePath(); 243 | } 244 | }); 245 | context.fill(); 246 | context.restore(); 247 | } 248 | computeJustifySpacing(text, lineWidth, justifyWidth) { 249 | let count = 0; 250 | for (let index = 0; index < text.length; index++) { 251 | if (!(0, util_1.isEnglishWord)(text[index])) { 252 | count++; 253 | } 254 | } 255 | return (justifyWidth - lineWidth) / (count - 1); 256 | } 257 | drawBackground(span, context, options) { 258 | if (span.style.backgroundColor) { 259 | const { currentDrawLine, drawingLeft, drawingRight, textTop, textHeight, } = options; 260 | context.fillStyle = span.toBackgroundFillStyle(); 261 | context.fillRect(drawingLeft, textTop + currentDrawLine.yOffset, drawingRight - drawingLeft, textHeight); 262 | } 263 | } 264 | drawDecoration(span, context, options) { 265 | var _a, _b, _c; 266 | const { currentDrawLine, drawingLeft, drawingRight, textBaseline, textTop, textHeight, } = options; 267 | if (span.style.decoration) { 268 | context.save(); 269 | context.strokeStyle = span.toDecorationStrokeStyle(); 270 | context.lineWidth = 271 | ((_a = span.style.decorationThickness) !== null && _a !== void 0 ? _a : 1) * 272 | Math.max(1, ((_b = span.style.fontSize) !== null && _b !== void 0 ? _b : 12) / 14); 273 | const decorationStyle = (_c = span.style.decorationStyle) === null || _c === void 0 ? void 0 : _c.value; 274 | switch (decorationStyle) { 275 | case skia_2.DecorationStyle.Dashed: 276 | context.lineCap = "butt"; 277 | context.setLineDash([4, 2]); 278 | break; 279 | case skia_2.DecorationStyle.Dotted: 280 | context.lineCap = "butt"; 281 | context.setLineDash([2, 2]); 282 | break; 283 | } 284 | if (span.style.decoration === skia_2.UnderlineDecoration) { 285 | context.beginPath(); 286 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textBaseline + 1); 287 | context.lineTo(drawingRight, currentDrawLine.yOffset + textBaseline + 1); 288 | context.stroke(); 289 | if (decorationStyle === skia_2.DecorationStyle.Double) { 290 | context.beginPath(); 291 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textBaseline + 3); 292 | context.lineTo(drawingRight, currentDrawLine.yOffset + textBaseline + 3); 293 | context.stroke(); 294 | } 295 | } 296 | if (span.style.decoration === skia_2.LineThroughDecoration || span.style.decoration === 3) { 297 | context.beginPath(); 298 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textTop + textHeight / 2.0); 299 | context.lineTo(drawingRight, currentDrawLine.yOffset + textTop + textHeight / 2.0); 300 | if (decorationStyle === skia_2.DecorationStyle.Double) { 301 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textTop + textHeight / 2.0 + 2); 302 | context.lineTo(drawingRight, currentDrawLine.yOffset + textTop + textHeight / 2.0 + 2); 303 | } 304 | context.stroke(); 305 | } 306 | if (span.style.decoration === skia_2.OverlineDecoration) { 307 | context.beginPath(); 308 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textTop); 309 | context.lineTo(drawingRight, currentDrawLine.yOffset + textTop); 310 | if (decorationStyle === skia_2.DecorationStyle.Double) { 311 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textTop + 2); 312 | context.lineTo(drawingRight, currentDrawLine.yOffset + textTop + 2); 313 | } 314 | context.stroke(); 315 | } 316 | context.restore(); 317 | } 318 | } 319 | } 320 | exports.Drawer = Drawer; 321 | Drawer.pixelRatio = 1.0; 322 | -------------------------------------------------------------------------------- /dist/impl/layout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.TextLayout = void 0; 7 | const skia_1 = require("../adapter/skia"); 8 | const skia_2 = require("../adapter/skia"); 9 | const logger_1 = require("../logger"); 10 | const util_1 = require("../util"); 11 | const span_1 = require("./span"); 12 | class LetterMeasurer { 13 | static measureLetters(span, context) { 14 | let advances = [0]; 15 | let curPosWidth = 0; 16 | for (let index = 0; index < span.charSequence.length; index++) { 17 | const letter = span.charSequence[index]; 18 | let wordWidth = (() => { 19 | if ((0, util_1.isSquareCharacter)(letter)) { 20 | return this.measureSquareCharacter(context); 21 | } 22 | else { 23 | return this.measureNormalLetter(letter, context); 24 | } 25 | })(); 26 | if (span.hasWordSpacing() && 27 | letter === " " && 28 | (0, util_1.isEnglishWord)(span.charSequence[index - 1])) { 29 | wordWidth = span.style.wordSpacing; 30 | } 31 | else if (span.hasLetterSpacing()) { 32 | wordWidth += span.style.letterSpacing; 33 | } 34 | curPosWidth += wordWidth; 35 | advances.push(curPosWidth); 36 | } 37 | return { advances }; 38 | } 39 | static measureNormalLetter(letter, context) { 40 | var _a; 41 | const width = (_a = this.widthFromCache(context, letter)) !== null && _a !== void 0 ? _a : context.measureText(letter).width; 42 | this.setWidthToCache(context, letter, width); 43 | return width; 44 | } 45 | static measureSquareCharacter(context) { 46 | var _a; 47 | const width = (_a = this.widthFromCache(context, "测")) !== null && _a !== void 0 ? _a : context.measureText("测").width; 48 | this.setWidthToCache(context, "测", width); 49 | return width; 50 | } 51 | static widthFromCache(context, word) { 52 | var _a; 53 | const cacheKey = context.font + "_" + word; 54 | return (_a = this.measureLRUCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.width; 55 | } 56 | static setWidthToCache(context, word, width) { 57 | const cacheKey = context.font + "_" + word; 58 | if (this.measureLRUCache[cacheKey]) { 59 | this.measureLRUCache[cacheKey].useCount++; 60 | return; 61 | } 62 | this.measureLRUCache[cacheKey] = { 63 | useCount: 1, 64 | width: width, 65 | }; 66 | if (Object.keys(this.measureLRUCache).length > this.LRUConfig.maxCacheCount) { 67 | this.clearCache(); 68 | } 69 | } 70 | static clearCache() { 71 | const keys = Object.keys(this.measureLRUCache).sort((a, b) => { 72 | return this.measureLRUCache[a].useCount > this.measureLRUCache[b].useCount 73 | ? 1 74 | : -1; 75 | }); 76 | keys 77 | .slice(0, this.LRUConfig.maxCacheCount - this.LRUConfig.minCacheCount) 78 | .forEach((it) => { 79 | delete this.measureLRUCache[it]; 80 | }); 81 | } 82 | } 83 | LetterMeasurer.LRUConfig = { 84 | maxCacheCount: 1000, 85 | minCacheCount: 200, 86 | }; 87 | LetterMeasurer.measureLRUCache = {}; 88 | class TextLayout { 89 | constructor(paragraph) { 90 | this.paragraph = paragraph; 91 | this.glyphInfos = []; 92 | this.lineMetrics = []; 93 | this.didExceedMaxLines = false; 94 | this.previousLayoutWidth = 0; 95 | } 96 | initCanvas() { 97 | if (!TextLayout.sharedLayoutCanvas) { 98 | TextLayout.sharedLayoutCanvas = (0, util_1.createCanvas)(1, 1); 99 | TextLayout.sharedLayoutContext = 100 | TextLayout.sharedLayoutCanvas.getContext("2d"); 101 | } 102 | } 103 | measureGlyphIfNeeded() { 104 | if (Object.keys(this.glyphInfos).length <= 0) { 105 | this.layout(-1, true); 106 | } 107 | } 108 | layout(layoutWidth, forceCalcGlyphInfos = false) { 109 | var _a, _b; 110 | let layoutStartTime; 111 | if (logger_1.logger.profileMode) { 112 | layoutStartTime = new Date().getTime(); 113 | } 114 | if (layoutWidth < 0) { 115 | layoutWidth = this.previousLayoutWidth; 116 | } 117 | this.previousLayoutWidth = layoutWidth; 118 | this.initCanvas(); 119 | this.glyphInfos = []; 120 | let currentLineMetrics = { 121 | startIndex: 0, 122 | endIndex: 0, 123 | endExcludingWhitespaces: 0, 124 | endIncludingNewline: 0, 125 | isHardBreak: false, 126 | ascent: 0, 127 | descent: 0, 128 | height: 0, 129 | heightMultiplier: Math.max(1, ((_a = this.paragraph.paragraphStyle.heightMultiplier) !== null && _a !== void 0 ? _a : 1.5) / 1.5), 130 | width: 0, 131 | justifyWidth: ((_b = this.paragraph.paragraphStyle.textAlign) === null || _b === void 0 ? void 0 : _b.value) === skia_1.TextAlign.Justify 132 | ? layoutWidth 133 | : undefined, 134 | left: 0, 135 | yOffset: 0, 136 | baseline: 0, 137 | lineNumber: 0, 138 | isLastLine: false, 139 | }; 140 | let lineMetrics = []; 141 | const spans = (0, span_1.spanWithNewline)(this.paragraph.spans); 142 | spans.forEach((span) => { 143 | var _a, _b, _c, _d; 144 | if (span instanceof span_1.TextSpan) { 145 | TextLayout.sharedLayoutContext.font = span.toCanvasFont(); 146 | const matrics = TextLayout.sharedLayoutContext.measureText(span.originText); 147 | let iconFontWidth = 0; 148 | if (this.paragraph.iconFontData) { 149 | const fontSize = (_a = span.style.fontSize) !== null && _a !== void 0 ? _a : 14; 150 | iconFontWidth = fontSize; 151 | currentLineMetrics.ascent = fontSize; 152 | currentLineMetrics.descent = 0; 153 | span.letterBaseline = fontSize; 154 | span.letterHeight = fontSize; 155 | } 156 | else { 157 | const mHeight = TextLayout.sharedLayoutContext.measureText("M").width; 158 | currentLineMetrics.ascent = mHeight * 1.15; 159 | currentLineMetrics.descent = mHeight * 0.35; 160 | span.letterBaseline = mHeight * 1.15; 161 | span.letterHeight = mHeight * 1.15 + mHeight * 0.35; 162 | } 163 | if (span.style.heightMultiplier && span.style.heightMultiplier > 0) { 164 | currentLineMetrics.heightMultiplier = Math.max(currentLineMetrics.heightMultiplier, span.style.heightMultiplier / 1.5); 165 | } 166 | currentLineMetrics.height = Math.max(currentLineMetrics.height, currentLineMetrics.ascent + currentLineMetrics.descent); 167 | currentLineMetrics.baseline = Math.max(currentLineMetrics.baseline, currentLineMetrics.ascent); 168 | if (this.paragraph.iconFontData) { 169 | const textWidth = span.charSequence.length * iconFontWidth; 170 | currentLineMetrics.endIndex += span.charSequence.length; 171 | currentLineMetrics.width += textWidth; 172 | } 173 | else if (currentLineMetrics.width + matrics.width < layoutWidth && 174 | !span.hasLetterSpacing() && 175 | !span.hasWordSpacing() && 176 | !forceCalcGlyphInfos) { 177 | // fast measure 178 | if (span instanceof span_1.NewlineSpan) { 179 | const newLineMatrics = this.createNewLine(currentLineMetrics); 180 | lineMetrics.push(currentLineMetrics); 181 | currentLineMetrics = newLineMatrics; 182 | } 183 | else { 184 | currentLineMetrics.endIndex += span.charSequence.length; 185 | currentLineMetrics.width += matrics.width; 186 | if (((_c = (_b = span.style.fontStyle) === null || _b === void 0 ? void 0 : _b.slant) === null || _c === void 0 ? void 0 : _c.value) === skia_2.FontSlant.Italic) { 187 | currentLineMetrics.width += 2; 188 | } 189 | } 190 | } 191 | else { 192 | let letterMeasureResult = LetterMeasurer.measureLetters(span, TextLayout.sharedLayoutContext); 193 | let advances = letterMeasureResult.advances; 194 | if (span instanceof span_1.NewlineSpan) { 195 | advances = [0, 0]; 196 | } 197 | if (Math.abs(advances[advances.length - 1] - layoutWidth) < 10 && 198 | layoutWidth === this.previousLayoutWidth) { 199 | layoutWidth = advances[advances.length - 1]; 200 | } 201 | let currentWord = ""; 202 | let currentWordWidth = 0; 203 | let currentWordLength = 0; 204 | let nextWordWidth = 0; 205 | let canBreak = true; 206 | let forceBreak = false; 207 | for (let index = 0; index < span.charSequence.length; index++) { 208 | const letter = span.charSequence[index]; 209 | currentWord += letter; 210 | let currentLetterLeft = currentWordWidth; 211 | let spanEnded = span.charSequence[index + 1] === undefined; 212 | let nextWord = (_d = currentWord + span.charSequence[index + 1]) !== null && _d !== void 0 ? _d : ""; 213 | if (advances[index + 1] === undefined) { 214 | currentWordWidth += advances[index] - advances[index - 1]; 215 | } 216 | else { 217 | currentWordWidth += advances[index + 1] - advances[index]; 218 | } 219 | if (advances[index + 2] === undefined) { 220 | nextWordWidth = currentWordWidth; 221 | } 222 | else { 223 | nextWordWidth = 224 | currentWordWidth + (advances[index + 2] - advances[index + 1]); 225 | } 226 | currentWordLength += 1; 227 | canBreak = true; 228 | forceBreak = false; 229 | if (spanEnded) { 230 | canBreak = true; 231 | } 232 | else if ((0, util_1.isEnglishWord)(nextWord)) { 233 | canBreak = false; 234 | } 235 | if ((0, util_1.isPunctuation)(nextWord[nextWord.length - 1]) && 236 | currentLineMetrics.width + nextWordWidth >= layoutWidth) { 237 | forceBreak = true; 238 | } 239 | if (span instanceof span_1.NewlineSpan) { 240 | forceBreak = true; 241 | } 242 | const currentGlyphLeft = currentLineMetrics.width + currentLetterLeft; 243 | const currentGlyphTop = currentLineMetrics.yOffset; 244 | const currentGlyphWidth = (() => { 245 | if (advances[index + 1] === undefined) { 246 | return advances[index] - advances[index - 1]; 247 | } 248 | else { 249 | return advances[index + 1] - advances[index]; 250 | } 251 | })(); 252 | const currentGlyphHeight = currentLineMetrics.height; 253 | const currentGlyphInfo = { 254 | graphemeLayoutBounds: (0, util_1.valueOfRectXYWH)(currentGlyphLeft, currentGlyphTop, currentGlyphWidth, currentGlyphHeight), 255 | graphemeClusterTextRange: { start: index, end: index + 1 }, 256 | dir: { value: skia_1.TextDirection.LTR }, 257 | isEllipsis: false, 258 | }; 259 | this.glyphInfos.push(currentGlyphInfo); 260 | if (!canBreak) { 261 | continue; 262 | } 263 | else if (!forceBreak && 264 | currentLineMetrics.width + currentWordWidth <= layoutWidth) { 265 | currentLineMetrics.width += currentWordWidth; 266 | currentLineMetrics.endIndex += currentWordLength; 267 | currentWord = ""; 268 | currentWordWidth = 0; 269 | currentWordLength = 0; 270 | canBreak = true; 271 | } 272 | else if (forceBreak || 273 | currentLineMetrics.width + currentWordWidth > layoutWidth) { 274 | const newLineMatrics = this.createNewLine(currentLineMetrics); 275 | lineMetrics.push(currentLineMetrics); 276 | currentLineMetrics = newLineMatrics; 277 | currentLineMetrics.width += currentWordWidth; 278 | currentLineMetrics.endIndex += currentWordLength; 279 | currentWord = ""; 280 | currentWordWidth = 0; 281 | currentWordLength = 0; 282 | canBreak = true; 283 | } 284 | } 285 | if (currentWord.length > 0) { 286 | currentLineMetrics.width += currentWordWidth; 287 | currentLineMetrics.endIndex += currentWordLength; 288 | } 289 | } 290 | } 291 | }); 292 | lineMetrics.push(currentLineMetrics); 293 | if (this.paragraph.paragraphStyle.maxLines && 294 | lineMetrics.length > this.paragraph.paragraphStyle.maxLines) { 295 | this.didExceedMaxLines = true; 296 | lineMetrics = lineMetrics.slice(0, this.paragraph.paragraphStyle.maxLines); 297 | } 298 | else { 299 | this.didExceedMaxLines = false; 300 | } 301 | logger_1.logger.debug("TextLayout.layout.lineMetrics", lineMetrics); 302 | if (logger_1.logger.profileMode) { 303 | const layoutCostTime = new Date().getTime() - layoutStartTime; 304 | logger_1.logger.profile("Layout cost", layoutCostTime); 305 | } 306 | lineMetrics[lineMetrics.length - 1].isLastLine = true; 307 | this.lineMetrics = lineMetrics; 308 | } 309 | createNewLine(currentLineMetrics) { 310 | var _a; 311 | return { 312 | startIndex: currentLineMetrics.endIndex, 313 | endIndex: currentLineMetrics.endIndex, 314 | endExcludingWhitespaces: 0, 315 | endIncludingNewline: 0, 316 | isHardBreak: false, 317 | ascent: currentLineMetrics.ascent, 318 | descent: currentLineMetrics.descent, 319 | height: currentLineMetrics.height, 320 | heightMultiplier: Math.max(1, ((_a = this.paragraph.paragraphStyle.heightMultiplier) !== null && _a !== void 0 ? _a : 1.5) / 1.5), 321 | width: 0, 322 | justifyWidth: currentLineMetrics.justifyWidth, 323 | left: 0, 324 | yOffset: currentLineMetrics.yOffset + 325 | currentLineMetrics.height * currentLineMetrics.heightMultiplier + 326 | currentLineMetrics.height * 0.15, // 行间距 327 | baseline: currentLineMetrics.baseline, 328 | lineNumber: currentLineMetrics.lineNumber + 1, 329 | isLastLine: false, 330 | }; 331 | } 332 | } 333 | exports.TextLayout = TextLayout; 334 | -------------------------------------------------------------------------------- /dist/impl/span.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.spanWithNewline = exports.NewlineSpan = exports.TextSpan = exports.Span = void 0; 7 | const skia_1 = require("../adapter/skia"); 8 | const util_1 = require("../util"); 9 | class Span { 10 | constructor() { 11 | this.letterBaseline = 0; 12 | this.letterHeight = 0; 13 | this.lettersBounding = []; 14 | } 15 | } 16 | exports.Span = Span; 17 | class TextSpan extends Span { 18 | constructor(text, style) { 19 | super(); 20 | this.text = text; 21 | this.style = style; 22 | this.charSequence = Array.from(text); 23 | this.originText = text; 24 | } 25 | hasLetterSpacing() { 26 | return (this.style.letterSpacing !== undefined && this.style.letterSpacing > 1); 27 | } 28 | hasWordSpacing() { 29 | return this.style.wordSpacing !== undefined && this.style.wordSpacing > 1; 30 | } 31 | hasJustifySpacing(paragraphStyle) { 32 | var _a; 33 | return ((_a = paragraphStyle.textAlign) === null || _a === void 0 ? void 0 : _a.value) === skia_1.TextAlign.Justify; 34 | } 35 | toBackgroundFillStyle() { 36 | if (this.style.backgroundColor) { 37 | return (0, util_1.colorToHex)(this.style.backgroundColor); 38 | } 39 | else { 40 | return "#000000"; 41 | } 42 | } 43 | toTextFillStyle() { 44 | if (this.style.color) { 45 | return (0, util_1.colorToHex)(this.style.color); 46 | } 47 | else { 48 | return "#000000"; 49 | } 50 | } 51 | toDecorationStrokeStyle() { 52 | if (this.style.decorationColor) { 53 | return (0, util_1.colorToHex)(this.style.decorationColor); 54 | } 55 | else { 56 | return "#000000"; 57 | } 58 | } 59 | toCanvasFont() { 60 | var _a, _b, _c, _d; 61 | let font = `${this.style.fontSize}px system-ui, Roboto`; 62 | const fontWeight = (_b = (_a = this.style.fontStyle) === null || _a === void 0 ? void 0 : _a.weight) === null || _b === void 0 ? void 0 : _b.value; 63 | if (fontWeight && fontWeight !== 400) { 64 | if (fontWeight >= 900) { 65 | font = "900 " + font; 66 | } 67 | else { 68 | font = fontWeight.toFixed(0) + " " + font; 69 | } 70 | } 71 | const slant = (_d = (_c = this.style.fontStyle) === null || _c === void 0 ? void 0 : _c.slant) === null || _d === void 0 ? void 0 : _d.value; 72 | if (slant) { 73 | switch (slant) { 74 | case skia_1.FontSlant.Italic: 75 | font = "italic " + font; 76 | break; 77 | case skia_1.FontSlant.Oblique: 78 | font = "oblique " + font; 79 | break; 80 | } 81 | } 82 | return font; 83 | } 84 | } 85 | exports.TextSpan = TextSpan; 86 | class NewlineSpan extends TextSpan { 87 | constructor() { 88 | super("\n", {}); 89 | } 90 | } 91 | exports.NewlineSpan = NewlineSpan; 92 | const spanWithNewline = (spans) => { 93 | let result = []; 94 | spans.forEach((span) => { 95 | if (span instanceof TextSpan) { 96 | if (span.originText.indexOf("\n") >= 0) { 97 | const components = span.originText.split("\n"); 98 | for (let index = 0; index < components.length; index++) { 99 | const component = components[index]; 100 | if (index > 0) { 101 | result.push(new NewlineSpan()); 102 | } 103 | result.push(new TextSpan(component, span.style)); 104 | } 105 | return; 106 | } 107 | } 108 | result.push(span); 109 | }); 110 | return result; 111 | }; 112 | exports.spanWithNewline = spanWithNewline; 113 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.MiniTex = void 0; 7 | const drawer_1 = require("./impl/drawer"); 8 | const paragraph_1 = require("./adapter/paragraph"); 9 | const paragraph_builder_1 = require("./adapter/paragraph_builder"); 10 | const logger_1 = require("./logger"); 11 | const polyfill_1 = require("./polyfill"); 12 | // import { logger } from "./logger"; 13 | class MiniTex { 14 | static install(canvasKit, pixelRatio, embeddingFonts, iconFonts) { 15 | if (typeof canvasKit.ParagraphBuilder === "undefined") { 16 | (0, polyfill_1.installPolyfill)(canvasKit); 17 | paragraph_builder_1.ParagraphBuilder.usingPolyfill = true; 18 | } 19 | // logger.profileMode = true; 20 | logger_1.logger.setLogLevel(logger_1.LogLevel.ERROR); 21 | drawer_1.Drawer.pixelRatio = pixelRatio; 22 | const originMakeFromFontCollectionMethod = canvasKit.ParagraphBuilder.MakeFromFontCollection; 23 | canvasKit.ParagraphBuilder.MakeFromFontCollection = function (style, fontCollection) { 24 | return paragraph_builder_1.ParagraphBuilder.MakeFromFontCollection(originMakeFromFontCollectionMethod, style, fontCollection, embeddingFonts, iconFonts); 25 | }; 26 | const originDrawParagraphMethod = canvasKit.Canvas.prototype.drawParagraph; 27 | canvasKit.Canvas.prototype.drawParagraph = function (paragraph, dx, dy) { 28 | if (paragraph.isMiniTex === true) { 29 | (0, paragraph_1.drawParagraph)(canvasKit, this, paragraph, dx, dy); 30 | } 31 | else { 32 | originDrawParagraphMethod.apply(this, [paragraph, dx, dy]); 33 | } 34 | }; 35 | } 36 | } 37 | exports.MiniTex = MiniTex; 38 | -------------------------------------------------------------------------------- /dist/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.logger = exports.Logger = exports.LogLevel = void 0; 7 | var LogLevel; 8 | (function (LogLevel) { 9 | LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG"; 10 | LogLevel[LogLevel["INFO"] = 1] = "INFO"; 11 | LogLevel[LogLevel["WARN"] = 2] = "WARN"; 12 | LogLevel[LogLevel["ERROR"] = 3] = "ERROR"; 13 | })(LogLevel || (exports.LogLevel = LogLevel = {})); 14 | class Logger { 15 | constructor(logLevel = LogLevel.ERROR) { 16 | this.profileMode = false; 17 | this.logLevel = logLevel; 18 | } 19 | setLogLevel(logLevel = LogLevel.DEBUG) { 20 | this.logLevel = logLevel; 21 | } 22 | log(level, ...args) { 23 | if (level >= this.logLevel) { 24 | const message = args.length === 1 ? args[0] : args; 25 | console.log(`[${LogLevel[level]}]`, ...message); 26 | } 27 | } 28 | debug(...args) { 29 | this.log(LogLevel.DEBUG, ...args); 30 | } 31 | info(...args) { 32 | this.log(LogLevel.INFO, ...args); 33 | } 34 | warn(...args) { 35 | this.log(LogLevel.WARN, ...args); 36 | } 37 | error(...args) { 38 | this.log(LogLevel.ERROR, ...args); 39 | } 40 | profile(...args) { 41 | if (this.profileMode) { 42 | console.info("[PROFILE]", ...args); 43 | } 44 | } 45 | } 46 | exports.Logger = Logger; 47 | exports.logger = new Logger(LogLevel.ERROR); 48 | -------------------------------------------------------------------------------- /dist/polyfill.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.installPolyfill = void 0; 4 | const skia_1 = require("./adapter/skia"); 5 | const polyfill_types_1 = require("./polyfill.types"); 6 | const util_1 = require("./util"); 7 | const installPolyfill = (canvasKit) => { 8 | canvasKit.ParagraphBuilder = new _ParagraphBuilderFactory(); 9 | canvasKit.FontCollection = new _FontCollectionFactory(); 10 | canvasKit.FontMgr = new _FontMgrFactory(); 11 | canvasKit.Typeface = new _TypefaceFactory(); 12 | canvasKit.TypefaceFontProvider = new _TypefaceFontProviderFactory(); 13 | canvasKit.Font = _Font; 14 | canvasKit.ParagraphStyle = (properties) => { 15 | return new _ParagraphStyle(properties); 16 | }; 17 | canvasKit.TextStyle = (properties) => { 18 | return new _TextStyle(properties); 19 | }; 20 | // Paragraph Enums 21 | canvasKit.TextAlign = new polyfill_types_1.TextAlignEnumValues(); 22 | canvasKit.TextDirection = new polyfill_types_1.TextDirectionEnumValues(); 23 | canvasKit.TextBaseline = new polyfill_types_1.TextBaselineEnumValues(); 24 | canvasKit.RectHeightStyle = new polyfill_types_1.RectHeightStyleEnumValues(); 25 | canvasKit.RectWidthStyle = new polyfill_types_1.RectWidthStyleEnumValues(); 26 | canvasKit.Affinity = new polyfill_types_1.AffinityEnumValues(); 27 | canvasKit.FontWeight = new polyfill_types_1.FontWeightEnumValues(); 28 | canvasKit.FontWidth = new polyfill_types_1.FontWidthEnumValues(); 29 | canvasKit.FontSlant = new polyfill_types_1.FontSlantEnumValues(); 30 | canvasKit.DecorationStyle = new polyfill_types_1.DecorationStyleEnumValues(); 31 | canvasKit.TextHeightBehavior = new polyfill_types_1.TextHeightBehaviorEnumValues(); 32 | canvasKit.PlaceholderAlignment = new polyfill_types_1.PlaceholderAlignmentEnumValues(); 33 | // Paragraph Constants 34 | canvasKit.NoDecoration = 0; 35 | canvasKit.UnderlineDecoration = 1; 36 | canvasKit.OverlineDecoration = 2; 37 | canvasKit.LineThroughDecoration = 3; 38 | }; 39 | exports.installPolyfill = installPolyfill; 40 | class _ParagraphBuilderFactory { 41 | Make(style, fontManager) { 42 | return this.MakeFromFontCollection(style, {}); 43 | } 44 | MakeFromFontCollection(style, fontCollection) { 45 | throw new Error("MakeFromFontCollection not implemented."); 46 | } 47 | RequiresClientICU() { 48 | return false; 49 | } 50 | } 51 | class _ParagraphStyle extends skia_1.SkEmbindObject { 52 | constructor(properties) { 53 | super(); 54 | Object.assign(this, properties); 55 | } 56 | } 57 | class _TextStyle extends skia_1.SkEmbindObject { 58 | constructor(properties) { 59 | super(); 60 | Object.assign(this, properties); 61 | } 62 | } 63 | class _FontCollection extends skia_1.SkEmbindObject { 64 | setDefaultFontManager(fontManager) { } 65 | enableFontFallback() { } 66 | } 67 | class _FontCollectionFactory { 68 | Make() { 69 | return new _FontCollection(); 70 | } 71 | } 72 | class _FontMgr extends skia_1.SkEmbindObject { 73 | countFamilies() { 74 | return 0; 75 | } 76 | getFamilyName(index) { 77 | return ""; 78 | } 79 | } 80 | class _FontMgrFactory { 81 | FromData(...buffers) { 82 | return new _FontMgr(); 83 | } 84 | } 85 | class _TypefaceFactory { 86 | GetDefault() { 87 | return new _Typeface(); 88 | } 89 | MakeTypefaceFromData(fontData) { 90 | return new _Typeface(); 91 | } 92 | MakeFreeTypeFaceFromData(fontData) { 93 | return new _Typeface(); 94 | } 95 | } 96 | class _TypefaceFontProvider extends skia_1.SkEmbindObject { 97 | registerFont(bytes, family) { } 98 | countFamilies() { 99 | return 0; 100 | } 101 | getFamilyName(index) { 102 | return ""; 103 | } 104 | } 105 | class _TypefaceFontProviderFactory { 106 | Make() { 107 | return new _TypefaceFontProvider(); 108 | } 109 | } 110 | class _Typeface extends skia_1.SkEmbindObject { 111 | getGlyphIDs(str, numCodePoints, output) { 112 | return (0, util_1.makeUint16Array)([]); 113 | } 114 | } 115 | class _Font extends skia_1.SkEmbindObject { 116 | constructor(face, size, scaleX, skewX) { 117 | super(); 118 | } 119 | getMetrics() { 120 | return { ascent: 0, descent: 0, leading: 0 }; 121 | } 122 | getGlyphBounds(glyphs, paint, output) { 123 | return (0, util_1.makeFloat32Array)([0, 0, 0, 0]); 124 | } 125 | getGlyphIDs(str, numCodePoints, output) { 126 | return (0, util_1.makeUint16Array)([]); 127 | } 128 | getGlyphWidths(glyphs, paint, output) { 129 | return (0, util_1.makeFloat32Array)([]); 130 | } 131 | getGlyphIntercepts(glyphs, positions, top, bottom) { 132 | return (0, util_1.makeFloat32Array)([]); 133 | } 134 | getScaleX() { 135 | return 1; 136 | } 137 | getSize() { 138 | return 0; 139 | } 140 | getSkewX() { 141 | return 1; 142 | } 143 | isEmbolden() { 144 | return false; 145 | } 146 | getTypeface() { 147 | return new _Typeface(); 148 | } 149 | setEdging(edging) { } 150 | setEmbeddedBitmaps(embeddedBitmaps) { } 151 | setHinting(hinting) { } 152 | setLinearMetrics(linearMetrics) { } 153 | setScaleX(sx) { } 154 | setSize(points) { } 155 | setSkewX(sx) { } 156 | setEmbolden(embolden) { } 157 | setSubpixel(subpixel) { } 158 | setTypeface(face) { } 159 | } 160 | -------------------------------------------------------------------------------- /dist/polyfill.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.PlaceholderAlignmentEnumValues = exports.TextHeightBehaviorEnumValues = exports.DecorationStyleEnumValues = exports.FontSlantEnumValues = exports.FontWidthEnumValues = exports.FontWeightEnumValues = exports.AffinityEnumValues = exports.RectWidthStyleEnumValues = exports.RectHeightStyleEnumValues = exports.TextBaselineEnumValues = exports.TextDirectionEnumValues = exports.TextAlignEnumValues = exports.FontHinting = exports.FontEdging = void 0; 4 | var FontEdging; 5 | (function (FontEdging) { 6 | FontEdging[FontEdging["Alias"] = 0] = "Alias"; 7 | FontEdging[FontEdging["AntiAlias"] = 1] = "AntiAlias"; 8 | FontEdging[FontEdging["SubpixelAntiAlias"] = 2] = "SubpixelAntiAlias"; 9 | })(FontEdging || (exports.FontEdging = FontEdging = {})); 10 | var FontHinting; 11 | (function (FontHinting) { 12 | FontHinting[FontHinting["None"] = 0] = "None"; 13 | FontHinting[FontHinting["Slight"] = 1] = "Slight"; 14 | FontHinting[FontHinting["Normal"] = 2] = "Normal"; 15 | FontHinting[FontHinting["Full"] = 3] = "Full"; 16 | })(FontHinting || (exports.FontHinting = FontHinting = {})); 17 | class TextAlignEnumValues { 18 | constructor() { 19 | this.Left = { value: 0 }; 20 | this.Right = { value: 1 }; 21 | this.Center = { value: 2 }; 22 | this.Justify = { value: 3 }; 23 | this.Start = { value: 4 }; 24 | this.End = { value: 5 }; 25 | } 26 | } 27 | exports.TextAlignEnumValues = TextAlignEnumValues; 28 | class TextDirectionEnumValues { 29 | constructor() { 30 | this.RTL = { value: 0 }; 31 | this.LTR = { value: 1 }; 32 | } 33 | } 34 | exports.TextDirectionEnumValues = TextDirectionEnumValues; 35 | class TextBaselineEnumValues { 36 | constructor() { 37 | this.Alphabetic = { value: 0 }; 38 | this.Ideographic = { value: 1 }; 39 | } 40 | } 41 | exports.TextBaselineEnumValues = TextBaselineEnumValues; 42 | class RectHeightStyleEnumValues { 43 | constructor() { 44 | this.Tight = { value: 0 }; 45 | this.Max = { value: 1 }; 46 | this.IncludeLineSpacingMiddle = { value: 2 }; 47 | this.IncludeLineSpacingTop = { value: 3 }; 48 | this.IncludeLineSpacingBottom = { value: 4 }; 49 | this.Strut = { value: 5 }; 50 | } 51 | } 52 | exports.RectHeightStyleEnumValues = RectHeightStyleEnumValues; 53 | class RectWidthStyleEnumValues { 54 | constructor() { 55 | this.Tight = { value: 0 }; 56 | this.Max = { value: 1 }; 57 | } 58 | } 59 | exports.RectWidthStyleEnumValues = RectWidthStyleEnumValues; 60 | class AffinityEnumValues { 61 | constructor() { 62 | this.Upstream = { value: 0 }; 63 | this.Downstream = { value: 1 }; 64 | } 65 | } 66 | exports.AffinityEnumValues = AffinityEnumValues; 67 | class FontWeightEnumValues { 68 | constructor() { 69 | this.Invisible = { value: 0 }; 70 | this.Thin = { value: 100 }; 71 | this.ExtraLight = { value: 200 }; 72 | this.Light = { value: 300 }; 73 | this.Normal = { value: 400 }; 74 | this.Medium = { value: 500 }; 75 | this.SemiBold = { value: 600 }; 76 | this.Bold = { value: 700 }; 77 | this.ExtraBold = { value: 800 }; 78 | this.Black = { value: 900 }; 79 | this.ExtraBlack = { value: 1000 }; 80 | } 81 | } 82 | exports.FontWeightEnumValues = FontWeightEnumValues; 83 | class FontWidthEnumValues { 84 | constructor() { 85 | this.UltraCondensed = { value: 0 }; 86 | this.ExtraCondensed = { value: 1 }; 87 | this.Condensed = { value: 2 }; 88 | this.SemiCondensed = { value: 3 }; 89 | this.Normal = { value: 4 }; 90 | this.SemiExpanded = { value: 5 }; 91 | this.Expanded = { value: 6 }; 92 | this.ExtraExpanded = { value: 7 }; 93 | this.UltraExpanded = { value: 8 }; 94 | } 95 | } 96 | exports.FontWidthEnumValues = FontWidthEnumValues; 97 | class FontSlantEnumValues { 98 | constructor() { 99 | this.Upright = { value: 0 }; 100 | this.Italic = { value: 1 }; 101 | this.Oblique = { value: 2 }; 102 | } 103 | } 104 | exports.FontSlantEnumValues = FontSlantEnumValues; 105 | class DecorationStyleEnumValues { 106 | constructor() { 107 | this.Solid = { value: 0 }; 108 | this.Double = { value: 1 }; 109 | this.Dotted = { value: 2 }; 110 | this.Dashed = { value: 3 }; 111 | this.Wavy = { value: 4 }; 112 | } 113 | } 114 | exports.DecorationStyleEnumValues = DecorationStyleEnumValues; 115 | class TextHeightBehaviorEnumValues { 116 | constructor() { 117 | this.All = { value: 0 }; 118 | this.DisableFirstAscent = { value: 1 }; 119 | this.DisableLastDescent = { value: 2 }; 120 | this.DisableAll = { value: 3 }; 121 | } 122 | } 123 | exports.TextHeightBehaviorEnumValues = TextHeightBehaviorEnumValues; 124 | class PlaceholderAlignmentEnumValues { 125 | constructor() { 126 | this.Baseline = { value: 0 }; 127 | this.AboveBaseline = { value: 1 }; 128 | this.BelowBaseline = { value: 2 }; 129 | this.Top = { value: 3 }; 130 | this.Bottom = { value: 4 }; 131 | this.Middle = { value: 5 }; 132 | } 133 | } 134 | exports.PlaceholderAlignmentEnumValues = PlaceholderAlignmentEnumValues; 135 | -------------------------------------------------------------------------------- /dist/target.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.appTarget = void 0; 4 | exports.appTarget = "normal"; 5 | -------------------------------------------------------------------------------- /dist/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 3 | // Use of this source code is governed by a Apache License Version 2.0 that can be 4 | // found in the LICENSE file. 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.createCanvas = exports.convertToUpwardToPixelRatio = exports.isPunctuation = exports.isSquareCharacter = exports.isEnglishWord = exports.valueOfRectXYWH = exports.valueOfRGBAInt = exports.colorToHex = exports.makeUint16Array = exports.makeFloat32Array = void 0; 7 | const target_1 = require("./target"); 8 | const makeFloat32Array = (arr) => { 9 | if (target_1.appTarget === "wegame") { 10 | return arr; 11 | } 12 | return new Float32Array(arr); 13 | }; 14 | exports.makeFloat32Array = makeFloat32Array; 15 | const makeUint16Array = (arr) => { 16 | if (target_1.appTarget === "wegame") { 17 | return arr; 18 | } 19 | return new Uint16Array(arr); 20 | }; 21 | exports.makeUint16Array = makeUint16Array; 22 | const colorToHex = (rgbaColor) => { 23 | const r = Math.round(rgbaColor[0] * 255).toString(16); 24 | const g = Math.round(rgbaColor[1] * 255).toString(16); 25 | const b = Math.round(rgbaColor[2] * 255).toString(16); 26 | const a = Math.round(rgbaColor[3] * 255).toString(16); 27 | const padHex = (hex) => (hex.length === 1 ? "0" + hex : hex); 28 | const hexColor = "#" + padHex(r) + padHex(g) + padHex(b) + padHex(a); 29 | return hexColor; 30 | }; 31 | exports.colorToHex = colorToHex; 32 | const valueOfRGBAInt = (r, g, b, a) => { 33 | return Float32Array.from([r, g, b, a]); 34 | }; 35 | exports.valueOfRGBAInt = valueOfRGBAInt; 36 | const valueOfRectXYWH = (x, y, w, h) => { 37 | return Float32Array.from([x, y, x + w, y + h]); 38 | }; 39 | exports.valueOfRectXYWH = valueOfRectXYWH; 40 | function isEnglishWord(str) { 41 | const englishRegex = /^[A-Za-z,.]+$/; 42 | const result = englishRegex.test(str); 43 | return result; 44 | } 45 | exports.isEnglishWord = isEnglishWord; 46 | function isSquareCharacter(str) { 47 | const squareCharacterRange = /[\u4e00-\u9fa5]/; 48 | return squareCharacterRange.test(str); 49 | } 50 | exports.isSquareCharacter = isSquareCharacter; 51 | const mapOfPunctuation = { 52 | "!": 1, 53 | "?": 1, 54 | "。": 1, 55 | ",": 1, 56 | "、": 1, 57 | "“": 1, 58 | "”": 1, 59 | "‘": 1, 60 | "’": 1, 61 | ";": 1, 62 | ":": 1, 63 | "【": 1, 64 | "】": 1, 65 | "『": 1, 66 | "』": 1, 67 | "(": 1, 68 | ")": 1, 69 | "《": 1, 70 | "》": 1, 71 | "〈": 1, 72 | "〉": 1, 73 | "〔": 1, 74 | "〕": 1, 75 | "[": 1, 76 | "]": 1, 77 | "{": 1, 78 | "}": 1, 79 | "〖": 1, 80 | "〗": 1, 81 | "〘": 1, 82 | "〙": 1, 83 | "〚": 1, 84 | "〛": 1, 85 | "〝": 1, 86 | "〞": 1, 87 | "〟": 1, 88 | "﹏": 1, 89 | "…": 1, 90 | "—": 1, 91 | "~": 1, 92 | "·": 1, 93 | "•": 1, 94 | ",": 1, 95 | ".": 1, 96 | }; 97 | function isPunctuation(char) { 98 | return mapOfPunctuation[char] === 1; 99 | } 100 | exports.isPunctuation = isPunctuation; 101 | function convertToUpwardToPixelRatio(number, pixelRatio) { 102 | const upwardInt = Math.ceil(number); 103 | const remainder = upwardInt % pixelRatio; 104 | return remainder === 0 ? upwardInt : upwardInt + (pixelRatio - remainder); 105 | } 106 | exports.convertToUpwardToPixelRatio = convertToUpwardToPixelRatio; 107 | function createCanvas(width, height) { 108 | if (typeof wx === "object" && 109 | typeof wx.createOffscreenCanvas === "function") { 110 | return wx.createOffscreenCanvas({ 111 | type: "2d", 112 | width: width, 113 | height: height, 114 | }); 115 | } 116 | else if (typeof wx === "object" && typeof wx.createCanvas === "function") { 117 | return wx.createCanvas({ 118 | type: "2d", 119 | width: width, 120 | height: height, 121 | }); 122 | } 123 | else if (typeof window === "object") { 124 | const canvas = document.createElement("canvas"); 125 | canvas.width = width; 126 | canvas.height = height; 127 | return canvas; 128 | } 129 | else { 130 | throw "can not create canvas"; 131 | } 132 | } 133 | exports.createCanvas = createCanvas; 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minitex", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "build:web": "tsc && browserify dist/index.js --standalone MiniTex > minitex.js && terser --compress -- minitex.js > minitex.min.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "browserify": "^17.0.0", 13 | "terser": "^5.27.1", 14 | "typescript": "^5.3.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/adapter/paint.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | import { valueOfRGBAInt } from "../util"; 6 | import { Color, SkEmbindObject, StrokeCap, StrokeJoin } from "./skia"; 7 | 8 | export class Paint extends SkEmbindObject { 9 | public _type = "SkPaint"; 10 | 11 | /** 12 | * Returns a copy of this paint. 13 | */ 14 | copy(): Paint { 15 | const newValue = new Paint(); 16 | Object.assign(newValue, this); 17 | return newValue; 18 | } 19 | 20 | private _color: Color = valueOfRGBAInt(0, 0, 0, 255); 21 | 22 | /** 23 | * Retrieves the alpha and RGB unpremultiplied. RGB are extended sRGB values 24 | * (sRGB gamut, and encoded with the sRGB transfer function). 25 | */ 26 | getColor(): Color { 27 | return this._color; 28 | } 29 | 30 | private _strokeCap = StrokeCap.Butt; 31 | 32 | /** 33 | * Returns the geometry drawn at the beginning and end of strokes. 34 | */ 35 | getStrokeCap(): StrokeCap { 36 | return this._strokeCap; 37 | } 38 | 39 | private _strokeJoin = StrokeJoin.Bevel; 40 | 41 | /** 42 | * Returns the geometry drawn at the corners of strokes. 43 | */ 44 | getStrokeJoin(): StrokeJoin { 45 | return this._strokeJoin; 46 | } 47 | 48 | private _strokeMiter = 0; 49 | 50 | /** 51 | * Returns the limit at which a sharp corner is drawn beveled. 52 | */ 53 | getStrokeMiter(): number { 54 | return this._strokeMiter; 55 | } 56 | 57 | private _strokeWidth = 0; 58 | 59 | /** 60 | * Returns the thickness of the pen used to outline the shape. 61 | */ 62 | getStrokeWidth(): number { 63 | return this._strokeWidth; 64 | } 65 | 66 | private _alpha = 1.0; 67 | 68 | /** 69 | * Replaces alpha, leaving RGBA unchanged. 0 means fully transparent, 1.0 means opaque. 70 | * @param alpha 71 | */ 72 | setAlphaf(alpha: number): void { 73 | this._alpha = alpha; 74 | } 75 | 76 | private _antiAlias = true; 77 | 78 | /** 79 | * Requests, but does not require, that edge pixels draw opaque or with 80 | * partial transparency. 81 | * @param aa 82 | */ 83 | setAntiAlias(aa: boolean): void { 84 | this._antiAlias = aa; 85 | } 86 | 87 | /** 88 | * Sets the blend mode that is, the mode used to combine source color 89 | * with destination color. 90 | * @param mode 91 | */ 92 | setBlendMode(mode: any): void {} 93 | 94 | /** 95 | * Sets the current blender, increasing its refcnt, and if a blender is already 96 | * present, decreasing that object's refcnt. 97 | * 98 | * * A nullptr blender signifies the default SrcOver behavior. 99 | * 100 | * * For convenience, you can call setBlendMode() if the blend effect can be expressed 101 | * as one of those values. 102 | * @param blender 103 | */ 104 | setBlender(blender: any): void {} 105 | 106 | /** 107 | * Sets alpha and RGB used when stroking and filling. The color is four floating 108 | * point values, unpremultiplied. The color values are interpreted as being in 109 | * the provided colorSpace. 110 | * @param color 111 | * @param colorSpace - defaults to sRGB 112 | */ 113 | setColor(color: Color): void { 114 | this._color = color; 115 | } 116 | 117 | /** 118 | * Sets alpha and RGB used when stroking and filling. The color is four floating 119 | * point values, unpremultiplied. The color values are interpreted as being in 120 | * the provided colorSpace. 121 | * @param r 122 | * @param g 123 | * @param b 124 | * @param a 125 | * @param colorSpace - defaults to sRGB 126 | */ 127 | setColorComponents(r: number, g: number, b: number, a: number): void { 128 | this.setColor(valueOfRGBAInt(r, g, b, a)); 129 | } 130 | 131 | /** 132 | * Sets the current color filter, replacing the existing one if there was one. 133 | * @param filter 134 | */ 135 | setColorFilter(filter: any): void {} 136 | 137 | /** 138 | * Sets the color used when stroking and filling. The color values are interpreted as being in 139 | * the provided colorSpace. 140 | * @param color 141 | * @param colorSpace - defaults to sRGB. 142 | */ 143 | setColorInt(color: any, colorSpace?: any): void {} 144 | 145 | /** 146 | * Requests, but does not require, to distribute color error. 147 | * @param shouldDither 148 | */ 149 | setDither(shouldDither: boolean): void {} 150 | 151 | /** 152 | * Sets the current image filter, replacing the existing one if there was one. 153 | * @param filter 154 | */ 155 | setImageFilter(filter: any): void {} 156 | 157 | /** 158 | * Sets the current mask filter, replacing the existing one if there was one. 159 | * @param filter 160 | */ 161 | setMaskFilter(filter: any): void {} 162 | 163 | /** 164 | * Sets the current path effect, replacing the existing one if there was one. 165 | * @param effect 166 | */ 167 | setPathEffect(effect: any): void {} 168 | 169 | /** 170 | * Sets the current shader, replacing the existing one if there was one. 171 | * @param shader 172 | */ 173 | setShader(shader: any): void {} 174 | 175 | /** 176 | * Sets the geometry drawn at the beginning and end of strokes. 177 | * @param cap 178 | */ 179 | setStrokeCap(cap: StrokeCap): void { 180 | this._strokeCap = cap; 181 | } 182 | 183 | /** 184 | * Sets the geometry drawn at the corners of strokes. 185 | * @param join 186 | */ 187 | setStrokeJoin(join: StrokeJoin): void { 188 | this._strokeJoin = join; 189 | } 190 | 191 | /** 192 | * Sets the limit at which a sharp corner is drawn beveled. 193 | * @param limit 194 | */ 195 | setStrokeMiter(limit: number): void { 196 | this._strokeMiter = limit; 197 | } 198 | 199 | /** 200 | * Sets the thickness of the pen used to outline the shape. 201 | * @param width 202 | */ 203 | setStrokeWidth(width: number): void { 204 | this._strokeWidth = width; 205 | } 206 | 207 | /** 208 | * Sets whether the geometry is filled or stroked. 209 | * @param style 210 | */ 211 | setStyle(style: any): void {} 212 | } 213 | -------------------------------------------------------------------------------- /src/adapter/paragraph.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | import { Drawer } from "../impl/drawer"; 6 | import { TextLayout } from "../impl/layout"; 7 | import { Span, TextSpan } from "../impl/span"; 8 | import { logger } from "../logger"; 9 | import { makeFloat32Array } from "../util"; 10 | import { 11 | Affinity, 12 | SkEmbindObject, 13 | GlyphInfo, 14 | LineMetrics, 15 | ParagraphStyle, 16 | PositionWithAffinity, 17 | RectHeightStyle, 18 | RectWidthStyle, 19 | RectWithDirection, 20 | ShapedLine, 21 | SkEnum, 22 | TextDirection, 23 | URange, 24 | } from "./skia"; 25 | 26 | let drawParagraphSharedPaint: any; 27 | 28 | export const drawParagraph = function ( 29 | CanvasKit: any, 30 | skCanvas: any, 31 | paragraph: Paragraph, 32 | dx: number, 33 | dy: number 34 | ) { 35 | let drawStartTime!: number; 36 | if (logger.profileMode) { 37 | drawStartTime = new Date().getTime(); 38 | } 39 | let canvasImg = paragraph.skImageCache; 40 | if (!canvasImg) { 41 | const drawer = new Drawer(paragraph); 42 | const imageData = drawer.draw(); 43 | canvasImg = CanvasKit.MakeImage( 44 | { 45 | width: imageData.width, 46 | height: imageData.height, 47 | alphaType: CanvasKit.AlphaType.Unpremul, 48 | colorType: CanvasKit.ColorType.RGBA_8888, 49 | colorSpace: CanvasKit.ColorSpace.SRGB, 50 | }, 51 | imageData.data, 52 | 4 * imageData.width 53 | ); 54 | paragraph.skImageCache = canvasImg; 55 | paragraph.skImageWidth = imageData.width; 56 | paragraph.skImageHeight = imageData.height; 57 | } 58 | const srcRect = CanvasKit.XYWHRect( 59 | 0, 60 | 0, 61 | paragraph.skImageWidth!, 62 | paragraph.skImageHeight! 63 | ); 64 | const dstRect = CanvasKit.XYWHRect( 65 | Math.ceil(dx), 66 | Math.ceil(dy), 67 | paragraph.skImageWidth! / Drawer.pixelRatio, 68 | paragraph.skImageHeight! / Drawer.pixelRatio 69 | ); 70 | const skPaint = drawParagraphSharedPaint ?? new CanvasKit.Paint(); 71 | drawParagraphSharedPaint = skPaint; 72 | skCanvas.drawImageRect(canvasImg, srcRect, dstRect, skPaint); 73 | if (logger.profileMode) { 74 | const drawCostTime = new Date().getTime() - drawStartTime; 75 | logger.profile("drawParagraph cost", drawCostTime); 76 | } 77 | }; 78 | 79 | export class Paragraph extends SkEmbindObject { 80 | iconFontMap?: Record; 81 | 82 | constructor( 83 | readonly spans: Span[], 84 | readonly paragraphStyle: ParagraphStyle, 85 | readonly iconFontData?: string 86 | ) { 87 | super(); 88 | if (this.iconFontData) { 89 | this.iconFontMap = JSON.parse(this.iconFontData); 90 | } 91 | } 92 | 93 | delete(): void { 94 | if (this.skImageCache) { 95 | this.skImageCache.delete(); 96 | this.skImageCache = undefined; 97 | } 98 | super.delete(); 99 | } 100 | 101 | public _type = "SkParagraph"; 102 | public isMiniTex = true; 103 | public skImageCache?: SkEmbindObject; 104 | public skImageWidth?: number; 105 | public skImageHeight?: number; 106 | private _textLayout = new TextLayout(this); 107 | 108 | didExceedMaxLines(): boolean { 109 | return this._textLayout.didExceedMaxLines; 110 | } 111 | 112 | getAlphabeticBaseline(): number { 113 | return 0; 114 | } 115 | 116 | /** 117 | * Returns the index of the glyph that corresponds to the provided coordinate, 118 | * with the top left corner as the origin, and +y direction as down. 119 | */ 120 | getGlyphPositionAtCoordinate(dx: number, dy: number): PositionWithAffinity { 121 | this._textLayout.measureGlyphIfNeeded(); 122 | for (let index = 0; index < this._textLayout.glyphInfos.length; index++) { 123 | const glyphInfo = this._textLayout.glyphInfos[index]; 124 | const left = glyphInfo.graphemeLayoutBounds[0]; 125 | const top = glyphInfo.graphemeLayoutBounds[1]; 126 | const width = glyphInfo.graphemeLayoutBounds[2] - left; 127 | const height = glyphInfo.graphemeLayoutBounds[3] - top; 128 | if (dx >= left && dx <= left + width && dy >= top && dy <= top + height) { 129 | return { pos: index, affinity: { value: Affinity.Downstream } }; 130 | } 131 | } 132 | for (let index = 0; index < this._textLayout.lineMetrics.length; index++) { 133 | const lineMetrics = this._textLayout.lineMetrics[index]; 134 | const isLastLine = index === this._textLayout.lineMetrics.length - 1; 135 | const left = 0; 136 | const top = lineMetrics.yOffset; 137 | const width = lineMetrics.width; 138 | const height = lineMetrics.height; 139 | if (dy >= top && dy <= top + height) { 140 | if (dx <= 0) { 141 | return { 142 | pos: lineMetrics.startIndex, 143 | affinity: { value: Affinity.Downstream }, 144 | }; 145 | } else if (dx >= width) { 146 | return { 147 | pos: lineMetrics.endIndex, 148 | affinity: { value: Affinity.Downstream }, 149 | }; 150 | } 151 | } 152 | if (dy >= top + height && isLastLine) { 153 | return { 154 | pos: lineMetrics.endIndex, 155 | affinity: { value: Affinity.Downstream }, 156 | }; 157 | } 158 | } 159 | return { pos: 0, affinity: { value: Affinity.Upstream } }; 160 | } 161 | 162 | /** 163 | * Returns the information associated with the closest glyph at the specified 164 | * paragraph coordinate, or null if the paragraph is empty. 165 | */ 166 | getClosestGlyphInfoAtCoordinate(dx: number, dy: number): GlyphInfo | null { 167 | return this.getGlyphInfoAt(this.getGlyphPositionAtCoordinate(dx, dy).pos); 168 | } 169 | 170 | /** 171 | * Returns the information associated with the glyph at the specified UTF-16 172 | * offset within the paragraph's visible lines, or null if the index is out 173 | * of bounds, or points to a codepoint that is logically after the last 174 | * visible codepoint. 175 | */ 176 | getGlyphInfoAt(index: number): GlyphInfo | null { 177 | this._textLayout.measureGlyphIfNeeded(); 178 | return this._textLayout.glyphInfos[index] ?? null; 179 | } 180 | 181 | getHeight(): number { 182 | const lineMetrics = this.getLineMetrics(); 183 | let height = 0; 184 | for (let i = 0; i < lineMetrics.length; i++) { 185 | height += lineMetrics[i].height * lineMetrics[i].heightMultiplier; 186 | if (i > 0 && i < lineMetrics.length) { 187 | height += lineMetrics[i].height * 0.15; 188 | } 189 | } 190 | // console.log("getHeight", height); 191 | return height; 192 | } 193 | 194 | getIdeographicBaseline(): number { 195 | return 0; 196 | } 197 | 198 | /** 199 | * Returns the line number of the line that contains the specified UTF-16 200 | * offset within the paragraph, or -1 if the index is out of bounds, or 201 | * points to a codepoint that is logically after the last visible codepoint. 202 | */ 203 | getLineNumberAt(index: number): number { 204 | return this.getLineMetricsOfRange(index, index)[0]?.lineNumber ?? 0; 205 | } 206 | 207 | getLineMetrics(): LineMetrics[] { 208 | return this._textLayout.lineMetrics; 209 | } 210 | 211 | /** 212 | * Returns the LineMetrics of the line at the specified line number, or null 213 | * if the line number is out of bounds, or is larger than or equal to the 214 | * specified max line number. 215 | */ 216 | getLineMetricsAt(lineNumber: number): LineMetrics | null { 217 | return this._textLayout.lineMetrics[lineNumber] ?? null; 218 | } 219 | 220 | getLineMetricsOfRange(start: number, end: number): LineMetrics[] { 221 | let lineMetrics: LineMetrics[] = []; 222 | this._textLayout.lineMetrics.forEach((it) => { 223 | const range0 = [start, end]; 224 | const range1 = [it.startIndex, it.endIndex]; 225 | const hasIntersection = range0[1] >= range1[0] && range1[1] >= range0[0]; 226 | if (hasIntersection) { 227 | lineMetrics.push(it); 228 | } 229 | }); 230 | return lineMetrics; 231 | } 232 | 233 | getLongestLine(): number { 234 | return 0; 235 | } 236 | 237 | getMaxIntrinsicWidth(): number { 238 | const lineMetrics = this.getLineMetrics(); 239 | let maxWidth = 0; 240 | for (let i = 0; i < lineMetrics.length; i++) { 241 | maxWidth = Math.max( 242 | maxWidth, 243 | lineMetrics[i].justifyWidth ?? lineMetrics[i].width 244 | ); 245 | } 246 | // console.log("getMaxIntrinsicWidth", maxWidth); 247 | return maxWidth; 248 | } 249 | 250 | getMaxWidth(): number { 251 | const lineMetrics = this.getLineMetrics(); 252 | let maxWidth = 0; 253 | for (let i = 0; i < lineMetrics.length; i++) { 254 | maxWidth = Math.max( 255 | maxWidth, 256 | lineMetrics[i].justifyWidth ?? lineMetrics[i].width 257 | ); 258 | } 259 | // console.log("getMaxWidth", maxWidth); 260 | return maxWidth; 261 | } 262 | 263 | getMinIntrinsicWidth(): number { 264 | const lineMetrics = this.getLineMetrics(); 265 | let width = 0; 266 | for (let i = 0; i < lineMetrics.length; i++) { 267 | width = Math.max(width, lineMetrics[i].width); 268 | } 269 | // console.log("getMinIntrinsicWidth", width); 270 | return width; 271 | } 272 | 273 | /** 274 | * Returns the total number of visible lines in the paragraph. 275 | */ 276 | getNumberOfLines(): number { 277 | return this._textLayout.lineMetrics.length; 278 | } 279 | 280 | getRectsForPlaceholders(): RectWithDirection[] { 281 | return []; 282 | } 283 | 284 | /** 285 | * Returns bounding boxes that enclose all text in the range of glpyh indexes [start, end). 286 | * @param start 287 | * @param end 288 | * @param hStyle 289 | * @param wStyle 290 | */ 291 | getRectsForRange( 292 | start: number, 293 | end: number, 294 | hStyle: SkEnum, 295 | wStyle: SkEnum 296 | ): RectWithDirection[] { 297 | this._textLayout.measureGlyphIfNeeded(); 298 | let result: RectWithDirection[] = []; 299 | this._textLayout.lineMetrics.forEach((it) => { 300 | const range0 = [start, end]; 301 | const range1 = [it.startIndex, it.endIndex]; 302 | const hasIntersection = range0[1] > range1[0] && range1[1] > range0[0]; 303 | if (hasIntersection) { 304 | const intersecRange = [ 305 | Math.max(range0[0], range1[0]), 306 | Math.min(range0[1], range1[1]), 307 | ]; 308 | let currentLineLeft = -1; 309 | let currentLineTop = -1; 310 | let currentLineWidth = 0; 311 | let currentLineHeight = 0; 312 | for (let index = intersecRange[0]; index < intersecRange[1]; index++) { 313 | const glyphInfo = this._textLayout.glyphInfos[index]; 314 | if (glyphInfo) { 315 | if (currentLineLeft < 0) { 316 | currentLineLeft = glyphInfo.graphemeLayoutBounds[0]; 317 | } 318 | if (currentLineTop < 0) { 319 | currentLineTop = glyphInfo.graphemeLayoutBounds[1]; 320 | } 321 | currentLineTop = Math.min( 322 | currentLineTop, 323 | glyphInfo.graphemeLayoutBounds[1] 324 | ); 325 | currentLineWidth = 326 | glyphInfo.graphemeLayoutBounds[2] - currentLineLeft; 327 | currentLineHeight = Math.max( 328 | currentLineHeight, 329 | glyphInfo.graphemeLayoutBounds[3] - currentLineTop 330 | ); 331 | } 332 | } 333 | result.push({ 334 | rect: makeFloat32Array([ 335 | currentLineLeft, 336 | currentLineTop, 337 | currentLineLeft + currentLineWidth, 338 | currentLineTop + currentLineHeight, 339 | ]), 340 | dir: { value: TextDirection.LTR }, 341 | }); 342 | } 343 | }); 344 | if (result.length === 0) { 345 | const lastSpan = this.spans[this.spans.length - 1]; 346 | const lastLine = 347 | this._textLayout.lineMetrics[this._textLayout.lineMetrics.length - 1]; 348 | if ( 349 | end > lastLine.endIndex && 350 | lastSpan instanceof TextSpan && 351 | lastSpan.originText.endsWith("\n") 352 | ) { 353 | return [ 354 | { 355 | rect: makeFloat32Array([ 356 | 0, 357 | lastLine.yOffset, 358 | 0, 359 | lastLine.yOffset + lastLine.height, 360 | ]), 361 | dir: { value: TextDirection.LTR }, 362 | }, 363 | ]; 364 | } 365 | } 366 | return result; 367 | } 368 | 369 | /** 370 | * Finds the first and last glyphs that define a word containing the glyph at index offset. 371 | * @param offset 372 | */ 373 | getWordBoundary(offset: number): URange { 374 | return { start: offset, end: offset }; 375 | } 376 | 377 | /** 378 | * Returns an array of ShapedLine objects, describing the paragraph. 379 | */ 380 | getShapedLines(): ShapedLine[] { 381 | return []; 382 | } 383 | 384 | /** 385 | * Lays out the text in the paragraph so it is wrapped to the given width. 386 | * @param width 387 | */ 388 | layout(width: number): void { 389 | if (this.skImageCache) { 390 | this.skImageCache.delete(); 391 | } 392 | this.skImageCache = undefined; 393 | this._textLayout.layout(width); 394 | } 395 | 396 | /** 397 | * When called after shaping, returns the glyph IDs which were not matched 398 | * by any of the provided fonts. 399 | */ 400 | unresolvedCodepoints(): number[] { 401 | return []; 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/adapter/paragraph_builder.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | import { Span, TextSpan } from "../impl/span"; 6 | import { logger } from "../logger"; 7 | import { Paint } from "./paint"; 8 | import { Paragraph } from "./paragraph"; 9 | import { 10 | SkEmbindObject, 11 | InputGraphemes, 12 | InputLineBreaks, 13 | InputWords, 14 | ParagraphStyle, 15 | PlaceholderAlignment, 16 | TextBaseline, 17 | } from "./skia"; 18 | import { TextStyle } from "./skia"; 19 | 20 | export class ParagraphBuilder extends SkEmbindObject { 21 | static usingPolyfill = false; 22 | 23 | static MakeFromFontCollection( 24 | originMakeFromFontCollectionMethod: ( 25 | style: ParagraphStyle, 26 | fontCollection: any 27 | ) => any, 28 | style: ParagraphStyle, 29 | fontCollection: any, 30 | embeddingFonts: string[], 31 | iconFonts?: Record 32 | ) { 33 | const fontFamilies = style.textStyle?.fontFamilies; 34 | if (fontFamilies && fontFamilies[0] === "MiniTex") { 35 | logger.info("use minitex paragraph builder.", fontFamilies); 36 | return new ParagraphBuilder(style); 37 | } else if (fontFamilies && iconFonts && iconFonts[fontFamilies[0]]) { 38 | logger.info("use fontPaths paragraph builder.", fontFamilies); 39 | return new ParagraphBuilder(style, iconFonts[fontFamilies[0]]); 40 | } else if (ParagraphBuilder.usingPolyfill) { 41 | logger.info( 42 | "usingPolyfill, so use minitex paragraph builder.", 43 | fontFamilies 44 | ); 45 | return new ParagraphBuilder(style); 46 | } else { 47 | if (fontFamilies) { 48 | if ( 49 | fontFamilies.filter((it) => { 50 | return embeddingFonts.indexOf(it) >= 0; 51 | }).length === 0 52 | ) { 53 | logger.info("use minitex paragraph builder.", fontFamilies); 54 | return new ParagraphBuilder(style); 55 | } 56 | } 57 | logger.info("use skia paragraph builder.", fontFamilies); 58 | return originMakeFromFontCollectionMethod(style, fontCollection); 59 | } 60 | } 61 | 62 | constructor(readonly style: ParagraphStyle, readonly iconFontData?: string) { 63 | super(); 64 | } 65 | 66 | isMiniTex = true; 67 | 68 | private spans: Span[] = []; 69 | private styles: TextStyle[] = []; 70 | 71 | /** 72 | * Pushes the information required to leave an open space. 73 | * @param width 74 | * @param height 75 | * @param alignment 76 | * @param baseline 77 | * @param offset 78 | */ 79 | addPlaceholder( 80 | width?: number, 81 | height?: number, 82 | alignment?: PlaceholderAlignment, 83 | baseline?: TextBaseline, 84 | offset?: number 85 | ): void {} 86 | 87 | /** 88 | * Adds text to the builder. Forms the proper runs to use the upper-most style 89 | * on the style_stack. 90 | * @param str 91 | */ 92 | addText(str: string): void { 93 | logger.debug("ParagraphBuilder.addText", str); 94 | let mergedStyle: TextStyle = {}; 95 | this.styles.forEach((it) => { 96 | Object.assign(mergedStyle, it); 97 | }); 98 | const span = new TextSpan(str, mergedStyle); 99 | this.spans.push(span); 100 | } 101 | 102 | /** 103 | * Returns a Paragraph object that can be used to be layout and paint the text to an 104 | * Canvas. 105 | */ 106 | build(): Paragraph { 107 | return new Paragraph(this.spans, this.style, this.iconFontData); 108 | } 109 | 110 | /** 111 | * @param words is an array of word edges (starting or ending). You can 112 | * pass 2 elements (0 as a start of the entire text and text.size as the 113 | * end). This information is only needed for a specific API method getWords. 114 | * 115 | * The indices are expected to be relative to the UTF-8 representation of 116 | * the text. 117 | */ 118 | setWordsUtf8(words: InputWords): void {} 119 | /** 120 | * @param words is an array of word edges (starting or ending). You can 121 | * pass 2 elements (0 as a start of the entire text and text.size as the 122 | * end). This information is only needed for a specific API method getWords. 123 | * 124 | * The indices are expected to be relative to the UTF-16 representation of 125 | * the text. 126 | * 127 | * The `Intl.Segmenter` API can be used as a source for this data. 128 | */ 129 | setWordsUtf16(words: InputWords): void {} 130 | 131 | /** 132 | * @param graphemes is an array of indexes in the input text that point 133 | * to the start of each grapheme. 134 | * 135 | * The indices are expected to be relative to the UTF-8 representation of 136 | * the text. 137 | */ 138 | setGraphemeBreaksUtf8(graphemes: InputGraphemes): void {} 139 | /** 140 | * @param graphemes is an array of indexes in the input text that point 141 | * to the start of each grapheme. 142 | * 143 | * The indices are expected to be relative to the UTF-16 representation of 144 | * the text. 145 | * 146 | * The `Intl.Segmenter` API can be used as a source for this data. 147 | */ 148 | setGraphemeBreaksUtf16(graphemes: InputGraphemes): void {} 149 | 150 | /** 151 | * @param lineBreaks is an array of unsigned integers that should be 152 | * treated as pairs (index, break type) that point to the places of possible 153 | * line breaking if needed. It should include 0 as the first element. 154 | * Break type == 0 means soft break, break type == 1 is a hard break. 155 | * 156 | * The indices are expected to be relative to the UTF-8 representation of 157 | * the text. 158 | */ 159 | setLineBreaksUtf8(lineBreaks: InputLineBreaks): void {} 160 | /** 161 | * @param lineBreaks is an array of unsigned integers that should be 162 | * treated as pairs (index, break type) that point to the places of possible 163 | * line breaking if needed. It should include 0 as the first element. 164 | * Break type == 0 means soft break, break type == 1 is a hard break. 165 | * 166 | * The indices are expected to be relative to the UTF-16 representation of 167 | * the text. 168 | * 169 | * Chrome's `v8BreakIterator` API can be used as a source for this data. 170 | */ 171 | setLineBreaksUtf16(lineBreaks: InputLineBreaks): void {} 172 | 173 | /** 174 | * Returns the entire Paragraph text (which is useful in case that text 175 | * was produced as a set of addText calls). 176 | */ 177 | getText(): string { 178 | let text = ""; 179 | this.spans.forEach((it) => { 180 | if (it instanceof TextSpan) { 181 | text += it.originText; 182 | } 183 | }); 184 | if (typeof window === "object" && window.TextEncoder) { 185 | const encoder = new window.TextEncoder(); 186 | const view = encoder.encode(text); 187 | return String.fromCharCode(...Array.from(view)); 188 | } 189 | return text; 190 | } 191 | 192 | /** 193 | * Remove a style from the stack. Useful to apply different styles to chunks 194 | * of text such as bolding. 195 | */ 196 | pop(): void { 197 | logger.debug("ParagraphBuilder.pop"); 198 | this.styles.pop(); 199 | } 200 | 201 | /** 202 | * Push a style to the stack. The corresponding text added with addText will 203 | * use the top-most style. 204 | * @param textStyle 205 | */ 206 | pushStyle(textStyle: TextStyle): void { 207 | logger.debug("ParagraphBuilder.pushStyle", textStyle); 208 | this.styles.push(textStyle); 209 | } 210 | 211 | /** 212 | * Pushes a TextStyle using paints instead of colors for foreground and background. 213 | * @param textStyle 214 | * @param fg 215 | * @param bg 216 | */ 217 | pushPaintStyle(textStyle: TextStyle, fg: Paint, bg: Paint): void { 218 | logger.debug("ParagraphBuilder.pushPaintStyle", textStyle, fg, bg); 219 | this.styles.push(textStyle); 220 | } 221 | 222 | /** 223 | * Resets this builder to its initial state, discarding any text, styles, placeholders that have 224 | * been added, but keeping the initial ParagraphStyle. 225 | */ 226 | reset(): void { 227 | logger.debug("ParagraphBuilder.reset"); 228 | this.spans = []; 229 | this.styles = []; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/adapter/skia.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | export class SkEmbindObject { 6 | _type = ""; 7 | _deleted = false; 8 | 9 | delete(): void { 10 | this._deleted = true; 11 | } 12 | 13 | deleteLater(): void { 14 | this._deleted = true; 15 | } 16 | 17 | isAliasOf(other: any): boolean { 18 | return other._type === this._type; 19 | } 20 | 21 | isDeleted(): boolean { 22 | return this._deleted; 23 | } 24 | } 25 | 26 | export interface SkEnum { 27 | value: T; 28 | } 29 | 30 | export type InputWords = Uint32Array | number[]; 31 | export type InputGraphemes = Uint32Array | number[]; 32 | export type InputLineBreaks = Uint32Array | number[]; 33 | export type Color = Float32Array; 34 | export type InputColor = Color | number[]; 35 | export type Rect = Float32Array; 36 | export type GlyphIDArray = Uint16Array; 37 | 38 | export enum PlaceholderAlignment { 39 | Baseline = "Baseline", 40 | AboveBaseline = "AboveBaseline", 41 | BelowBaseline = "BelowBaseline", 42 | Top = "Top", 43 | Bottom = "Bottom", 44 | Middle = "Middle", 45 | } 46 | 47 | export enum StrokeCap { 48 | Butt = "Butt", 49 | Round = "Round", 50 | Square = "Square", 51 | } 52 | 53 | export enum StrokeJoin { 54 | Bevel = "Bevel", 55 | Miter = "Miter", 56 | Round = "Round", 57 | } 58 | 59 | export enum TextBaseline { 60 | Alphabetic, 61 | Ideographic, 62 | } 63 | 64 | export enum TextDirection { 65 | RTL, 66 | LTR, 67 | } 68 | 69 | export enum RectHeightStyle { 70 | Tight, 71 | Max, 72 | IncludeLineSpacingMiddle, 73 | IncludeLineSpacingTop, 74 | IncludeLineSpacingBottom, 75 | Strut, 76 | } 77 | 78 | export enum RectWidthStyle { 79 | Tight, 80 | Max, 81 | } 82 | 83 | export enum Affinity { 84 | Upstream, 85 | Downstream, 86 | } 87 | 88 | export interface GlyphInfo { 89 | /** 90 | * The layout bounds of the grapheme cluster the code point belongs to, in 91 | * the paragraph's coordinates. 92 | * 93 | * This width of the rect is horizontal advance of the grapheme cluster, 94 | * the height of the rect is the line height when the grapheme cluster 95 | * occupies a full line. 96 | */ 97 | graphemeLayoutBounds: Rect; 98 | /** 99 | * The left-closed-right-open UTF-16 range of the grapheme cluster the code 100 | * point belongs to. 101 | */ 102 | graphemeClusterTextRange: URange; 103 | /** The writing direction of the grapheme cluster. */ 104 | dir: SkEnum; 105 | /** 106 | * Whether the associated glyph points to an ellipsis added by the text 107 | * layout library. 108 | * 109 | * The text layout library truncates the lines that exceed the specified 110 | * max line number, and may add an ellipsis to replace the last few code 111 | * points near the logical end of the last visible line. If True, this object 112 | * marks the logical end of the list of GlyphInfo objects that are 113 | * retrievable from the text layout library. 114 | */ 115 | isEllipsis: boolean; 116 | } 117 | 118 | export interface URange { 119 | start: number; 120 | end: number; 121 | } 122 | 123 | export interface LineMetrics { 124 | /** The index in the text buffer the line begins. */ 125 | startIndex: number; 126 | /** The index in the text buffer the line ends. */ 127 | endIndex: number; 128 | endExcludingWhitespaces: number; 129 | endIncludingNewline: number; 130 | /** True if the line ends in a hard break (e.g. newline) */ 131 | isHardBreak: boolean; 132 | /** 133 | * The final computed ascent for the line. This can be impacted by 134 | * the strut, height, scaling, as well as outlying runs that are very tall. 135 | */ 136 | ascent: number; 137 | /** 138 | * The final computed descent for the line. This can be impacted by 139 | * the strut, height, scaling, as well as outlying runs that are very tall. 140 | */ 141 | descent: number; 142 | /** round(ascent + descent) */ 143 | height: number; 144 | /** heightMultiplier */ 145 | heightMultiplier: number; 146 | /** width of the line */ 147 | width: number; 148 | /** maxWidth of the line */ 149 | justifyWidth?: number; 150 | /** The left edge of the line. The right edge can be obtained with `left + width` */ 151 | left: number; 152 | /** The y offset of the line to top. */ 153 | yOffset: number; 154 | /** The y position of the baseline for this line from the top of the paragraph. */ 155 | baseline: number; 156 | /** Zero indexed line number. */ 157 | lineNumber: number; 158 | isLastLine: boolean; 159 | } 160 | 161 | export interface RectWithDirection { 162 | rect: Rect; 163 | dir: SkEnum; 164 | } 165 | 166 | export interface ShapedLine { 167 | textRange: Range; // first and last character offsets for the line (derived from runs[]) 168 | top: number; // top y-coordinate for the line 169 | bottom: number; // bottom y-coordinate for the line 170 | baseline: number; // baseline y-coordinate for the line 171 | runs: GlyphRun[]; // array of GlyphRun objects for the line 172 | } 173 | 174 | export interface GlyphRun { 175 | typeface: Typeface; // currently set to null (temporary) 176 | size: number; 177 | fakeBold: boolean; 178 | fakeItalic: boolean; 179 | 180 | glyphs: Uint16Array; 181 | positions: Float32Array; // alternating x0, y0, x1, y1, ... 182 | offsets: Uint32Array; 183 | flags: number; // see GlyphRunFlags 184 | } 185 | 186 | export interface Typeface { 187 | getGlyphIDs( 188 | str: string, 189 | numCodePoints?: number, 190 | output?: GlyphIDArray 191 | ): GlyphIDArray; 192 | } 193 | 194 | export interface PositionWithAffinity { 195 | pos: number; 196 | affinity: SkEnum; 197 | } 198 | 199 | export interface ParagraphStyle { 200 | disableHinting?: boolean; 201 | ellipsis?: string; 202 | heightMultiplier?: number; 203 | maxLines?: number; 204 | replaceTabCharacters?: boolean; 205 | strutStyle?: StrutStyle; 206 | textAlign?: SkEnum; 207 | textDirection?: SkEnum; 208 | // textHeightBehavior?, 209 | textStyle?: TextStyle; 210 | // applyRoundingHack?: boolean; 211 | } 212 | 213 | export interface StrutStyle { 214 | strutEnabled?: boolean; 215 | fontFamilies?: string[]; 216 | fontStyle?: FontStyle; 217 | fontSize?: number; 218 | heightMultiplier?: number; 219 | halfLeading?: boolean; 220 | leading?: number; 221 | forceStrutHeight?: boolean; 222 | } 223 | 224 | export enum TextAlign { 225 | Left = 0, 226 | Right = 1, 227 | Center = 2, 228 | Justify = 3, 229 | Start = 4, 230 | End = 5, 231 | } 232 | 233 | export interface LetterRect { 234 | x: number; 235 | y: number; 236 | w: number; 237 | h: number; 238 | } 239 | 240 | export interface TextShadow { 241 | color?: InputColor; 242 | offset?: number[]; 243 | blurRadius?: number; 244 | } 245 | 246 | export const NoDecoration = 0; 247 | export const UnderlineDecoration = 1; 248 | export const OverlineDecoration = 2; 249 | export const LineThroughDecoration = 4; 250 | 251 | export interface TextStyle { 252 | backgroundColor?: InputColor; 253 | color?: InputColor; 254 | decoration?: number; 255 | decorationColor?: InputColor; 256 | decorationThickness?: number; 257 | decorationStyle?: SkEnum; 258 | fontFamilies?: string[]; 259 | fontFeatures?: TextFontFeatures[]; 260 | fontSize?: number; 261 | fontStyle?: FontStyle; 262 | fontVariations?: TextFontVariations[]; 263 | foregroundColor?: InputColor; 264 | heightMultiplier?: number; 265 | halfLeading?: boolean; 266 | letterSpacing?: number; 267 | locale?: string; 268 | shadows?: TextShadow[]; 269 | textBaseline?: SkEnum; 270 | wordSpacing?: number; 271 | } 272 | 273 | export interface FontStyle { 274 | weight?: SkEnum; 275 | width?: SkEnum; 276 | slant?: SkEnum; 277 | } 278 | 279 | export enum FontWeight { 280 | Invisible = 0, 281 | Thin = 100, 282 | ExtraLight = 200, 283 | Light = 300, 284 | Normal = 400, 285 | Medium = 500, 286 | SemiBold = 600, 287 | Bold = 700, 288 | ExtraBold = 800, 289 | Black = 900, 290 | ExtraBlack = 1000, 291 | } 292 | 293 | export enum FontWidth { 294 | UltraCondensed, 295 | ExtraCondensed, 296 | Condensed, 297 | SemiCondensed, 298 | Normal, 299 | SemiExpanded, 300 | Expanded, 301 | ExtraExpanded, 302 | UltraExpanded, 303 | } 304 | 305 | export enum FontSlant { 306 | Upright, 307 | Italic, 308 | Oblique, 309 | } 310 | 311 | export enum DecorationStyle { 312 | Solid, 313 | Double, 314 | Dotted, 315 | Dashed, 316 | Wavy, 317 | } 318 | 319 | export enum TextHeightBehavior { 320 | All, 321 | DisableFirstAscent, 322 | DisableLastDescent, 323 | DisableAll, 324 | } 325 | 326 | export interface TextFontFeatures { 327 | name: string; 328 | value: number; 329 | } 330 | 331 | export interface TextFontVariations { 332 | axis: string; 333 | value: number; 334 | } 335 | -------------------------------------------------------------------------------- /src/impl/drawer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | declare var wx: any; 6 | import { Paragraph } from "../adapter/paragraph"; 7 | import { 8 | LineMetrics, 9 | ParagraphStyle, 10 | TextAlign, 11 | TextDirection, 12 | } from "../adapter/skia"; 13 | import { 14 | DecorationStyle, 15 | LineThroughDecoration, 16 | OverlineDecoration, 17 | UnderlineDecoration, 18 | } from "../adapter/skia"; 19 | import { NewlineSpan, TextSpan, spanWithNewline } from "./span"; 20 | import { 21 | colorToHex, 22 | convertToUpwardToPixelRatio, 23 | createCanvas, 24 | isEnglishWord, 25 | isSquareCharacter, 26 | } from "../util"; 27 | import { logger } from "../logger"; 28 | 29 | export class Drawer { 30 | static pixelRatio = 1.0; 31 | static sharedRenderCanvas: HTMLCanvasElement; 32 | static sharedRenderContext: CanvasRenderingContext2D; 33 | 34 | constructor(readonly paragraph: Paragraph) {} 35 | 36 | private initCanvas() { 37 | if (!Drawer.sharedRenderCanvas) { 38 | Drawer.sharedRenderCanvas = createCanvas( 39 | Math.min(4000, 1000 * Drawer.pixelRatio), 40 | Math.min(4000, 1000 * Drawer.pixelRatio) 41 | ); 42 | Drawer.sharedRenderContext = Drawer.sharedRenderCanvas!.getContext( 43 | "2d" 44 | ) as CanvasRenderingContext2D; 45 | } 46 | } 47 | 48 | draw(): ImageData { 49 | this.initCanvas(); 50 | const width = convertToUpwardToPixelRatio( 51 | this.paragraph.getMaxWidth() * Drawer.pixelRatio, 52 | Drawer.pixelRatio 53 | ); 54 | const height = convertToUpwardToPixelRatio( 55 | this.paragraph.getHeight() * Drawer.pixelRatio, 56 | Drawer.pixelRatio 57 | ); 58 | if (width <= 0 || height <= 0) { 59 | const context = Drawer.sharedRenderContext; 60 | context.clearRect(0, 0, 1, 1); 61 | return context.getImageData(0, 0, 1, 1); 62 | } 63 | const context = Drawer.sharedRenderContext; 64 | context.clearRect(0, 0, width, height); 65 | context.save(); 66 | context.scale(Drawer.pixelRatio, Drawer.pixelRatio); 67 | 68 | let didExceedMaxLines = false; 69 | let spanLetterStartIndex = 0; 70 | let linesDrawingRightBounds: Record = {}; 71 | 72 | const spans = spanWithNewline(this.paragraph.spans); 73 | let linesUndrawed: Record = {}; 74 | this.paragraph.getLineMetrics().forEach((it) => { 75 | linesUndrawed[it.lineNumber] = it.endIndex - it.startIndex; 76 | }); 77 | spans.forEach((span) => { 78 | if (didExceedMaxLines) return; 79 | if (span instanceof TextSpan) { 80 | if (span instanceof NewlineSpan) { 81 | spanLetterStartIndex++; 82 | return; 83 | } 84 | let spanUndrawLength = span.charSequence.length; 85 | let spanLetterEndIndex = 86 | spanLetterStartIndex + span.charSequence.length; 87 | const lineMetrics = this.paragraph.getLineMetricsOfRange( 88 | spanLetterStartIndex, 89 | spanLetterEndIndex 90 | ); 91 | 92 | context.font = span.toCanvasFont(); 93 | 94 | while (spanUndrawLength > 0) { 95 | let currentDrawText: string[] = []; 96 | let currentDrawLine: LineMetrics | undefined; 97 | for (let index = 0; index < lineMetrics.length; index++) { 98 | const line = lineMetrics[index]; 99 | if (linesUndrawed[line.lineNumber] > 0) { 100 | const currentDrawLength = Math.min( 101 | linesUndrawed[line.lineNumber], 102 | spanUndrawLength 103 | ); 104 | currentDrawText = span.charSequence.slice( 105 | span.charSequence.length - spanUndrawLength, 106 | span.charSequence.length - spanUndrawLength + currentDrawLength 107 | ); 108 | spanUndrawLength -= currentDrawLength; 109 | linesUndrawed[line.lineNumber] -= currentDrawLength; 110 | currentDrawLine = line; 111 | break; 112 | } 113 | } 114 | 115 | if (!currentDrawLine) break; 116 | 117 | if ( 118 | this.paragraph.didExceedMaxLines() && 119 | this.paragraph.paragraphStyle.maxLines === 120 | currentDrawLine.lineNumber + 1 && 121 | linesUndrawed[currentDrawLine.lineNumber] <= 0 122 | ) { 123 | const trimLength = isSquareCharacter( 124 | currentDrawText[currentDrawText.length - 1] 125 | ) 126 | ? 1 127 | : 3; 128 | currentDrawText = currentDrawText.slice( 129 | 0, 130 | currentDrawText.length - trimLength 131 | ); 132 | currentDrawText.push( 133 | ...Array.from(this.paragraph.paragraphStyle.ellipsis ?? "...") 134 | ); 135 | didExceedMaxLines = true; 136 | } 137 | 138 | let drawingLeft = (() => { 139 | if ( 140 | linesDrawingRightBounds[currentDrawLine.lineNumber] === undefined 141 | ) { 142 | const textAlign = this.paragraph.paragraphStyle.textAlign?.value; 143 | const textDirection = 144 | this.paragraph.paragraphStyle.textDirection?.value; 145 | if (textAlign === TextAlign.Center) { 146 | linesDrawingRightBounds[currentDrawLine.lineNumber] = 147 | (this.paragraph.getMaxWidth() - currentDrawLine.width) / 2.0; 148 | } else if ( 149 | textAlign === TextAlign.Right || 150 | (textAlign === TextAlign.End && 151 | textDirection !== TextDirection.RTL) || 152 | (textAlign === TextAlign.Start && 153 | textDirection === TextDirection.RTL) 154 | ) { 155 | linesDrawingRightBounds[currentDrawLine.lineNumber] = 156 | this.paragraph.getMaxWidth() - currentDrawLine.width; 157 | } else { 158 | linesDrawingRightBounds[currentDrawLine.lineNumber] = 0; 159 | } 160 | } 161 | return linesDrawingRightBounds[currentDrawLine.lineNumber]; 162 | })(); 163 | 164 | const drawingRight = 165 | drawingLeft + 166 | (() => { 167 | if (currentDrawText.length === 1 && currentDrawText[0] === "\n") { 168 | return 0; 169 | } 170 | const extraLetterSpacing = span.hasLetterSpacing() 171 | ? currentDrawText.length * span.style.letterSpacing! 172 | : 0; 173 | return ( 174 | context.measureText(currentDrawText.join("")).width + 175 | extraLetterSpacing 176 | ); 177 | })(); 178 | 179 | linesDrawingRightBounds[currentDrawLine.lineNumber] = drawingRight; 180 | 181 | const textTop = 182 | currentDrawLine.baseline * currentDrawLine.heightMultiplier - 183 | span.letterBaseline; 184 | const textBaseline = 185 | currentDrawLine.baseline * currentDrawLine.heightMultiplier; 186 | const textHeight = span.letterHeight; 187 | 188 | this.drawBackground(span, context, { 189 | currentDrawLine, 190 | drawingLeft, 191 | drawingRight, 192 | textBaseline, 193 | textTop, 194 | textHeight, 195 | }); 196 | 197 | context.save(); 198 | if (span.style.shadows && span.style.shadows.length > 0) { 199 | context.shadowColor = span.style.shadows[0].color 200 | ? colorToHex(span.style.shadows[0].color as Float32Array) 201 | : "transparent"; 202 | context.shadowOffsetX = span.style.shadows[0].offset?.[0] ?? 0; 203 | context.shadowOffsetY = span.style.shadows[0].offset?.[1] ?? 0; 204 | context.shadowBlur = span.style.shadows[0].blurRadius ?? 0; 205 | } 206 | context.fillStyle = span.toTextFillStyle(); 207 | if (this.paragraph.iconFontData) { 208 | for (let index = 0; index < currentDrawText.length; index++) { 209 | const currentDrawLetter = currentDrawText[index]; 210 | const letterWidth = span.style.fontSize ?? 14; 211 | this.fillIcon( 212 | context, 213 | currentDrawLetter, 214 | letterWidth, 215 | drawingLeft, 216 | textBaseline + currentDrawLine.yOffset 217 | ); 218 | drawingLeft += letterWidth; 219 | } 220 | } else if ( 221 | span.hasLetterSpacing() || 222 | span.hasWordSpacing() || 223 | span.hasJustifySpacing(this.paragraph.paragraphStyle) 224 | ) { 225 | const letterSpacing = span.hasLetterSpacing() 226 | ? span.style.letterSpacing! 227 | : 0; 228 | const justifySpacing = 229 | span.hasJustifySpacing(this.paragraph.paragraphStyle) && 230 | !currentDrawLine.isLastLine 231 | ? this.computeJustifySpacing( 232 | currentDrawText, 233 | currentDrawLine.width, 234 | currentDrawLine.justifyWidth! 235 | ) 236 | : 0; 237 | for (let index = 0; index < currentDrawText.length; index++) { 238 | const currentDrawLetter = currentDrawText[index]; 239 | context.fillText( 240 | currentDrawLetter, 241 | drawingLeft, 242 | textBaseline + currentDrawLine.yOffset 243 | ); 244 | const letterWidth = context.measureText(currentDrawLetter).width; 245 | if ( 246 | span.hasWordSpacing() && 247 | currentDrawLetter === " " && 248 | isEnglishWord(currentDrawText[index - 1]) 249 | ) { 250 | drawingLeft += span.style.wordSpacing!; 251 | } else { 252 | drawingLeft += letterWidth + letterSpacing; 253 | } 254 | if (!isEnglishWord(currentDrawText[index])) { 255 | drawingLeft += justifySpacing; 256 | } 257 | } 258 | } else { 259 | context.fillText( 260 | currentDrawText.join(""), 261 | drawingLeft, 262 | textBaseline + currentDrawLine.yOffset 263 | ); 264 | } 265 | context.restore(); 266 | 267 | logger.debug( 268 | "Drawer.draw.fillText", 269 | currentDrawText, 270 | drawingLeft, 271 | textBaseline + currentDrawLine.yOffset 272 | ); 273 | 274 | this.drawDecoration(span, context, { 275 | currentDrawLine, 276 | drawingLeft, 277 | drawingRight, 278 | textBaseline, 279 | textTop, 280 | textHeight, 281 | }); 282 | 283 | if (didExceedMaxLines) { 284 | break; 285 | } 286 | } 287 | 288 | spanLetterStartIndex = spanLetterEndIndex; 289 | } 290 | }); 291 | 292 | context.restore(); 293 | return context.getImageData(0, 0, width, height); 294 | } 295 | 296 | private fillIcon( 297 | context: CanvasRenderingContext2D, 298 | text: string, 299 | fontSize: number, 300 | x: number, 301 | y: number 302 | ) { 303 | const svgPath = this.paragraph.iconFontMap?.[text]; 304 | if (!svgPath) { 305 | console.log("fill icon not found", text.charCodeAt(0).toString(16)); 306 | return; 307 | } 308 | const pathCommands = svgPath.match(/[A-Za-z]\d+([\.\d,]+)?/g); 309 | if (!pathCommands) return; 310 | context.save(); 311 | context.beginPath(); 312 | let lastControlPoint = null; 313 | pathCommands.forEach((command) => { 314 | const type = command.charAt(0); 315 | const args = command 316 | .substring(1) 317 | .split(",") 318 | .map(parseFloat) 319 | .map((it, index) => { 320 | let value = it; 321 | if (index % 2 === 1) { 322 | value = 150 - value + 150; 323 | } 324 | return value * (fontSize / 300); 325 | }); 326 | if (type === "M") { 327 | context.moveTo(args[0], args[1]); 328 | } else if (type === "L") { 329 | context.lineTo(args[0], args[1]); 330 | } else if (type === "C") { 331 | context.bezierCurveTo( 332 | args[0], 333 | args[1], 334 | args[2], 335 | args[3], 336 | args[4], 337 | args[5] 338 | ); 339 | lastControlPoint = [args[2], args[3]]; 340 | } else if (type === "Q") { 341 | context.quadraticCurveTo(args[0], args[1], args[2], args[3]); 342 | lastControlPoint = [args[0], args[1]]; 343 | } else if (type === "A") { 344 | // no need A 345 | } else if (type === "Z") { 346 | context.closePath(); 347 | } 348 | }); 349 | context.fill(); 350 | context.restore(); 351 | } 352 | 353 | private computeJustifySpacing( 354 | text: string[], 355 | lineWidth: number, 356 | justifyWidth: number 357 | ): number { 358 | let count = 0; 359 | for (let index = 0; index < text.length; index++) { 360 | if (!isEnglishWord(text[index])) { 361 | count++; 362 | } 363 | } 364 | return (justifyWidth - lineWidth) / (count - 1); 365 | } 366 | 367 | private drawBackground( 368 | span: TextSpan, 369 | context: CanvasRenderingContext2D, 370 | options: { 371 | currentDrawLine: LineMetrics; 372 | drawingLeft: number; 373 | drawingRight: number; 374 | textBaseline: number; 375 | textTop: number; 376 | textHeight: number; 377 | } 378 | ) { 379 | if (span.style.backgroundColor) { 380 | const { 381 | currentDrawLine, 382 | drawingLeft, 383 | drawingRight, 384 | textTop, 385 | textHeight, 386 | } = options; 387 | context.fillStyle = span.toBackgroundFillStyle(); 388 | context.fillRect( 389 | drawingLeft, 390 | textTop + currentDrawLine.yOffset, 391 | drawingRight - drawingLeft, 392 | textHeight 393 | ); 394 | } 395 | } 396 | 397 | private drawDecoration( 398 | span: TextSpan, 399 | context: CanvasRenderingContext2D, 400 | options: { 401 | currentDrawLine: LineMetrics; 402 | drawingLeft: number; 403 | drawingRight: number; 404 | textBaseline: number; 405 | textTop: number; 406 | textHeight: number; 407 | } 408 | ) { 409 | const { 410 | currentDrawLine, 411 | drawingLeft, 412 | drawingRight, 413 | textBaseline, 414 | textTop, 415 | textHeight, 416 | } = options; 417 | if (span.style.decoration) { 418 | context.save(); 419 | context.strokeStyle = span.toDecorationStrokeStyle(); 420 | context.lineWidth = 421 | (span.style.decorationThickness ?? 1) * 422 | Math.max(1, (span.style.fontSize ?? 12) / 14); 423 | const decorationStyle = span.style.decorationStyle?.value; 424 | 425 | switch (decorationStyle) { 426 | case DecorationStyle.Dashed: 427 | context.lineCap = "butt"; 428 | context.setLineDash([4, 2]); 429 | break; 430 | case DecorationStyle.Dotted: 431 | context.lineCap = "butt"; 432 | context.setLineDash([2, 2]); 433 | break; 434 | } 435 | 436 | if (span.style.decoration === UnderlineDecoration) { 437 | context.beginPath(); 438 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textBaseline + 1); 439 | context.lineTo( 440 | drawingRight, 441 | currentDrawLine.yOffset + textBaseline + 1 442 | ); 443 | context.stroke(); 444 | if (decorationStyle === DecorationStyle.Double) { 445 | context.beginPath(); 446 | context.moveTo( 447 | drawingLeft, 448 | currentDrawLine.yOffset + textBaseline + 3 449 | ); 450 | context.lineTo( 451 | drawingRight, 452 | currentDrawLine.yOffset + textBaseline + 3 453 | ); 454 | context.stroke(); 455 | } 456 | } 457 | if (span.style.decoration === LineThroughDecoration || span.style.decoration === 3) { 458 | context.beginPath(); 459 | context.moveTo( 460 | drawingLeft, 461 | currentDrawLine.yOffset + textTop + textHeight / 2.0 462 | ); 463 | context.lineTo( 464 | drawingRight, 465 | currentDrawLine.yOffset + textTop + textHeight / 2.0 466 | ); 467 | if (decorationStyle === DecorationStyle.Double) { 468 | context.moveTo( 469 | drawingLeft, 470 | currentDrawLine.yOffset + textTop + textHeight / 2.0 + 2 471 | ); 472 | context.lineTo( 473 | drawingRight, 474 | currentDrawLine.yOffset + textTop + textHeight / 2.0 + 2 475 | ); 476 | } 477 | context.stroke(); 478 | } 479 | if (span.style.decoration === OverlineDecoration) { 480 | context.beginPath(); 481 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textTop); 482 | context.lineTo(drawingRight, currentDrawLine.yOffset + textTop); 483 | 484 | if (decorationStyle === DecorationStyle.Double) { 485 | context.moveTo(drawingLeft, currentDrawLine.yOffset + textTop + 2); 486 | context.lineTo(drawingRight, currentDrawLine.yOffset + textTop + 2); 487 | } 488 | context.stroke(); 489 | } 490 | context.restore(); 491 | } 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /src/impl/layout.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | declare var wx: any; 6 | import { type Paragraph } from "../adapter/paragraph"; 7 | import { 8 | GlyphInfo, 9 | LineMetrics, 10 | TextAlign, 11 | TextDirection, 12 | } from "../adapter/skia"; 13 | import { FontSlant } from "../adapter/skia"; 14 | import { logger } from "../logger"; 15 | import { 16 | createCanvas, 17 | isEnglishWord, 18 | isPunctuation, 19 | isSquareCharacter, 20 | valueOfRectXYWH, 21 | } from "../util"; 22 | import { NewlineSpan, TextSpan, spanWithNewline } from "./span"; 23 | 24 | interface LetterMeasureResult { 25 | useCount: number; 26 | width: number; 27 | } 28 | 29 | class LetterMeasurer { 30 | private static LRUConfig = { 31 | maxCacheCount: 1000, 32 | minCacheCount: 200, 33 | }; 34 | 35 | private static measureLRUCache: Record = {}; 36 | 37 | static measureLetters( 38 | span: TextSpan, 39 | context: CanvasRenderingContext2D 40 | ): { advances: number[] } { 41 | let advances: number[] = [0]; 42 | let curPosWidth = 0; 43 | for (let index = 0; index < span.charSequence.length; index++) { 44 | const letter = span.charSequence[index]; 45 | let wordWidth = (() => { 46 | if (isSquareCharacter(letter)) { 47 | return this.measureSquareCharacter(context); 48 | } else { 49 | return this.measureNormalLetter(letter, context); 50 | } 51 | })(); 52 | if ( 53 | span.hasWordSpacing() && 54 | letter === " " && 55 | isEnglishWord(span.charSequence[index - 1]) 56 | ) { 57 | wordWidth = span.style.wordSpacing!; 58 | } else if (span.hasLetterSpacing()) { 59 | wordWidth += span.style.letterSpacing!; 60 | } 61 | curPosWidth += wordWidth; 62 | advances.push(curPosWidth); 63 | } 64 | return { advances }; 65 | } 66 | 67 | private static measureNormalLetter( 68 | letter: string, 69 | context: CanvasRenderingContext2D 70 | ): number { 71 | const width = 72 | this.widthFromCache(context, letter) ?? context.measureText(letter).width; 73 | this.setWidthToCache(context, letter, width); 74 | return width; 75 | } 76 | 77 | private static measureSquareCharacter( 78 | context: CanvasRenderingContext2D 79 | ): number { 80 | const width = 81 | this.widthFromCache(context, "测") ?? context.measureText("测").width; 82 | this.setWidthToCache(context, "测", width); 83 | return width; 84 | } 85 | 86 | private static widthFromCache( 87 | context: CanvasRenderingContext2D, 88 | word: string 89 | ): number | undefined { 90 | const cacheKey = context.font + "_" + word; 91 | return this.measureLRUCache[cacheKey]?.width; 92 | } 93 | 94 | private static setWidthToCache( 95 | context: CanvasRenderingContext2D, 96 | word: string, 97 | width: number 98 | ) { 99 | const cacheKey = context.font + "_" + word; 100 | if (this.measureLRUCache[cacheKey]) { 101 | this.measureLRUCache[cacheKey].useCount++; 102 | return; 103 | } 104 | this.measureLRUCache[cacheKey] = { 105 | useCount: 1, 106 | width: width, 107 | }; 108 | if ( 109 | Object.keys(this.measureLRUCache).length > this.LRUConfig.maxCacheCount 110 | ) { 111 | this.clearCache(); 112 | } 113 | } 114 | 115 | private static clearCache() { 116 | const keys = Object.keys(this.measureLRUCache).sort((a, b) => { 117 | return this.measureLRUCache[a].useCount > this.measureLRUCache[b].useCount 118 | ? 1 119 | : -1; 120 | }); 121 | keys 122 | .slice(0, this.LRUConfig.maxCacheCount - this.LRUConfig.minCacheCount) 123 | .forEach((it) => { 124 | delete this.measureLRUCache[it]; 125 | }); 126 | } 127 | } 128 | 129 | export class TextLayout { 130 | static sharedLayoutCanvas: HTMLCanvasElement; 131 | static sharedLayoutContext: CanvasRenderingContext2D; 132 | 133 | constructor(readonly paragraph: Paragraph) {} 134 | 135 | glyphInfos: GlyphInfo[] = []; 136 | lineMetrics: LineMetrics[] = []; 137 | didExceedMaxLines: boolean = false; 138 | 139 | private previousLayoutWidth: number = 0; 140 | 141 | private initCanvas() { 142 | if (!TextLayout.sharedLayoutCanvas) { 143 | TextLayout.sharedLayoutCanvas = createCanvas(1, 1); 144 | TextLayout.sharedLayoutContext = 145 | TextLayout.sharedLayoutCanvas!.getContext( 146 | "2d" 147 | ) as CanvasRenderingContext2D; 148 | } 149 | } 150 | 151 | measureGlyphIfNeeded() { 152 | if (Object.keys(this.glyphInfos).length <= 0) { 153 | this.layout(-1, true); 154 | } 155 | } 156 | 157 | layout(layoutWidth: number, forceCalcGlyphInfos: boolean = false): void { 158 | let layoutStartTime!: number; 159 | if (logger.profileMode) { 160 | layoutStartTime = new Date().getTime(); 161 | } 162 | if (layoutWidth < 0) { 163 | layoutWidth = this.previousLayoutWidth; 164 | } 165 | this.previousLayoutWidth = layoutWidth; 166 | this.initCanvas(); 167 | this.glyphInfos = []; 168 | let currentLineMetrics: LineMetrics = { 169 | startIndex: 0, 170 | endIndex: 0, 171 | endExcludingWhitespaces: 0, 172 | endIncludingNewline: 0, 173 | isHardBreak: false, 174 | ascent: 0, 175 | descent: 0, 176 | height: 0, 177 | heightMultiplier: Math.max( 178 | 1, 179 | (this.paragraph.paragraphStyle.heightMultiplier ?? 1.5) / 1.5 180 | ), 181 | width: 0, 182 | justifyWidth: 183 | this.paragraph.paragraphStyle.textAlign?.value === TextAlign.Justify 184 | ? layoutWidth 185 | : undefined, 186 | left: 0, 187 | yOffset: 0, 188 | baseline: 0, 189 | lineNumber: 0, 190 | isLastLine: false, 191 | }; 192 | let lineMetrics: LineMetrics[] = []; 193 | const spans = spanWithNewline(this.paragraph.spans); 194 | spans.forEach((span) => { 195 | if (span instanceof TextSpan) { 196 | TextLayout.sharedLayoutContext.font = span.toCanvasFont(); 197 | const matrics = TextLayout.sharedLayoutContext.measureText(span.originText); 198 | 199 | let iconFontWidth = 0; 200 | if (this.paragraph.iconFontData) { 201 | const fontSize = span.style.fontSize ?? 14; 202 | iconFontWidth = fontSize; 203 | currentLineMetrics.ascent = fontSize; 204 | currentLineMetrics.descent = 0; 205 | span.letterBaseline = fontSize; 206 | span.letterHeight = fontSize; 207 | } else { 208 | const mHeight = TextLayout.sharedLayoutContext.measureText("M").width; 209 | currentLineMetrics.ascent = mHeight * 1.15; 210 | currentLineMetrics.descent = mHeight * 0.35; 211 | span.letterBaseline = mHeight * 1.15; 212 | span.letterHeight = mHeight * 1.15 + mHeight * 0.35; 213 | } 214 | 215 | if (span.style.heightMultiplier && span.style.heightMultiplier > 0) { 216 | currentLineMetrics.heightMultiplier = Math.max( 217 | currentLineMetrics.heightMultiplier, 218 | span.style.heightMultiplier / 1.5 219 | ); 220 | } 221 | 222 | currentLineMetrics.height = Math.max( 223 | currentLineMetrics.height, 224 | currentLineMetrics.ascent + currentLineMetrics.descent 225 | ); 226 | 227 | currentLineMetrics.baseline = Math.max( 228 | currentLineMetrics.baseline, 229 | currentLineMetrics.ascent 230 | ); 231 | 232 | if (this.paragraph.iconFontData) { 233 | const textWidth = span.charSequence.length * iconFontWidth; 234 | currentLineMetrics.endIndex += span.charSequence.length; 235 | currentLineMetrics.width += textWidth; 236 | } else if ( 237 | currentLineMetrics.width + matrics.width < layoutWidth && 238 | !span.hasLetterSpacing() && 239 | !span.hasWordSpacing() && 240 | !forceCalcGlyphInfos 241 | ) { 242 | // fast measure 243 | if (span instanceof NewlineSpan) { 244 | const newLineMatrics: LineMetrics = 245 | this.createNewLine(currentLineMetrics); 246 | lineMetrics.push(currentLineMetrics); 247 | currentLineMetrics = newLineMatrics; 248 | } else { 249 | currentLineMetrics.endIndex += span.charSequence.length; 250 | currentLineMetrics.width += matrics.width; 251 | if (span.style.fontStyle?.slant?.value === FontSlant.Italic) { 252 | currentLineMetrics.width += 2; 253 | } 254 | } 255 | } else { 256 | let letterMeasureResult = LetterMeasurer.measureLetters( 257 | span, 258 | TextLayout.sharedLayoutContext 259 | ); 260 | let advances: number[] = letterMeasureResult.advances; 261 | 262 | if (span instanceof NewlineSpan) { 263 | advances = [0, 0]; 264 | } 265 | 266 | if ( 267 | Math.abs(advances[advances.length - 1] - layoutWidth) < 10 && 268 | layoutWidth === this.previousLayoutWidth 269 | ) { 270 | layoutWidth = advances[advances.length - 1]; 271 | } 272 | 273 | let currentWord = ""; 274 | let currentWordWidth = 0; 275 | let currentWordLength = 0; 276 | let nextWordWidth = 0; 277 | let canBreak = true; 278 | let forceBreak = false; 279 | 280 | for (let index = 0; index < span.charSequence.length; index++) { 281 | const letter = span.charSequence[index]; 282 | currentWord += letter; 283 | let currentLetterLeft = currentWordWidth; 284 | let spanEnded = span.charSequence[index + 1] === undefined; 285 | let nextWord = currentWord + span.charSequence[index + 1] ?? ""; 286 | if (advances[index + 1] === undefined) { 287 | currentWordWidth += advances[index] - advances[index - 1]; 288 | } else { 289 | currentWordWidth += advances[index + 1] - advances[index]; 290 | } 291 | if (advances[index + 2] === undefined) { 292 | nextWordWidth = currentWordWidth; 293 | } else { 294 | nextWordWidth = 295 | currentWordWidth + (advances[index + 2] - advances[index + 1]); 296 | } 297 | currentWordLength += 1; 298 | canBreak = true; 299 | forceBreak = false; 300 | 301 | if (spanEnded) { 302 | canBreak = true; 303 | } else if (isEnglishWord(nextWord)) { 304 | canBreak = false; 305 | } 306 | if ( 307 | isPunctuation(nextWord[nextWord.length - 1]) && 308 | currentLineMetrics.width + nextWordWidth >= layoutWidth 309 | ) { 310 | forceBreak = true; 311 | } 312 | if (span instanceof NewlineSpan) { 313 | forceBreak = true; 314 | } 315 | 316 | const currentGlyphLeft = 317 | currentLineMetrics.width + currentLetterLeft; 318 | const currentGlyphTop = currentLineMetrics.yOffset; 319 | const currentGlyphWidth = (() => { 320 | if (advances[index + 1] === undefined) { 321 | return advances[index] - advances[index - 1]; 322 | } else { 323 | return advances[index + 1] - advances[index]; 324 | } 325 | })(); 326 | const currentGlyphHeight = currentLineMetrics.height; 327 | const currentGlyphInfo: GlyphInfo = { 328 | graphemeLayoutBounds: valueOfRectXYWH( 329 | currentGlyphLeft, 330 | currentGlyphTop, 331 | currentGlyphWidth, 332 | currentGlyphHeight 333 | ), 334 | graphemeClusterTextRange: { start: index, end: index + 1 }, 335 | dir: { value: TextDirection.LTR }, 336 | isEllipsis: false, 337 | }; 338 | this.glyphInfos.push(currentGlyphInfo); 339 | 340 | if (!canBreak) { 341 | continue; 342 | } else if ( 343 | !forceBreak && 344 | currentLineMetrics.width + currentWordWidth <= layoutWidth 345 | ) { 346 | currentLineMetrics.width += currentWordWidth; 347 | currentLineMetrics.endIndex += currentWordLength; 348 | currentWord = ""; 349 | currentWordWidth = 0; 350 | currentWordLength = 0; 351 | canBreak = true; 352 | } else if ( 353 | forceBreak || 354 | currentLineMetrics.width + currentWordWidth > layoutWidth 355 | ) { 356 | const newLineMatrics: LineMetrics = 357 | this.createNewLine(currentLineMetrics); 358 | lineMetrics.push(currentLineMetrics); 359 | currentLineMetrics = newLineMatrics; 360 | currentLineMetrics.width += currentWordWidth; 361 | currentLineMetrics.endIndex += currentWordLength; 362 | currentWord = ""; 363 | currentWordWidth = 0; 364 | currentWordLength = 0; 365 | canBreak = true; 366 | } 367 | } 368 | 369 | if (currentWord.length > 0) { 370 | currentLineMetrics.width += currentWordWidth; 371 | currentLineMetrics.endIndex += currentWordLength; 372 | } 373 | } 374 | } 375 | }); 376 | lineMetrics.push(currentLineMetrics); 377 | if ( 378 | this.paragraph.paragraphStyle.maxLines && 379 | lineMetrics.length > this.paragraph.paragraphStyle.maxLines 380 | ) { 381 | this.didExceedMaxLines = true; 382 | lineMetrics = lineMetrics.slice( 383 | 0, 384 | this.paragraph.paragraphStyle.maxLines 385 | ); 386 | } else { 387 | this.didExceedMaxLines = false; 388 | } 389 | logger.debug("TextLayout.layout.lineMetrics", lineMetrics); 390 | if (logger.profileMode) { 391 | const layoutCostTime = new Date().getTime() - layoutStartTime; 392 | logger.profile("Layout cost", layoutCostTime); 393 | } 394 | lineMetrics[lineMetrics.length - 1].isLastLine = true; 395 | this.lineMetrics = lineMetrics; 396 | } 397 | 398 | private createNewLine(currentLineMetrics: LineMetrics): LineMetrics { 399 | return { 400 | startIndex: currentLineMetrics.endIndex, 401 | endIndex: currentLineMetrics.endIndex, 402 | endExcludingWhitespaces: 0, 403 | endIncludingNewline: 0, 404 | isHardBreak: false, 405 | ascent: currentLineMetrics.ascent, 406 | descent: currentLineMetrics.descent, 407 | height: currentLineMetrics.height, 408 | heightMultiplier: Math.max( 409 | 1, 410 | (this.paragraph.paragraphStyle.heightMultiplier ?? 1.5) / 1.5 411 | ), 412 | width: 0, 413 | justifyWidth: currentLineMetrics.justifyWidth, 414 | left: 0, 415 | yOffset: 416 | currentLineMetrics.yOffset + 417 | currentLineMetrics.height * currentLineMetrics.heightMultiplier + 418 | currentLineMetrics.height * 0.15, // 行间距 419 | baseline: currentLineMetrics.baseline, 420 | lineNumber: currentLineMetrics.lineNumber + 1, 421 | isLastLine: false, 422 | }; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/impl/span.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | import { 6 | LetterRect, 7 | FontSlant, 8 | TextStyle, 9 | ParagraphStyle, 10 | TextAlign, 11 | } from "../adapter/skia"; 12 | import { colorToHex } from "../util"; 13 | 14 | export class Span { 15 | letterBaseline: number = 0; 16 | letterHeight: number = 0; 17 | lettersBounding: LetterRect[] = []; 18 | } 19 | 20 | export class TextSpan extends Span { 21 | charSequence: string[]; 22 | originText: string; 23 | 24 | constructor(private readonly text: string, readonly style: TextStyle) { 25 | super(); 26 | this.charSequence = Array.from(text); 27 | this.originText = text; 28 | } 29 | 30 | hasLetterSpacing() { 31 | return ( 32 | this.style.letterSpacing !== undefined && this.style.letterSpacing > 1 33 | ); 34 | } 35 | 36 | hasWordSpacing() { 37 | return this.style.wordSpacing !== undefined && this.style.wordSpacing > 1; 38 | } 39 | 40 | hasJustifySpacing(paragraphStyle: ParagraphStyle) { 41 | return paragraphStyle.textAlign?.value === TextAlign.Justify; 42 | } 43 | 44 | toBackgroundFillStyle(): string { 45 | if (this.style.backgroundColor) { 46 | return colorToHex(this.style.backgroundColor as Float32Array); 47 | } else { 48 | return "#000000"; 49 | } 50 | } 51 | 52 | toTextFillStyle(): string { 53 | if (this.style.color) { 54 | return colorToHex(this.style.color as Float32Array); 55 | } else { 56 | return "#000000"; 57 | } 58 | } 59 | 60 | toDecorationStrokeStyle(): string { 61 | if (this.style.decorationColor) { 62 | return colorToHex(this.style.decorationColor as Float32Array); 63 | } else { 64 | return "#000000"; 65 | } 66 | } 67 | 68 | toCanvasFont(): string { 69 | let font = `${this.style.fontSize}px system-ui, Roboto`; 70 | const fontWeight = this.style.fontStyle?.weight?.value; 71 | if (fontWeight && fontWeight !== 400) { 72 | if (fontWeight >= 900) { 73 | font = "900 " + font; 74 | } else { 75 | font = fontWeight.toFixed(0) + " " + font; 76 | } 77 | } 78 | const slant = this.style.fontStyle?.slant?.value; 79 | if (slant) { 80 | switch (slant) { 81 | case FontSlant.Italic: 82 | font = "italic " + font; 83 | break; 84 | case FontSlant.Oblique: 85 | font = "oblique " + font; 86 | break; 87 | } 88 | } 89 | return font; 90 | } 91 | } 92 | 93 | export class NewlineSpan extends TextSpan { 94 | constructor() { 95 | super("\n", {}); 96 | } 97 | } 98 | 99 | export const spanWithNewline = (spans: Span[]): Span[] => { 100 | let result: Span[] = []; 101 | spans.forEach((span) => { 102 | if (span instanceof TextSpan) { 103 | if (span.originText.indexOf("\n") >= 0) { 104 | const components = span.originText.split("\n"); 105 | for (let index = 0; index < components.length; index++) { 106 | const component = components[index]; 107 | if (index > 0) { 108 | result.push(new NewlineSpan()); 109 | } 110 | result.push(new TextSpan(component, span.style)); 111 | } 112 | return; 113 | } 114 | } 115 | result.push(span); 116 | }); 117 | return result; 118 | }; 119 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | import { Drawer } from "./impl/drawer"; 6 | import { drawParagraph } from "./adapter/paragraph"; 7 | import { ParagraphBuilder } from "./adapter/paragraph_builder"; 8 | import { LogLevel, logger } from "./logger"; 9 | import { installPolyfill } from "./polyfill"; 10 | // import { logger } from "./logger"; 11 | 12 | export class MiniTex { 13 | static install( 14 | canvasKit: any, 15 | pixelRatio: number, 16 | embeddingFonts: string[], 17 | iconFonts?: Record 18 | ) { 19 | if (typeof canvasKit.ParagraphBuilder === "undefined") { 20 | installPolyfill(canvasKit); 21 | ParagraphBuilder.usingPolyfill = true; 22 | } 23 | // logger.profileMode = true; 24 | logger.setLogLevel(LogLevel.ERROR); 25 | Drawer.pixelRatio = pixelRatio; 26 | const originMakeFromFontCollectionMethod = 27 | canvasKit.ParagraphBuilder.MakeFromFontCollection; 28 | canvasKit.ParagraphBuilder.MakeFromFontCollection = function ( 29 | style: any, 30 | fontCollection: any 31 | ) { 32 | return ParagraphBuilder.MakeFromFontCollection( 33 | originMakeFromFontCollectionMethod, 34 | style, 35 | fontCollection, 36 | embeddingFonts, 37 | iconFonts 38 | ); 39 | }; 40 | const originDrawParagraphMethod = canvasKit.Canvas.prototype.drawParagraph; 41 | canvasKit.Canvas.prototype.drawParagraph = function ( 42 | paragraph: any, 43 | dx: number, 44 | dy: number 45 | ) { 46 | if (paragraph.isMiniTex === true) { 47 | drawParagraph(canvasKit, this, paragraph, dx, dy); 48 | } else { 49 | originDrawParagraphMethod.apply(this, [paragraph, dx, dy]); 50 | } 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | export enum LogLevel { 6 | DEBUG, 7 | INFO, 8 | WARN, 9 | ERROR, 10 | } 11 | 12 | export class Logger { 13 | private logLevel: LogLevel; 14 | public profileMode = false; 15 | 16 | constructor(logLevel: LogLevel = LogLevel.ERROR) { 17 | this.logLevel = logLevel; 18 | } 19 | 20 | setLogLevel(logLevel: LogLevel = LogLevel.DEBUG) { 21 | this.logLevel = logLevel; 22 | } 23 | 24 | private log(level: LogLevel, ...args: any[]): void { 25 | if (level >= this.logLevel) { 26 | const message = args.length === 1 ? args[0] : args; 27 | console.log(`[${LogLevel[level]}]`, ...message); 28 | } 29 | } 30 | 31 | public debug(...args: any[]): void { 32 | this.log(LogLevel.DEBUG, ...args); 33 | } 34 | 35 | public info(...args: any[]): void { 36 | this.log(LogLevel.INFO, ...args); 37 | } 38 | 39 | public warn(...args: any[]): void { 40 | this.log(LogLevel.WARN, ...args); 41 | } 42 | 43 | public error(...args: any[]): void { 44 | this.log(LogLevel.ERROR, ...args); 45 | } 46 | 47 | public profile(...args: any[]): void { 48 | if (this.profileMode) { 49 | console.info("[PROFILE]", ...args); 50 | } 51 | } 52 | } 53 | 54 | export const logger = new Logger(LogLevel.ERROR); 55 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | import { Paint } from "./adapter/paint"; 2 | import { ParagraphBuilder } from "./adapter/paragraph_builder"; 3 | import { 4 | DecorationStyle, 5 | FontStyle, 6 | InputColor, 7 | ParagraphStyle, 8 | SkEmbindObject, 9 | SkEnum, 10 | StrutStyle, 11 | TextAlign, 12 | TextBaseline, 13 | TextDirection, 14 | TextFontFeatures, 15 | TextFontVariations, 16 | TextShadow, 17 | TextStyle, 18 | } from "./adapter/skia"; 19 | import { 20 | TextAlignEnumValues, 21 | type CanvasKit, 22 | type Font, 23 | type FontCollection, 24 | type FontCollectionFactory, 25 | type FontEdging, 26 | type FontHinting, 27 | type FontMetrics, 28 | type FontMgr, 29 | type FontMgrFactory, 30 | type InputGlyphIDArray, 31 | type ParagraphBuilderFactory, 32 | type Typeface, 33 | type TypefaceFactory, 34 | type TypefaceFontProvider, 35 | type TypefaceFontProviderFactory, 36 | TextDirectionEnumValues, 37 | TextBaselineEnumValues, 38 | RectHeightStyleEnumValues, 39 | RectWidthStyleEnumValues, 40 | AffinityEnumValues, 41 | FontWeightEnumValues, 42 | FontWidthEnumValues, 43 | FontSlantEnumValues, 44 | DecorationStyleEnumValues, 45 | TextHeightBehaviorEnumValues, 46 | PlaceholderAlignmentEnumValues, 47 | } from "./polyfill.types"; 48 | import { makeFloat32Array, makeUint16Array } from "./util"; 49 | 50 | export const installPolyfill = (canvasKit: CanvasKit) => { 51 | canvasKit.ParagraphBuilder = new _ParagraphBuilderFactory(); 52 | canvasKit.FontCollection = new _FontCollectionFactory(); 53 | canvasKit.FontMgr = new _FontMgrFactory(); 54 | canvasKit.Typeface = new _TypefaceFactory(); 55 | canvasKit.TypefaceFontProvider = new _TypefaceFontProviderFactory(); 56 | canvasKit.Font = _Font; 57 | canvasKit.ParagraphStyle = (properties: any) => { 58 | return new _ParagraphStyle(properties); 59 | }; 60 | canvasKit.TextStyle = (properties: any) => { 61 | return new _TextStyle(properties); 62 | }; 63 | // Paragraph Enums 64 | canvasKit.TextAlign = new TextAlignEnumValues(); 65 | canvasKit.TextDirection = new TextDirectionEnumValues(); 66 | canvasKit.TextBaseline = new TextBaselineEnumValues(); 67 | canvasKit.RectHeightStyle = new RectHeightStyleEnumValues(); 68 | canvasKit.RectWidthStyle = new RectWidthStyleEnumValues(); 69 | canvasKit.Affinity = new AffinityEnumValues(); 70 | canvasKit.FontWeight = new FontWeightEnumValues(); 71 | canvasKit.FontWidth = new FontWidthEnumValues(); 72 | canvasKit.FontSlant = new FontSlantEnumValues(); 73 | canvasKit.DecorationStyle = new DecorationStyleEnumValues(); 74 | canvasKit.TextHeightBehavior = new TextHeightBehaviorEnumValues(); 75 | canvasKit.PlaceholderAlignment = new PlaceholderAlignmentEnumValues(); 76 | // Paragraph Constants 77 | canvasKit.NoDecoration = 0; 78 | canvasKit.UnderlineDecoration = 1; 79 | canvasKit.OverlineDecoration = 2; 80 | canvasKit.LineThroughDecoration = 3; 81 | }; 82 | 83 | class _ParagraphBuilderFactory implements ParagraphBuilderFactory { 84 | Make(style: ParagraphStyle, fontManager: FontMgr): ParagraphBuilder { 85 | return this.MakeFromFontCollection(style, {} as any); 86 | } 87 | MakeFromFontCollection( 88 | style: ParagraphStyle, 89 | fontCollection: FontCollection 90 | ): ParagraphBuilder { 91 | throw new Error("MakeFromFontCollection not implemented."); 92 | } 93 | RequiresClientICU(): boolean { 94 | return false; 95 | } 96 | } 97 | 98 | class _ParagraphStyle extends SkEmbindObject implements ParagraphStyle { 99 | constructor(properties: any) { 100 | super(); 101 | Object.assign(this, properties); 102 | } 103 | 104 | disableHinting?: boolean; 105 | ellipsis?: string; 106 | heightMultiplier?: number; 107 | maxLines?: number; 108 | replaceTabCharacters?: boolean; 109 | strutStyle?: StrutStyle; 110 | textAlign?: SkEnum; 111 | textDirection?: SkEnum; 112 | // textHeightBehavior?, 113 | textStyle?: TextStyle; 114 | // applyRoundingHack?: boolean; 115 | } 116 | 117 | class _TextStyle extends SkEmbindObject implements TextStyle { 118 | constructor(properties: any) { 119 | super(); 120 | Object.assign(this, properties); 121 | } 122 | 123 | backgroundColor?: InputColor; 124 | color?: InputColor; 125 | decoration?: number; 126 | decorationColor?: InputColor; 127 | decorationThickness?: number; 128 | decorationStyle?: SkEnum; 129 | fontFamilies?: string[]; 130 | fontFeatures?: TextFontFeatures[]; 131 | fontSize?: number; 132 | fontStyle?: FontStyle; 133 | fontVariations?: TextFontVariations[]; 134 | foregroundColor?: InputColor; 135 | heightMultiplier?: number; 136 | halfLeading?: boolean; 137 | letterSpacing?: number; 138 | locale?: string; 139 | shadows?: TextShadow[]; 140 | textBaseline?: SkEnum; 141 | wordSpacing?: number; 142 | } 143 | 144 | class _FontCollection extends SkEmbindObject implements FontCollection { 145 | setDefaultFontManager(fontManager: TypefaceFontProvider | null): void {} 146 | enableFontFallback(): void {} 147 | } 148 | 149 | class _FontCollectionFactory implements FontCollectionFactory { 150 | Make(): FontCollection { 151 | return new _FontCollection(); 152 | } 153 | } 154 | 155 | class _FontMgr extends SkEmbindObject implements FontMgr { 156 | countFamilies(): number { 157 | return 0; 158 | } 159 | getFamilyName(index: number): string { 160 | return ""; 161 | } 162 | } 163 | 164 | class _FontMgrFactory implements FontMgrFactory { 165 | FromData(...buffers: ArrayBuffer[]): FontMgr | null { 166 | return new _FontMgr(); 167 | } 168 | } 169 | 170 | class _TypefaceFactory implements TypefaceFactory { 171 | GetDefault(): Typeface | null { 172 | return new _Typeface(); 173 | } 174 | MakeTypefaceFromData(fontData: ArrayBuffer): Typeface | null { 175 | return new _Typeface(); 176 | } 177 | MakeFreeTypeFaceFromData(fontData: ArrayBuffer): Typeface | null { 178 | return new _Typeface(); 179 | } 180 | } 181 | 182 | class _TypefaceFontProvider 183 | extends SkEmbindObject 184 | implements TypefaceFontProvider 185 | { 186 | registerFont(bytes: ArrayBuffer | Uint8Array, family: string): void {} 187 | countFamilies(): number { 188 | return 0; 189 | } 190 | getFamilyName(index: number): string { 191 | return ""; 192 | } 193 | } 194 | 195 | class _TypefaceFontProviderFactory implements TypefaceFontProviderFactory { 196 | Make(): TypefaceFontProvider { 197 | return new _TypefaceFontProvider(); 198 | } 199 | } 200 | 201 | class _Typeface extends SkEmbindObject implements Typeface { 202 | getGlyphIDs( 203 | str: string, 204 | numCodePoints?: number | undefined, 205 | output?: Uint16Array | undefined 206 | ): Uint16Array { 207 | return makeUint16Array([]); 208 | } 209 | } 210 | 211 | class _Font extends SkEmbindObject implements Font { 212 | constructor( 213 | face: Typeface | null, 214 | size: number, 215 | scaleX: number, 216 | skewX: number 217 | ) { 218 | super(); 219 | } 220 | 221 | getMetrics(): FontMetrics { 222 | return { ascent: 0, descent: 0, leading: 0 }; 223 | } 224 | getGlyphBounds( 225 | glyphs: InputGlyphIDArray, 226 | paint?: Paint | null | undefined, 227 | output?: Float32Array | undefined 228 | ): Float32Array { 229 | return makeFloat32Array([0, 0, 0, 0]); 230 | } 231 | getGlyphIDs( 232 | str: string, 233 | numCodePoints?: number | undefined, 234 | output?: Uint16Array | undefined 235 | ): Uint16Array { 236 | return makeUint16Array([]); 237 | } 238 | getGlyphWidths( 239 | glyphs: InputGlyphIDArray, 240 | paint?: Paint | null | undefined, 241 | output?: Float32Array | undefined 242 | ): Float32Array { 243 | return makeFloat32Array([]); 244 | } 245 | getGlyphIntercepts( 246 | glyphs: InputGlyphIDArray, 247 | positions: number[] | Float32Array, 248 | top: number, 249 | bottom: number 250 | ): Float32Array { 251 | return makeFloat32Array([]); 252 | } 253 | getScaleX(): number { 254 | return 1; 255 | } 256 | getSize(): number { 257 | return 0; 258 | } 259 | getSkewX(): number { 260 | return 1; 261 | } 262 | isEmbolden(): boolean { 263 | return false; 264 | } 265 | getTypeface(): Typeface | null { 266 | return new _Typeface(); 267 | } 268 | setEdging(edging: FontEdging): void {} 269 | setEmbeddedBitmaps(embeddedBitmaps: boolean): void {} 270 | setHinting(hinting: FontHinting): void {} 271 | setLinearMetrics(linearMetrics: boolean): void {} 272 | setScaleX(sx: number): void {} 273 | setSize(points: number): void {} 274 | setSkewX(sx: number): void {} 275 | setEmbolden(embolden: boolean): void {} 276 | setSubpixel(subpixel: boolean): void {} 277 | setTypeface(face: Typeface | null): void {} 278 | } 279 | -------------------------------------------------------------------------------- /src/polyfill.types.ts: -------------------------------------------------------------------------------- 1 | import { Paint } from "./adapter/paint"; 2 | import { ParagraphBuilder } from "./adapter/paragraph_builder"; 3 | import { ParagraphStyle, Rect, SkEmbindObject, SkEnum } from "./adapter/skia"; 4 | 5 | export type GlyphIDArray = Uint16Array; 6 | export type InputGlyphIDArray = GlyphIDArray | number[]; 7 | 8 | export interface CanvasKit { 9 | ParagraphBuilder: ParagraphBuilderFactory; 10 | FontCollection: FontCollectionFactory; 11 | FontMgr: FontMgrFactory; 12 | Typeface: TypefaceFactory; 13 | TypefaceFontProvider: TypefaceFontProviderFactory; 14 | Font: any; 15 | ParagraphStyle: any; 16 | TextStyle: any; 17 | FontEdging: SkEnum; 18 | FontHinting: SkEnum; 19 | // Paragraph Enums 20 | Affinity: AffinityEnumValues; 21 | DecorationStyle: DecorationStyleEnumValues; 22 | FontSlant: FontSlantEnumValues; 23 | FontWeight: FontWeightEnumValues; 24 | FontWidth: FontWidthEnumValues; 25 | PlaceholderAlignment: PlaceholderAlignmentEnumValues; 26 | RectHeightStyle: RectHeightStyleEnumValues; 27 | RectWidthStyle: RectWidthStyleEnumValues; 28 | TextAlign: TextAlignEnumValues; 29 | TextBaseline: TextBaselineEnumValues; 30 | TextDirection: TextDirectionEnumValues; 31 | TextHeightBehavior: TextHeightBehaviorEnumValues; 32 | // Paragraph Constants 33 | NoDecoration: number; 34 | UnderlineDecoration: number; 35 | OverlineDecoration: number; 36 | LineThroughDecoration: number; 37 | } 38 | 39 | export interface ParagraphBuilderFactory { 40 | /** 41 | * Creates a ParagraphBuilder using the fonts available from the given font manager. 42 | * @param style 43 | * @param fontManager 44 | */ 45 | Make(style: ParagraphStyle, fontManager: FontMgr): ParagraphBuilder; 46 | 47 | /** 48 | * Creates a ParagraphBuilder using the fonts available from the given font provider. 49 | * @param style 50 | * @param fontSrc 51 | */ 52 | // MakeFromFontProvider( 53 | // style: ParagraphStyle, 54 | // fontSrc: TypefaceFontProvider 55 | // ): ParagraphBuilder; 56 | 57 | /** 58 | * Creates a ParagraphBuilder using the given font collection. 59 | * @param style 60 | * @param fontCollection 61 | */ 62 | MakeFromFontCollection( 63 | style: ParagraphStyle, 64 | fontCollection: FontCollection 65 | ): ParagraphBuilder; 66 | 67 | /** 68 | * Return a shaped array of lines 69 | */ 70 | // ShapeText(text: string, runs: FontBlock[], width?: number): ShapedLine[]; 71 | 72 | /** 73 | * Whether the paragraph builder requires ICU data to be provided by the 74 | * client. 75 | */ 76 | RequiresClientICU(): boolean; 77 | } 78 | 79 | export interface FontCollection extends SkEmbindObject { 80 | /** 81 | * Enable fallback to dynamically discovered fonts for characters that are not handled 82 | * by the text style's fonts. 83 | */ 84 | enableFontFallback(): void; 85 | 86 | /** 87 | * Set the default provider used to locate fonts. 88 | */ 89 | setDefaultFontManager(fontManager: TypefaceFontProvider | null): void; 90 | } 91 | 92 | export interface FontMgr extends SkEmbindObject { 93 | /** 94 | * Return the number of font families loaded in this manager. Useful for debugging. 95 | */ 96 | countFamilies(): number; 97 | 98 | /** 99 | * Return the nth family name. Useful for debugging. 100 | * @param index 101 | */ 102 | getFamilyName(index: number): string; 103 | 104 | /** 105 | * Find the closest matching typeface to the specified familyName and style. 106 | */ 107 | // matchFamilyStyle(name: string, style: FontStyle): Typeface; 108 | } 109 | 110 | export interface FontCollectionFactory { 111 | /** 112 | * Return an empty FontCollection 113 | */ 114 | Make(): FontCollection; 115 | } 116 | 117 | export interface FontMgrFactory { 118 | /** 119 | * Create an FontMgr with the created font data. Returns null if buffers was empty. 120 | * @param buffers 121 | */ 122 | FromData(...buffers: ArrayBuffer[]): FontMgr | null; 123 | } 124 | 125 | export interface TypefaceFontProviderFactory { 126 | /** 127 | * Return an empty TypefaceFontProvider 128 | */ 129 | Make(): TypefaceFontProvider; 130 | } 131 | 132 | export interface TypefaceFontProvider extends FontMgr { 133 | /** 134 | * Registers a given typeface with the given family name (ignoring whatever name the 135 | * typface has for itself). 136 | * @param bytes - the raw bytes for a typeface. 137 | * @param family 138 | */ 139 | registerFont(bytes: ArrayBuffer | Uint8Array, family: string): void; 140 | } 141 | 142 | export interface TypefaceFactory { 143 | /** 144 | * By default, CanvasKit has a default monospace typeface compiled in so that text works out 145 | * of the box. This returns that typeface if it is available, null otherwise. 146 | */ 147 | GetDefault(): Typeface | null; 148 | 149 | /** 150 | * Create a typeface using Freetype from the specified bytes and return it. CanvasKit supports 151 | * .ttf, .woff and .woff2 fonts. It returns null if the bytes cannot be decoded. 152 | * @param fontData 153 | */ 154 | MakeTypefaceFromData(fontData: ArrayBuffer): Typeface | null; 155 | // Legacy 156 | MakeFreeTypeFaceFromData(fontData: ArrayBuffer): Typeface | null; 157 | } 158 | 159 | /** 160 | * See SkTypeface.h for more on this class. The objects are opaque. 161 | */ 162 | export interface Typeface extends SkEmbindObject { 163 | /** 164 | * Retrieves the glyph ids for each code point in the provided string. Note that glyph IDs 165 | * are typeface-dependent; different faces may have different ids for the same code point. 166 | * @param str 167 | * @param numCodePoints - the number of code points in the string. Defaults to str.length. 168 | * @param output - if provided, the results will be copied into this array. 169 | */ 170 | getGlyphIDs( 171 | str: string, 172 | numCodePoints?: number, 173 | output?: GlyphIDArray 174 | ): GlyphIDArray; 175 | } 176 | 177 | /** 178 | * See SkFont.h for more on this class. 179 | */ 180 | export interface Font extends SkEmbindObject { 181 | /** 182 | * Returns the FontMetrics for this font. 183 | */ 184 | getMetrics(): FontMetrics; 185 | 186 | /** 187 | * Retrieves the bounds for each glyph in glyphs. 188 | * If paint is not null, its stroking, PathEffect, and MaskFilter fields are respected. 189 | * These are returned as flattened rectangles. For each glyph, there will be 4 floats for 190 | * left, top, right, bottom (relative to 0, 0) for that glyph. 191 | * @param glyphs 192 | * @param paint 193 | * @param output - if provided, the results will be copied into this array. 194 | */ 195 | getGlyphBounds( 196 | glyphs: InputGlyphIDArray, 197 | paint?: Paint | null, 198 | output?: Float32Array 199 | ): Float32Array; 200 | 201 | /** 202 | * Retrieves the glyph ids for each code point in the provided string. This call is passed to 203 | * the typeface of this font. Note that glyph IDs are typeface-dependent; different faces 204 | * may have different ids for the same code point. 205 | * @param str 206 | * @param numCodePoints - the number of code points in the string. Defaults to str.length. 207 | * @param output - if provided, the results will be copied into this array. 208 | */ 209 | getGlyphIDs( 210 | str: string, 211 | numCodePoints?: number, 212 | output?: GlyphIDArray 213 | ): GlyphIDArray; 214 | 215 | /** 216 | * Retrieves the advanceX measurements for each glyph. 217 | * If paint is not null, its stroking, PathEffect, and MaskFilter fields are respected. 218 | * One width per glyph is returned in the returned array. 219 | * @param glyphs 220 | * @param paint 221 | * @param output - if provided, the results will be copied into this array. 222 | */ 223 | getGlyphWidths( 224 | glyphs: InputGlyphIDArray, 225 | paint?: Paint | null, 226 | output?: Float32Array 227 | ): Float32Array; 228 | 229 | /** 230 | * Computes any intersections of a thick "line" and a run of positionsed glyphs. 231 | * The thick line is represented as a top and bottom coordinate (positive for 232 | * below the baseline, negative for above). If there are no intersections 233 | * (e.g. if this is intended as an underline, and there are no "collisions") 234 | * then the returned array will be empty. If there are intersections, the array 235 | * will contain pairs of X coordinates [start, end] for each segment that 236 | * intersected with a glyph. 237 | * 238 | * @param glyphs the glyphs to intersect with 239 | * @param positions x,y coordinates (2 per glyph) for each glyph 240 | * @param top top of the thick "line" to use for intersection testing 241 | * @param bottom bottom of the thick "line" to use for intersection testing 242 | * @return array of [start, end] x-coordinate pairs. Maybe be empty. 243 | */ 244 | getGlyphIntercepts( 245 | glyphs: InputGlyphIDArray, 246 | positions: Float32Array | number[], 247 | top: number, 248 | bottom: number 249 | ): Float32Array; 250 | 251 | /** 252 | * Returns text scale on x-axis. Default value is 1. 253 | */ 254 | getScaleX(): number; 255 | 256 | /** 257 | * Returns text size in points. 258 | */ 259 | getSize(): number; 260 | 261 | /** 262 | * Returns text skew on x-axis. Default value is zero. 263 | */ 264 | getSkewX(): number; 265 | 266 | /** 267 | * Returns embolden effect for this font. Default value is false. 268 | */ 269 | isEmbolden(): boolean; 270 | 271 | /** 272 | * Returns the Typeface set for this font. 273 | */ 274 | getTypeface(): Typeface | null; 275 | 276 | /** 277 | * Requests, but does not require, that edge pixels draw opaque or with partial transparency. 278 | * @param edging 279 | */ 280 | setEdging(edging: FontEdging): void; 281 | 282 | /** 283 | * Requests, but does not require, to use bitmaps in fonts instead of outlines. 284 | * @param embeddedBitmaps 285 | */ 286 | setEmbeddedBitmaps(embeddedBitmaps: boolean): void; 287 | 288 | /** 289 | * Sets level of glyph outline adjustment. 290 | * @param hinting 291 | */ 292 | setHinting(hinting: FontHinting): void; 293 | 294 | /** 295 | * Requests, but does not require, linearly scalable font and glyph metrics. 296 | * 297 | * For outline fonts 'true' means font and glyph metrics should ignore hinting and rounding. 298 | * Note that some bitmap formats may not be able to scale linearly and will ignore this flag. 299 | * @param linearMetrics 300 | */ 301 | setLinearMetrics(linearMetrics: boolean): void; 302 | 303 | /** 304 | * Sets the text scale on the x-axis. 305 | * @param sx 306 | */ 307 | setScaleX(sx: number): void; 308 | 309 | /** 310 | * Sets the text size in points on this font. 311 | * @param points 312 | */ 313 | setSize(points: number): void; 314 | 315 | /** 316 | * Sets the text-skew on the x axis for this font. 317 | * @param sx 318 | */ 319 | setSkewX(sx: number): void; 320 | 321 | /** 322 | * Set embolden effect for this font. 323 | * @param embolden 324 | */ 325 | setEmbolden(embolden: boolean): void; 326 | 327 | /** 328 | * Requests, but does not require, that glyphs respect sub-pixel positioning. 329 | * @param subpixel 330 | */ 331 | setSubpixel(subpixel: boolean): void; 332 | 333 | /** 334 | * Sets the typeface to use with this font. null means to clear the typeface and use the 335 | * default one. 336 | * @param face 337 | */ 338 | setTypeface(face: Typeface | null): void; 339 | } 340 | 341 | export enum FontEdging { 342 | Alias, 343 | AntiAlias, 344 | SubpixelAntiAlias, 345 | } 346 | 347 | export enum FontHinting { 348 | None, 349 | Slight, 350 | Normal, 351 | Full, 352 | } 353 | 354 | export interface FontMetrics { 355 | ascent: number; // suggested space above the baseline. < 0 356 | descent: number; // suggested space below the baseline. > 0 357 | leading: number; // suggested spacing between descent of previous line and ascent of next line. 358 | bounds?: Rect; // smallest rect containing all glyphs (relative to 0,0) 359 | } 360 | 361 | export class TextAlignEnumValues { 362 | Left = { value: 0 }; 363 | Right = { value: 1 }; 364 | Center = { value: 2 }; 365 | Justify = { value: 3 }; 366 | Start = { value: 4 }; 367 | End = { value: 5 }; 368 | } 369 | 370 | export class TextDirectionEnumValues { 371 | RTL = { value: 0 }; 372 | LTR = { value: 1 }; 373 | } 374 | 375 | export class TextBaselineEnumValues { 376 | Alphabetic = { value: 0 }; 377 | Ideographic = { value: 1 }; 378 | } 379 | 380 | export class RectHeightStyleEnumValues { 381 | Tight = { value: 0 }; 382 | Max = { value: 1 }; 383 | IncludeLineSpacingMiddle = { value: 2 }; 384 | IncludeLineSpacingTop = { value: 3 }; 385 | IncludeLineSpacingBottom = { value: 4 }; 386 | Strut = { value: 5 }; 387 | } 388 | 389 | export class RectWidthStyleEnumValues { 390 | Tight = { value: 0 }; 391 | Max = { value: 1 }; 392 | } 393 | 394 | export class AffinityEnumValues { 395 | Upstream = { value: 0 }; 396 | Downstream = { value: 1 }; 397 | } 398 | 399 | export class FontWeightEnumValues { 400 | Invisible = { value: 0 }; 401 | Thin = { value: 100 }; 402 | ExtraLight = { value: 200 }; 403 | Light = { value: 300 }; 404 | Normal = { value: 400 }; 405 | Medium = { value: 500 }; 406 | SemiBold = { value: 600 }; 407 | Bold = { value: 700 }; 408 | ExtraBold = { value: 800 }; 409 | Black = { value: 900 }; 410 | ExtraBlack = { value: 1000 }; 411 | } 412 | 413 | export class FontWidthEnumValues { 414 | UltraCondensed = { value: 0 }; 415 | ExtraCondensed = { value: 1 }; 416 | Condensed = { value: 2 }; 417 | SemiCondensed = { value: 3 }; 418 | Normal = { value: 4 }; 419 | SemiExpanded = { value: 5 }; 420 | Expanded = { value: 6 }; 421 | ExtraExpanded = { value: 7 }; 422 | UltraExpanded = { value: 8 }; 423 | } 424 | 425 | export class FontSlantEnumValues { 426 | Upright = { value: 0 }; 427 | Italic = { value: 1 }; 428 | Oblique = { value: 2 }; 429 | } 430 | 431 | export class DecorationStyleEnumValues { 432 | Solid = { value: 0 }; 433 | Double = { value: 1 }; 434 | Dotted = { value: 2 }; 435 | Dashed = { value: 3 }; 436 | Wavy = { value: 4 }; 437 | } 438 | 439 | export class TextHeightBehaviorEnumValues { 440 | All = { value: 0 }; 441 | DisableFirstAscent = { value: 1 }; 442 | DisableLastDescent = { value: 2 }; 443 | DisableAll = { value: 3 }; 444 | } 445 | 446 | export class PlaceholderAlignmentEnumValues { 447 | Baseline = { value: 0 }; 448 | AboveBaseline = { value: 1 }; 449 | BelowBaseline = { value: 2 }; 450 | Top = { value: 3 }; 451 | Bottom = { value: 4 }; 452 | Middle = { value: 5 }; 453 | } 454 | -------------------------------------------------------------------------------- /src/target.ts: -------------------------------------------------------------------------------- 1 | export const appTarget: string = "normal" -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MPFlutter Authors. All rights reserved. 2 | // Use of this source code is governed by a Apache License Version 2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | import { appTarget } from "./target"; 6 | 7 | declare var wx: any; 8 | declare var window: any; 9 | 10 | export const makeFloat32Array = (arr: any) => { 11 | if (appTarget === "wegame") { 12 | return arr; 13 | } 14 | return new Float32Array(arr); 15 | }; 16 | 17 | export const makeUint16Array = (arr: any) => { 18 | if (appTarget === "wegame") { 19 | return arr; 20 | } 21 | return new Uint16Array(arr); 22 | }; 23 | 24 | export const colorToHex = (rgbaColor: Float32Array): string => { 25 | const r = Math.round(rgbaColor[0] * 255).toString(16); 26 | const g = Math.round(rgbaColor[1] * 255).toString(16); 27 | const b = Math.round(rgbaColor[2] * 255).toString(16); 28 | const a = Math.round(rgbaColor[3] * 255).toString(16); 29 | const padHex = (hex: string) => (hex.length === 1 ? "0" + hex : hex); 30 | const hexColor = "#" + padHex(r) + padHex(g) + padHex(b) + padHex(a); 31 | return hexColor; 32 | }; 33 | 34 | export const valueOfRGBAInt = ( 35 | r: number, 36 | g: number, 37 | b: number, 38 | a: number 39 | ): Float32Array => { 40 | return Float32Array.from([r, g, b, a]); 41 | }; 42 | 43 | export const valueOfRectXYWH = ( 44 | x: number, 45 | y: number, 46 | w: number, 47 | h: number 48 | ): Float32Array => { 49 | return Float32Array.from([x, y, x + w, y + h]); 50 | }; 51 | 52 | export function isEnglishWord(str: string) { 53 | const englishRegex = /^[A-Za-z,.]+$/; 54 | const result = englishRegex.test(str); 55 | return result; 56 | } 57 | 58 | export function isSquareCharacter(str: string) { 59 | const squareCharacterRange = /[\u4e00-\u9fa5]/; 60 | return squareCharacterRange.test(str); 61 | } 62 | 63 | const mapOfPunctuation: Record = { 64 | "!": 1, 65 | "?": 1, 66 | "。": 1, 67 | ",": 1, 68 | "、": 1, 69 | "“": 1, 70 | "”": 1, 71 | "‘": 1, 72 | "’": 1, 73 | ";": 1, 74 | ":": 1, 75 | "【": 1, 76 | "】": 1, 77 | "『": 1, 78 | "』": 1, 79 | "(": 1, 80 | ")": 1, 81 | "《": 1, 82 | "》": 1, 83 | "〈": 1, 84 | "〉": 1, 85 | "〔": 1, 86 | "〕": 1, 87 | "[": 1, 88 | "]": 1, 89 | "{": 1, 90 | "}": 1, 91 | "〖": 1, 92 | "〗": 1, 93 | "〘": 1, 94 | "〙": 1, 95 | "〚": 1, 96 | "〛": 1, 97 | "〝": 1, 98 | "〞": 1, 99 | "〟": 1, 100 | "﹏": 1, 101 | "…": 1, 102 | "—": 1, 103 | "~": 1, 104 | "·": 1, 105 | "•": 1, 106 | ",": 1, 107 | ".": 1, 108 | }; 109 | 110 | export function isPunctuation(char: string) { 111 | return mapOfPunctuation[char] === 1; 112 | } 113 | 114 | export function convertToUpwardToPixelRatio( 115 | number: number, 116 | pixelRatio: number 117 | ) { 118 | const upwardInt = Math.ceil(number); 119 | const remainder = upwardInt % pixelRatio; 120 | return remainder === 0 ? upwardInt : upwardInt + (pixelRatio - remainder); 121 | } 122 | 123 | export function createCanvas(width: number, height: number): HTMLCanvasElement { 124 | if ( 125 | typeof wx === "object" && 126 | typeof wx.createOffscreenCanvas === "function" 127 | ) { 128 | return wx.createOffscreenCanvas({ 129 | type: "2d", 130 | width: width, 131 | height: height, 132 | }); 133 | } else if (typeof wx === "object" && typeof wx.createCanvas === "function") { 134 | return wx.createCanvas({ 135 | type: "2d", 136 | width: width, 137 | height: height, 138 | }); 139 | } else if (typeof window === "object") { 140 | const canvas = document.createElement("canvas"); 141 | canvas.width = width; 142 | canvas.height = height; 143 | return canvas; 144 | } else { 145 | throw "can not create canvas"; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2015" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./src/" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist/" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | --------------------------------------------------------------------------------