text_layer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. exports.updateTextLayer = updateTextLayer;
  29. var _util = require("../shared/util.js");
  30. var _display_utils = require("./display_utils.js");
  31. const MAX_TEXT_DIVS_TO_RENDER = 100000;
  32. const DEFAULT_FONT_SIZE = 30;
  33. const DEFAULT_FONT_ASCENT = 0.8;
  34. const ascentCache = new Map();
  35. function getCtx(size, isOffscreenCanvasSupported) {
  36. let ctx;
  37. if (isOffscreenCanvasSupported && _util.FeatureTest.isOffscreenCanvasSupported) {
  38. ctx = new OffscreenCanvas(size, size).getContext("2d", {
  39. alpha: false
  40. });
  41. } else {
  42. const canvas = document.createElement("canvas");
  43. canvas.width = canvas.height = size;
  44. ctx = canvas.getContext("2d", {
  45. alpha: false
  46. });
  47. }
  48. return ctx;
  49. }
  50. function getAscent(fontFamily, isOffscreenCanvasSupported) {
  51. const cachedAscent = ascentCache.get(fontFamily);
  52. if (cachedAscent) {
  53. return cachedAscent;
  54. }
  55. const ctx = getCtx(DEFAULT_FONT_SIZE, isOffscreenCanvasSupported);
  56. ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
  57. const metrics = ctx.measureText("");
  58. let ascent = metrics.fontBoundingBoxAscent;
  59. let descent = Math.abs(metrics.fontBoundingBoxDescent);
  60. if (ascent) {
  61. const ratio = ascent / (ascent + descent);
  62. ascentCache.set(fontFamily, ratio);
  63. ctx.canvas.width = ctx.canvas.height = 0;
  64. return ratio;
  65. }
  66. ctx.strokeStyle = "red";
  67. ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
  68. ctx.strokeText("g", 0, 0);
  69. let pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE).data;
  70. descent = 0;
  71. for (let i = pixels.length - 1 - 3; i >= 0; i -= 4) {
  72. if (pixels[i] > 0) {
  73. descent = Math.ceil(i / 4 / DEFAULT_FONT_SIZE);
  74. break;
  75. }
  76. }
  77. ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
  78. ctx.strokeText("A", 0, DEFAULT_FONT_SIZE);
  79. pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE).data;
  80. ascent = 0;
  81. for (let i = 0, ii = pixels.length; i < ii; i += 4) {
  82. if (pixels[i] > 0) {
  83. ascent = DEFAULT_FONT_SIZE - Math.floor(i / 4 / DEFAULT_FONT_SIZE);
  84. break;
  85. }
  86. }
  87. ctx.canvas.width = ctx.canvas.height = 0;
  88. if (ascent) {
  89. const ratio = ascent / (ascent + descent);
  90. ascentCache.set(fontFamily, ratio);
  91. return ratio;
  92. }
  93. ascentCache.set(fontFamily, DEFAULT_FONT_ASCENT);
  94. return DEFAULT_FONT_ASCENT;
  95. }
  96. function appendText(task, geom, styles) {
  97. const textDiv = document.createElement("span");
  98. const textDivProperties = {
  99. angle: 0,
  100. canvasWidth: 0,
  101. hasText: geom.str !== "",
  102. hasEOL: geom.hasEOL,
  103. fontSize: 0
  104. };
  105. task._textDivs.push(textDiv);
  106. const tx = _util.Util.transform(task._transform, geom.transform);
  107. let angle = Math.atan2(tx[1], tx[0]);
  108. const style = styles[geom.fontName];
  109. if (style.vertical) {
  110. angle += Math.PI / 2;
  111. }
  112. const fontHeight = Math.hypot(tx[2], tx[3]);
  113. const fontAscent = fontHeight * getAscent(style.fontFamily, task._isOffscreenCanvasSupported);
  114. let left, top;
  115. if (angle === 0) {
  116. left = tx[4];
  117. top = tx[5] - fontAscent;
  118. } else {
  119. left = tx[4] + fontAscent * Math.sin(angle);
  120. top = tx[5] - fontAscent * Math.cos(angle);
  121. }
  122. const scaleFactorStr = "calc(var(--scale-factor)*";
  123. const divStyle = textDiv.style;
  124. if (task._container === task._rootContainer) {
  125. divStyle.left = `${(100 * left / task._pageWidth).toFixed(2)}%`;
  126. divStyle.top = `${(100 * top / task._pageHeight).toFixed(2)}%`;
  127. } else {
  128. divStyle.left = `${scaleFactorStr}${left.toFixed(2)}px)`;
  129. divStyle.top = `${scaleFactorStr}${top.toFixed(2)}px)`;
  130. }
  131. divStyle.fontSize = `${scaleFactorStr}${fontHeight.toFixed(2)}px)`;
  132. divStyle.fontFamily = style.fontFamily;
  133. textDivProperties.fontSize = fontHeight;
  134. textDiv.setAttribute("role", "presentation");
  135. textDiv.textContent = geom.str;
  136. textDiv.dir = geom.dir;
  137. if (task._fontInspectorEnabled) {
  138. textDiv.dataset.fontName = geom.fontName;
  139. }
  140. if (angle !== 0) {
  141. textDivProperties.angle = angle * (180 / Math.PI);
  142. }
  143. let shouldScaleText = false;
  144. if (geom.str.length > 1) {
  145. shouldScaleText = true;
  146. } else if (geom.str !== " " && geom.transform[0] !== geom.transform[3]) {
  147. const absScaleX = Math.abs(geom.transform[0]),
  148. absScaleY = Math.abs(geom.transform[3]);
  149. if (absScaleX !== absScaleY && Math.max(absScaleX, absScaleY) / Math.min(absScaleX, absScaleY) > 1.5) {
  150. shouldScaleText = true;
  151. }
  152. }
  153. if (shouldScaleText) {
  154. textDivProperties.canvasWidth = style.vertical ? geom.height : geom.width;
  155. }
  156. task._textDivProperties.set(textDiv, textDivProperties);
  157. if (task._isReadableStream) {
  158. task._layoutText(textDiv);
  159. }
  160. }
  161. function layout(params) {
  162. const {
  163. div,
  164. scale,
  165. properties,
  166. ctx,
  167. prevFontSize,
  168. prevFontFamily
  169. } = params;
  170. const {
  171. style
  172. } = div;
  173. let transform = "";
  174. if (properties.canvasWidth !== 0 && properties.hasText) {
  175. const {
  176. fontFamily
  177. } = style;
  178. const {
  179. canvasWidth,
  180. fontSize
  181. } = properties;
  182. if (prevFontSize !== fontSize || prevFontFamily !== fontFamily) {
  183. ctx.font = `${fontSize * scale}px ${fontFamily}`;
  184. params.prevFontSize = fontSize;
  185. params.prevFontFamily = fontFamily;
  186. }
  187. const {
  188. width
  189. } = ctx.measureText(div.textContent);
  190. if (width > 0) {
  191. transform = `scaleX(${canvasWidth * scale / width})`;
  192. }
  193. }
  194. if (properties.angle !== 0) {
  195. transform = `rotate(${properties.angle}deg) ${transform}`;
  196. }
  197. if (transform.length > 0) {
  198. style.transform = transform;
  199. }
  200. }
  201. function render(task) {
  202. if (task._canceled) {
  203. return;
  204. }
  205. const textDivs = task._textDivs;
  206. const capability = task._capability;
  207. const textDivsLength = textDivs.length;
  208. if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
  209. capability.resolve();
  210. return;
  211. }
  212. if (!task._isReadableStream) {
  213. for (const textDiv of textDivs) {
  214. task._layoutText(textDiv);
  215. }
  216. }
  217. capability.resolve();
  218. }
  219. class TextLayerRenderTask {
  220. constructor({
  221. textContentSource,
  222. container,
  223. viewport,
  224. textDivs,
  225. textDivProperties,
  226. textContentItemsStr,
  227. isOffscreenCanvasSupported
  228. }) {
  229. this._textContentSource = textContentSource;
  230. this._isReadableStream = textContentSource instanceof ReadableStream;
  231. this._container = this._rootContainer = container;
  232. this._textDivs = textDivs || [];
  233. this._textContentItemsStr = textContentItemsStr || [];
  234. this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
  235. this._reader = null;
  236. this._textDivProperties = textDivProperties || new WeakMap();
  237. this._canceled = false;
  238. this._capability = (0, _util.createPromiseCapability)();
  239. this._layoutTextParams = {
  240. prevFontSize: null,
  241. prevFontFamily: null,
  242. div: null,
  243. scale: viewport.scale * (globalThis.devicePixelRatio || 1),
  244. properties: null,
  245. ctx: getCtx(0, isOffscreenCanvasSupported)
  246. };
  247. const {
  248. pageWidth,
  249. pageHeight,
  250. pageX,
  251. pageY
  252. } = viewport.rawDims;
  253. this._transform = [1, 0, 0, -1, -pageX, pageY + pageHeight];
  254. this._pageWidth = pageWidth;
  255. this._pageHeight = pageHeight;
  256. (0, _display_utils.setLayerDimensions)(container, viewport);
  257. this._capability.promise.finally(() => {
  258. this._layoutTextParams = null;
  259. }).catch(() => {});
  260. }
  261. get promise() {
  262. return this._capability.promise;
  263. }
  264. cancel() {
  265. this._canceled = true;
  266. if (this._reader) {
  267. this._reader.cancel(new _util.AbortException("TextLayer task cancelled.")).catch(() => {});
  268. this._reader = null;
  269. }
  270. this._capability.reject(new _util.AbortException("TextLayer task cancelled."));
  271. }
  272. _processItems(items, styleCache) {
  273. for (const item of items) {
  274. if (item.str === undefined) {
  275. if (item.type === "beginMarkedContentProps" || item.type === "beginMarkedContent") {
  276. const parent = this._container;
  277. this._container = document.createElement("span");
  278. this._container.classList.add("markedContent");
  279. if (item.id !== null) {
  280. this._container.setAttribute("id", `${item.id}`);
  281. }
  282. parent.append(this._container);
  283. } else if (item.type === "endMarkedContent") {
  284. this._container = this._container.parentNode;
  285. }
  286. continue;
  287. }
  288. this._textContentItemsStr.push(item.str);
  289. appendText(this, item, styleCache);
  290. }
  291. }
  292. _layoutText(textDiv) {
  293. const textDivProperties = this._layoutTextParams.properties = this._textDivProperties.get(textDiv);
  294. this._layoutTextParams.div = textDiv;
  295. layout(this._layoutTextParams);
  296. if (textDivProperties.hasText) {
  297. this._container.append(textDiv);
  298. }
  299. if (textDivProperties.hasEOL) {
  300. const br = document.createElement("br");
  301. br.setAttribute("role", "presentation");
  302. this._container.append(br);
  303. }
  304. }
  305. _render() {
  306. const capability = (0, _util.createPromiseCapability)();
  307. let styleCache = Object.create(null);
  308. if (this._isReadableStream) {
  309. const pump = () => {
  310. this._reader.read().then(({
  311. value,
  312. done
  313. }) => {
  314. if (done) {
  315. capability.resolve();
  316. return;
  317. }
  318. Object.assign(styleCache, value.styles);
  319. this._processItems(value.items, styleCache);
  320. pump();
  321. }, capability.reject);
  322. };
  323. this._reader = this._textContentSource.getReader();
  324. pump();
  325. } else if (this._textContentSource) {
  326. const {
  327. items,
  328. styles
  329. } = this._textContentSource;
  330. this._processItems(items, styles);
  331. capability.resolve();
  332. } else {
  333. throw new Error('No "textContentSource" parameter specified.');
  334. }
  335. capability.promise.then(() => {
  336. styleCache = null;
  337. render(this);
  338. }, this._capability.reject);
  339. }
  340. }
  341. exports.TextLayerRenderTask = TextLayerRenderTask;
  342. function renderTextLayer(params) {
  343. if (!params.textContentSource && (params.textContent || params.textContentStream)) {
  344. (0, _display_utils.deprecated)("The TextLayerRender `textContent`/`textContentStream` parameters " + "will be removed in the future, please use `textContentSource` instead.");
  345. params.textContentSource = params.textContent || params.textContentStream;
  346. }
  347. const task = new TextLayerRenderTask(params);
  348. task._render();
  349. return task;
  350. }
  351. function updateTextLayer({
  352. container,
  353. viewport,
  354. textDivs,
  355. textDivProperties,
  356. isOffscreenCanvasSupported,
  357. mustRotate = true,
  358. mustRescale = true
  359. }) {
  360. if (mustRotate) {
  361. (0, _display_utils.setLayerDimensions)(container, {
  362. rotation: viewport.rotation
  363. });
  364. }
  365. if (mustRescale) {
  366. const ctx = getCtx(0, isOffscreenCanvasSupported);
  367. const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
  368. const params = {
  369. prevFontSize: null,
  370. prevFontFamily: null,
  371. div: null,
  372. scale,
  373. properties: null,
  374. ctx
  375. };
  376. for (const div of textDivs) {
  377. params.properties = textDivProperties.get(div);
  378. params.div = div;
  379. layout(params);
  380. }
  381. }
  382. }