text_layer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /**
  2. * @licstart The following is the entire license notice for the
  3. * JavaScript code in this page
  4. *
  5. * Copyright 2022 Mozilla Foundation
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. *
  19. * @licend The above is the entire license notice for the
  20. * JavaScript code in this page
  21. */
  22. "use strict";
  23. Object.defineProperty(exports, "__esModule", {
  24. value: true
  25. });
  26. exports.TextLayerRenderTask = void 0;
  27. exports.renderTextLayer = renderTextLayer;
  28. var _util = require("../shared/util.js");
  29. const MAX_TEXT_DIVS_TO_RENDER = 100000;
  30. const DEFAULT_FONT_SIZE = 30;
  31. const DEFAULT_FONT_ASCENT = 0.8;
  32. const ascentCache = new Map();
  33. function getAscent(fontFamily, ctx) {
  34. const cachedAscent = ascentCache.get(fontFamily);
  35. if (cachedAscent) {
  36. return cachedAscent;
  37. }
  38. ctx.save();
  39. ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
  40. const metrics = ctx.measureText("");
  41. let ascent = metrics.fontBoundingBoxAscent;
  42. let descent = Math.abs(metrics.fontBoundingBoxDescent);
  43. if (ascent) {
  44. ctx.restore();
  45. const ratio = ascent / (ascent + descent);
  46. ascentCache.set(fontFamily, ratio);
  47. return ratio;
  48. }
  49. ctx.strokeStyle = "red";
  50. ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
  51. ctx.strokeText("g", 0, 0);
  52. let pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE).data;
  53. descent = 0;
  54. for (let i = pixels.length - 1 - 3; i >= 0; i -= 4) {
  55. if (pixels[i] > 0) {
  56. descent = Math.ceil(i / 4 / DEFAULT_FONT_SIZE);
  57. break;
  58. }
  59. }
  60. ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
  61. ctx.strokeText("A", 0, DEFAULT_FONT_SIZE);
  62. pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE).data;
  63. ascent = 0;
  64. for (let i = 0, ii = pixels.length; i < ii; i += 4) {
  65. if (pixels[i] > 0) {
  66. ascent = DEFAULT_FONT_SIZE - Math.floor(i / 4 / DEFAULT_FONT_SIZE);
  67. break;
  68. }
  69. }
  70. ctx.restore();
  71. if (ascent) {
  72. const ratio = ascent / (ascent + descent);
  73. ascentCache.set(fontFamily, ratio);
  74. return ratio;
  75. }
  76. ascentCache.set(fontFamily, DEFAULT_FONT_ASCENT);
  77. return DEFAULT_FONT_ASCENT;
  78. }
  79. function appendText(task, geom, styles, ctx) {
  80. const textDiv = document.createElement("span");
  81. const textDivProperties = {
  82. angle: 0,
  83. canvasWidth: 0,
  84. hasText: geom.str !== "",
  85. hasEOL: geom.hasEOL,
  86. fontSize: 0
  87. };
  88. task._textDivs.push(textDiv);
  89. const tx = _util.Util.transform(task._viewport.transform, geom.transform);
  90. let angle = Math.atan2(tx[1], tx[0]);
  91. const style = styles[geom.fontName];
  92. if (style.vertical) {
  93. angle += Math.PI / 2;
  94. }
  95. const fontHeight = Math.hypot(tx[2], tx[3]);
  96. const fontAscent = fontHeight * getAscent(style.fontFamily, ctx);
  97. let left, top;
  98. if (angle === 0) {
  99. left = tx[4];
  100. top = tx[5] - fontAscent;
  101. } else {
  102. left = tx[4] + fontAscent * Math.sin(angle);
  103. top = tx[5] - fontAscent * Math.cos(angle);
  104. }
  105. textDiv.style.left = `${left}px`;
  106. textDiv.style.top = `${top}px`;
  107. textDiv.style.fontSize = `${fontHeight}px`;
  108. textDiv.style.fontFamily = style.fontFamily;
  109. textDivProperties.fontSize = fontHeight;
  110. textDiv.setAttribute("role", "presentation");
  111. textDiv.textContent = geom.str;
  112. textDiv.dir = geom.dir;
  113. if (task._fontInspectorEnabled) {
  114. textDiv.dataset.fontName = geom.fontName;
  115. }
  116. if (angle !== 0) {
  117. textDivProperties.angle = angle * (180 / Math.PI);
  118. }
  119. let shouldScaleText = false;
  120. if (geom.str.length > 1) {
  121. shouldScaleText = true;
  122. } else if (geom.str !== " " && geom.transform[0] !== geom.transform[3]) {
  123. const absScaleX = Math.abs(geom.transform[0]),
  124. absScaleY = Math.abs(geom.transform[3]);
  125. if (absScaleX !== absScaleY && Math.max(absScaleX, absScaleY) / Math.min(absScaleX, absScaleY) > 1.5) {
  126. shouldScaleText = true;
  127. }
  128. }
  129. if (shouldScaleText) {
  130. if (style.vertical) {
  131. textDivProperties.canvasWidth = geom.height * task._viewport.scale;
  132. } else {
  133. textDivProperties.canvasWidth = geom.width * task._viewport.scale;
  134. }
  135. }
  136. task._textDivProperties.set(textDiv, textDivProperties);
  137. if (task._textContentStream) {
  138. task._layoutText(textDiv);
  139. }
  140. }
  141. function render(task) {
  142. if (task._canceled) {
  143. return;
  144. }
  145. const textDivs = task._textDivs;
  146. const capability = task._capability;
  147. const textDivsLength = textDivs.length;
  148. if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
  149. task._renderingDone = true;
  150. capability.resolve();
  151. return;
  152. }
  153. if (!task._textContentStream) {
  154. for (const textDiv of textDivs) {
  155. task._layoutText(textDiv);
  156. }
  157. }
  158. task._renderingDone = true;
  159. capability.resolve();
  160. }
  161. class TextLayerRenderTask {
  162. constructor({
  163. textContent,
  164. textContentStream,
  165. container,
  166. viewport,
  167. textDivs,
  168. textContentItemsStr
  169. }) {
  170. this._textContent = textContent;
  171. this._textContentStream = textContentStream;
  172. this._container = container;
  173. this._document = container.ownerDocument;
  174. this._viewport = viewport;
  175. this._textDivs = textDivs || [];
  176. this._textContentItemsStr = textContentItemsStr || [];
  177. this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
  178. this._reader = null;
  179. this._layoutTextLastFontSize = null;
  180. this._layoutTextLastFontFamily = null;
  181. this._layoutTextCtx = null;
  182. this._textDivProperties = new WeakMap();
  183. this._renderingDone = false;
  184. this._canceled = false;
  185. this._capability = (0, _util.createPromiseCapability)();
  186. this._renderTimer = null;
  187. this._bounds = [];
  188. this._devicePixelRatio = globalThis.devicePixelRatio || 1;
  189. this._capability.promise.finally(() => {
  190. this._textDivProperties = null;
  191. if (this._layoutTextCtx) {
  192. this._layoutTextCtx.canvas.width = 0;
  193. this._layoutTextCtx.canvas.height = 0;
  194. this._layoutTextCtx = null;
  195. }
  196. }).catch(() => {});
  197. }
  198. get promise() {
  199. return this._capability.promise;
  200. }
  201. cancel() {
  202. this._canceled = true;
  203. if (this._reader) {
  204. this._reader.cancel(new _util.AbortException("TextLayer task cancelled.")).catch(() => {});
  205. this._reader = null;
  206. }
  207. if (this._renderTimer !== null) {
  208. clearTimeout(this._renderTimer);
  209. this._renderTimer = null;
  210. }
  211. this._capability.reject(new Error("TextLayer task cancelled."));
  212. }
  213. _processItems(items, styleCache) {
  214. for (const item of items) {
  215. if (item.str === undefined) {
  216. if (item.type === "beginMarkedContentProps" || item.type === "beginMarkedContent") {
  217. const parent = this._container;
  218. this._container = document.createElement("span");
  219. this._container.classList.add("markedContent");
  220. if (item.id !== null) {
  221. this._container.setAttribute("id", `${item.id}`);
  222. }
  223. parent.append(this._container);
  224. } else if (item.type === "endMarkedContent") {
  225. this._container = this._container.parentNode;
  226. }
  227. continue;
  228. }
  229. this._textContentItemsStr.push(item.str);
  230. appendText(this, item, styleCache, this._layoutTextCtx);
  231. }
  232. }
  233. _layoutText(textDiv) {
  234. const textDivProperties = this._textDivProperties.get(textDiv);
  235. let transform = "";
  236. if (textDivProperties.canvasWidth !== 0 && textDivProperties.hasText) {
  237. const {
  238. fontFamily
  239. } = textDiv.style;
  240. const {
  241. fontSize
  242. } = textDivProperties;
  243. if (fontSize !== this._layoutTextLastFontSize || fontFamily !== this._layoutTextLastFontFamily) {
  244. this._layoutTextCtx.font = `${fontSize * this._devicePixelRatio}px ${fontFamily}`;
  245. this._layoutTextLastFontSize = fontSize;
  246. this._layoutTextLastFontFamily = fontFamily;
  247. }
  248. const {
  249. width
  250. } = this._layoutTextCtx.measureText(textDiv.textContent);
  251. if (width > 0) {
  252. transform = `scaleX(${this._devicePixelRatio * textDivProperties.canvasWidth / width})`;
  253. }
  254. }
  255. if (textDivProperties.angle !== 0) {
  256. transform = `rotate(${textDivProperties.angle}deg) ${transform}`;
  257. }
  258. if (transform.length > 0) {
  259. textDiv.style.transform = transform;
  260. }
  261. if (textDivProperties.hasText) {
  262. this._container.append(textDiv);
  263. }
  264. if (textDivProperties.hasEOL) {
  265. const br = document.createElement("br");
  266. br.setAttribute("role", "presentation");
  267. this._container.append(br);
  268. }
  269. }
  270. _render(timeout = 0) {
  271. const capability = (0, _util.createPromiseCapability)();
  272. let styleCache = Object.create(null);
  273. const canvas = this._document.createElement("canvas");
  274. canvas.height = canvas.width = DEFAULT_FONT_SIZE;
  275. this._layoutTextCtx = canvas.getContext("2d", {
  276. alpha: false
  277. });
  278. if (this._textContent) {
  279. const textItems = this._textContent.items;
  280. const textStyles = this._textContent.styles;
  281. this._processItems(textItems, textStyles);
  282. capability.resolve();
  283. } else if (this._textContentStream) {
  284. const pump = () => {
  285. this._reader.read().then(({
  286. value,
  287. done
  288. }) => {
  289. if (done) {
  290. capability.resolve();
  291. return;
  292. }
  293. Object.assign(styleCache, value.styles);
  294. this._processItems(value.items, styleCache);
  295. pump();
  296. }, capability.reject);
  297. };
  298. this._reader = this._textContentStream.getReader();
  299. pump();
  300. } else {
  301. throw new Error('Neither "textContent" nor "textContentStream" parameters specified.');
  302. }
  303. capability.promise.then(() => {
  304. styleCache = null;
  305. if (!timeout) {
  306. render(this);
  307. } else {
  308. this._renderTimer = setTimeout(() => {
  309. render(this);
  310. this._renderTimer = null;
  311. }, timeout);
  312. }
  313. }, this._capability.reject);
  314. }
  315. }
  316. exports.TextLayerRenderTask = TextLayerRenderTask;
  317. function renderTextLayer(renderParameters) {
  318. const task = new TextLayerRenderTask({
  319. textContent: renderParameters.textContent,
  320. textContentStream: renderParameters.textContentStream,
  321. container: renderParameters.container,
  322. viewport: renderParameters.viewport,
  323. textDivs: renderParameters.textDivs,
  324. textContentItemsStr: renderParameters.textContentItemsStr
  325. });
  326. task._render(renderParameters.timeout);
  327. return task;
  328. }