UNPKG

devexpress-richedit

Version:

DevExpress Rich Text Editor is an advanced word-processing tool designed for working with rich text documents.

245 lines (244 loc) 11.3 kB
import { UnitConverter } from '@devexpress/utils/lib/class/unit-converter'; import { Size } from '@devexpress/utils/lib/geometry/size'; import { FixedInterval } from '@devexpress/utils/lib/intervals/fixed'; import { Base64Utils } from '@devexpress/utils/lib/utils/base64'; import { NumberMapUtils } from '@devexpress/utils/lib/utils/map/number'; import { ColumnCalculator } from '../../../../layout-formatter/formatter/utils/columns-calculator'; import { InlinePicturesUpdatedSubDocumentChange } from '../../../changes/sub-document/picture/inline-pictures-updated'; import { UpdatedImageInfo } from './updated-image-info'; import { isDefined } from '@devexpress/utils/lib/utils/common'; import { ExtensionHelper } from '../../../../formats/utils/extension-helper'; import { AnchoredTextBoxRun } from '../../../runs/anchored-text-box-run'; import { BoundsCalculator } from '../../../../layout-formatter/formatter/utils/bounds-calculator'; export class PicSizeUpdaterData { constructor(subDocument, runPosition, run, options, histItemSize) { this.subDocument = subDocument; this.runPosition = runPosition; this.run = run; this.options = options; this.histItemSize = histItemSize; } } class PicSizeUpdater { constructor(modelManipulator, pictureLoadedListener) { this.loadingData = {}; this.modelManipulator = modelManipulator; this.pictureLoadedListener = pictureLoadedListener; } getImageLoadingOptions(run) { const existingData = this.loadingData[run.cacheInfo.tmpId]; if (existingData) { const data = existingData.find(val => (val instanceof PicSizeUpdaterData) && val.run === run); if (data) return data.options; } return null; } addLoadListener(cacheInfo, callback) { const existingData = this.loadingData[cacheInfo.tmpId]; if (!existingData) return; existingData.push(callback); } allPicturesLoaded() { return NumberMapUtils.isEmpty(this.loadingData); } addSizes(subDocPos, options, run, histItemSize) { const cacheInfo = run.info.cacheInfo; let existingData = this.loadingData[cacheInfo.tmpId]; if (!existingData) existingData = this.loadingData[cacheInfo.tmpId] = []; const pos = subDocPos.subDocument.positionManager.registerNotLoadedPicturePosition(subDocPos.position); existingData.push(new PicSizeUpdaterData(subDocPos.subDocument, pos, run, options, histItemSize)); } update(cacheInfo, notify) { const existingData = this.loadingData[cacheInfo.tmpId]; if (existingData) { delete this.loadingData[cacheInfo.tmpId]; this.modelManipulator.batchUpdatableObject.beginUpdate(); for (let extData of existingData) { if (extData instanceof PicSizeUpdaterData) this.updateInner(extData, notify); else extData(); } this.modelManipulator.batchUpdatableObject.endUpdate(); this.pictureLoadedListener.notifyPictureLoaded(); } } updateInner(data, notify) { const options = data.options; const originalSize = data.run.info.cacheInfo.size; let scaleSize = null; if (options.actualSize) { scaleSize = Size.initByCommonAction(adp => 100 * adp(options.actualSize) / adp(originalSize)); } else if (options.calculateActualSize) { const section = this.modelManipulator.model.getSectionByPosition(data.runPosition.value); let maxPictureSize; if (data.subDocument.isTextBox()) { const anchoredRun = this.getAnchoredTextBoxRun(data.subDocument.info); const size = anchoredRun.size; const boundsCalculator = new BoundsCalculator(); boundsCalculator.init(section); const margins = anchoredRun.textBoxProperties.getContentMargins(); let width = (size.useAbsoluteWidth() ? size.absoluteSize.width : UnitConverter.pixelsToTwipsF(size.getActualRelativeWidth(boundsCalculator))) - margins.horizontal; let height; if (anchoredRun.textBoxProperties.resizeShapeToFitText) height = originalSize.height; else height = size.useAbsoluteHeight() ? size.absoluteSize.height : UnitConverter.pixelsToTwipsF(size.getActualRelativeHeight(boundsCalculator)) - margins.vertical; maxPictureSize = new Size(width, height); } else maxPictureSize = ColumnCalculator.findMinimalColumnSize(section.sectionProperties) .applyConverter(UnitConverter.pixelsToTwips); const scale = 100 * Math.min(maxPictureSize.width / Math.max(1, originalSize.width), maxPictureSize.height / Math.max(1, originalSize.height)); const resultScale = Math.max(1, Math.min(scale, 100)); scaleSize = new Size(resultScale, resultScale); } if (scaleSize) { if (data.histItemSize) data.histItemSize.scale = scaleSize.clone(); data.run.size.scale = scaleSize.clone(); if (notify && data.subDocument.getRunByPosition(data.runPosition.value) === data.run) this.modelManipulator.notifyModelChanged(new InlinePicturesUpdatedSubDocumentChange(data.subDocument.id, new UpdatedImageInfo(data.runPosition.value, data.run.info.cacheInfo, data.run.size))); } data.subDocument.positionManager.unregisterNotLoadedPicturePosition(data.runPosition); data.options.imageLoadedEvent.forEach(cb => cb(new FixedInterval(data.runPosition.value, 1), data.run.info.cacheInfo)); } getAnchoredTextBoxRun(info) { const parentSubDocument = this.modelManipulator.model.subDocuments[info.parentSubDocumentId]; const iterator = parentSubDocument.getRunIterator(parentSubDocument.interval); while (iterator.moveNext()) { const run = iterator.currentRun; if (run instanceof AnchoredTextBoxRun) { const anchoredRun = run; if (anchoredRun.subDocId === info.subDocumentId) return anchoredRun; } } return null; } } class CallbacksInfo { constructor(callback, timeoutId) { this.callback = callback; this.timeoutId = timeoutId; } } export class PictureLoader { get imageCache() { return this.modelManipulator.model.cache.imageCache; } constructor(modelManipulator) { this.callbacksInfo = []; this.modelManipulator = modelManipulator; this.sizeUpdater = new PicSizeUpdater(modelManipulator, this); } notifyPictureLoaded() { if (this.sizeUpdater.allPicturesLoaded()) { const clbs = this.callbacksInfo; this.callbacksInfo = []; clbs.forEach(info => { clearTimeout(info.timeoutId); info.callback(true); }); } } ensureAllPicturesLoaded(timeout, callback) { if (this.sizeUpdater.allPicturesLoaded()) { callback(true); return; } const info = new CallbacksInfo(callback, setTimeout(() => { const index = this.callbacksInfo.indexOf(info); if (index >= 0) { this.callbacksInfo.splice(index, 1); info.callback(false); } }, timeout)); this.callbacksInfo.push(info); } ensureAllPicturesPdfCompatible(timeout, convertImageToCompatibleFormat, callback) { this.ensureAllPicturesLoaded(timeout, (_isLoaded) => { if (isDefined(convertImageToCompatibleFormat)) { const infos = this.modelManipulator.model.getAllImages(); const promises = []; NumberMapUtils.forEach(infos, (info) => { if (info.shouldMakeImagePdfCompatible()) { const promise = convertImageToCompatibleFormat(info.base64); if (promise instanceof Promise) { promises.push(promise. then(converted => { info.setPdfCompatibleBase64(converted); }).catch((reason) => console.log(reason))); } } }); Promise.all(promises).then(() => callback()); } else callback(); }); } load(data) { if (data.isLoaded) return; this.loadInner(data); } finalizeLoading(loadedData, existingInfo) { if (!existingInfo) existingInfo = this.imageCache.getPictureData(loadedData.tmpId !== undefined ? loadedData.tmpId : loadedData.actualId); if (!existingInfo.isLoaded) this.imageCache.finalizeLoading(existingInfo, loadedData); this.sizeUpdater.update(existingInfo, true); } loadPictureByBase64(data, imageLoaded) { const img = new Image(); img.onload = () => { data.size = new Size(img.width, img.height).applyConverter(UnitConverter.pixelsToTwips); const contentType = ExtensionHelper.getMimeTypeFromBase64Uri(data.base64); if (contentType == 'image/bmp') this.convertBmpToPng(data, img); imageLoaded(data); }; img.src = data.base64; } convertBmpToPng(data, img) { const canvas = document.createElement('canvas'); if (!canvas) return; const originalSize = data.size.clone().applyConverter(UnitConverter.twipsToPixelsF); const context2d = canvas.getContext('2d'); if (!context2d) return; canvas.width = originalSize.width; canvas.height = originalSize.height; context2d.drawImage(img, 0, 0); data.base64 = canvas.toDataURL(); } loadPictureByUrl(data, imageLoaded) { const xhr = new XMLHttpRequest(); xhr.onload = () => { Base64Utils.fromBlobAsDataUrl(xhr.response, base64 => { data.base64 = base64; this.loadPictureByBase64(data, (data) => imageLoaded(data)); }); }; xhr.onerror = () => imageLoaded(this.imageCache.emptyImage); try { xhr.open('GET', data.imageUrl, true); xhr.responseType = 'blob'; xhr.send(); } catch (_a) { imageLoaded(this.imageCache.emptyImage); } } loadPictureByFile(data, imageLoaded) { Base64Utils.fromBlobAsDataUrl(data.file, base64 => { data.base64 = base64; data.file = undefined; this.loadPictureByBase64(data, data => imageLoaded(data)); }); } }