mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
修复背景色不正确问题;优化位置信息; 大小改回 pt 提升打印准确度
This commit is contained in:
parent
6b749b8b53
commit
7409ad6df1
@ -10,20 +10,7 @@ order: 23
|
|||||||
|
|
||||||
> 2.9.0 及以上版本
|
> 2.9.0 及以上版本
|
||||||
|
|
||||||
用于渲染 office 文档,目前只支持 docx 格式,通过前端转成 HTML 的方式进行渲染,支持以下功能:
|
用于渲染 office 文档,目前只支持 docx 格式
|
||||||
|
|
||||||
- 基础文本样式
|
|
||||||
- 表格及表格样式
|
|
||||||
- 内嵌图片
|
|
||||||
- 列表
|
|
||||||
- 注音
|
|
||||||
- 链接
|
|
||||||
- 文本框
|
|
||||||
- 形状
|
|
||||||
- 数学公式(依赖 MathML,需要比较新的浏览器,或者试试 [polyfill](https://github.com/w3c/mathml-polyfills))
|
|
||||||
- 分页渲染
|
|
||||||
|
|
||||||
不支持的功能:艺术字、域、对象、目录
|
|
||||||
|
|
||||||
## 基本用法
|
## 基本用法
|
||||||
|
|
||||||
@ -42,6 +29,21 @@ order: 23
|
|||||||
|
|
||||||
目前只支持 Word 文档,所以只有 word 的配置项,放在 `wordOptions` 下
|
目前只支持 Word 文档,所以只有 word 的配置项,放在 `wordOptions` 下
|
||||||
|
|
||||||
|
Word 渲染支持以下功能:
|
||||||
|
|
||||||
|
- 基础文本样式
|
||||||
|
- 表格及表格样式
|
||||||
|
- 内嵌图片
|
||||||
|
- 列表
|
||||||
|
- 注音
|
||||||
|
- 链接
|
||||||
|
- 文本框
|
||||||
|
- 形状
|
||||||
|
- 数学公式(依赖 MathML,需要比较新的浏览器,或者试试 [polyfill](https://github.com/w3c/mathml-polyfills))
|
||||||
|
- 分页渲染
|
||||||
|
|
||||||
|
不支持的功能:艺术字、域、对象、目录
|
||||||
|
|
||||||
### word 渲染配置属性表
|
### word 渲染配置属性表
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -63,6 +65,7 @@ order: 23
|
|||||||
| fontMapping | `object` | | 字体映射,是个键值对,用于替换文档中的字体 |
|
| fontMapping | `object` | | 字体映射,是个键值对,用于替换文档中的字体 |
|
||||||
| forceLineHeight | `string` | | 设置段落行高,忽略文档中的设置 |
|
| forceLineHeight | `string` | | 设置段落行高,忽略文档中的设置 |
|
||||||
| enableVar | `boolean` | true | 是否开启变量替换功能 |
|
| enableVar | `boolean` | true | 是否开启变量替换功能 |
|
||||||
|
| printOptions | `object` | | 针对打印的特殊设置,可以覆盖其它所有设置项 |
|
||||||
|
|
||||||
#### 分页渲染
|
#### 分页渲染
|
||||||
|
|
||||||
@ -83,17 +86,16 @@ order: 23
|
|||||||
|
|
||||||
分页渲染的其它设置项
|
分页渲染的其它设置项
|
||||||
|
|
||||||
| 属性名 | 类型 | 默认值 | 说明 |
|
| 属性名 | 类型 | 默认值 | 说明 |
|
||||||
| ------------------ | --------- | --------- | ------------------------------------------------ |
|
| ------------------ | --------- | --------- | ------------------------------------------ | --- |
|
||||||
| page | `boolean` | false | 是否开启分页渲染 |
|
| page | `boolean` | false | 是否开启分页渲染 |
|
||||||
| pageMarginBottom | `number` | 20 | 页面上下间距 |
|
| pageMarginBottom | `number` | 20 | 页面上下间距 |
|
||||||
| pageBackground | `string` | '#FFF' | 页面内背景色 |
|
| pageBackground | `string` | '#FFF' | 页面内背景色 |
|
||||||
| pageShadow | `boolean` | true | 是否显示阴影 |
|
| pageShadow | `boolean` | true | 是否显示阴影 |
|
||||||
| pageWrap | `boolean` | true | 是否显示页面包裹,开启这个后才能设置包裹的背景色 |
|
| pageWrap | `boolean` | true | 是否显示页面包裹 | |
|
||||||
| pageWrap | `boolean` | true | 是否显示页面包裹 |
|
| pageWrapBackground | `string` | '#ECECEC' | 页面包裹的背景色 |
|
||||||
| pageWrapBackground | `string` | '#ECECEC' | 是否显示页面包裹 |
|
| zoom | `number` | | 缩放比例,取值 0-1 之间 |
|
||||||
| zoom | `number` | | 缩放比例,取值 0-1 之间 |
|
| zoomFitWidth | `boolean` | false | 自适应宽度缩放,如果设置了 zoom 将不会生效 |
|
||||||
| zoomFitWidth | `boolean` | false | 自适应宽度缩放,如果设置了 zoom 将不会生效 |
|
|
||||||
|
|
||||||
### 关于渲染效果差异
|
### 关于渲染效果差异
|
||||||
|
|
||||||
@ -303,6 +305,18 @@ order: 23
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
有个 `printOptions` 配置项可以用来自定义在打印时的配置,默认设置是:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": true,
|
||||||
|
"pageWrap": false,
|
||||||
|
"pageShadow": false,
|
||||||
|
"pageMarginBottom": 0,
|
||||||
|
"pageWrapPadding": undefined
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 配合文件上传实现预览功能
|
## 配合文件上传实现预览功能
|
||||||
|
|
||||||
配置和 `input-file` 相同的 `name` 即可
|
配置和 `input-file` 相同的 `name` 即可
|
||||||
@ -331,7 +345,7 @@ order: 23
|
|||||||
|
|
||||||
## 是否显示 loading
|
## 是否显示 loading
|
||||||
|
|
||||||
通过 `"loading": true` 配置显示 loading
|
通过 `"loading": true` 配置显示 loading,主要用于网络较慢的场景。
|
||||||
|
|
||||||
## 属性表
|
## 属性表
|
||||||
|
|
||||||
|
@ -78,6 +78,14 @@ export default class OfficeViewer extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
JSON.stringify(prevProps.wordOptions) !==
|
||||||
|
JSON.stringify(props.wordOptions) ||
|
||||||
|
prevProps.display !== props.display
|
||||||
|
) {
|
||||||
|
this.renderWord();
|
||||||
|
}
|
||||||
|
|
||||||
// 这个变量替换只会更新变化的部分,所以性能还能接受
|
// 这个变量替换只会更新变化的部分,所以性能还能接受
|
||||||
this.word?.updateVariable();
|
this.word?.updateVariable();
|
||||||
}
|
}
|
||||||
@ -147,6 +155,9 @@ export default class OfficeViewer extends React.Component<
|
|||||||
|
|
||||||
if (display !== false) {
|
if (display !== false) {
|
||||||
word.render(this.rootElement?.current!);
|
word.render(this.rootElement?.current!);
|
||||||
|
} else if (display === false && this.rootElement?.current) {
|
||||||
|
// 设置为 false 后清空
|
||||||
|
this.rootElement.current.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.word = word;
|
this.word = word;
|
||||||
@ -172,6 +183,9 @@ export default class OfficeViewer extends React.Component<
|
|||||||
});
|
});
|
||||||
if (display !== false) {
|
if (display !== false) {
|
||||||
word.render(this.rootElement?.current!);
|
word.render(this.rootElement?.current!);
|
||||||
|
} else if (display === false && this.rootElement?.current) {
|
||||||
|
// 设置为 false 后清空
|
||||||
|
this.rootElement.current.innerHTML = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Binary file not shown.
BIN
packages/ooxml-viewer/__tests__/docx/simple/padding.docx
Normal file
BIN
packages/ooxml-viewer/__tests__/docx/simple/padding.docx
Normal file
Binary file not shown.
2805
packages/ooxml-viewer/__tests__/docx/simple/pict-position.xml
Normal file
2805
packages/ooxml-viewer/__tests__/docx/simple/pict-position.xml
Normal file
File diff suppressed because it is too large
Load Diff
1548
packages/ooxml-viewer/__tests__/docx/simple/tab.xml
Normal file
1548
packages/ooxml-viewer/__tests__/docx/simple/tab.xml
Normal file
File diff suppressed because it is too large
Load Diff
2430
packages/ooxml-viewer/__tests__/docx/simple/table-background.xml
Normal file
2430
packages/ooxml-viewer/__tests__/docx/simple/table-background.xml
Normal file
File diff suppressed because it is too large
Load Diff
67097
packages/ooxml-viewer/__tests__/docx/simple/table-height.xml
Normal file
67097
packages/ooxml-viewer/__tests__/docx/simple/table-height.xml
Normal file
File diff suppressed because it is too large
Load Diff
1221
packages/ooxml-viewer/__tests__/docx/simple/test.xml
Normal file
1221
packages/ooxml-viewer/__tests__/docx/simple/test.xml
Normal file
File diff suppressed because it is too large
Load Diff
BIN
packages/ooxml-viewer/__tests__/docx/unsupport/table-height.docx
Normal file
BIN
packages/ooxml-viewer/__tests__/docx/unsupport/table-height.docx
Normal file
Binary file not shown.
@ -45,7 +45,6 @@ const fileLists = {
|
|||||||
],
|
],
|
||||||
docx4j: [
|
docx4j: [
|
||||||
'ArialUnicodeMS.docx',
|
'ArialUnicodeMS.docx',
|
||||||
'DOCPROP_builtin.docx',
|
|
||||||
'Symbols.docx',
|
'Symbols.docx',
|
||||||
'Word2007-fonts.docx',
|
'Word2007-fonts.docx',
|
||||||
'chart.docx',
|
'chart.docx',
|
||||||
@ -115,8 +114,7 @@ const data = {
|
|||||||
};
|
};
|
||||||
const renderOptions = {
|
const renderOptions = {
|
||||||
debug: true,
|
debug: true,
|
||||||
page,
|
page
|
||||||
zoomFitWidth: true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function renderDocx(fileName: string) {
|
async function renderDocx(fileName: string) {
|
||||||
|
@ -27,6 +27,7 @@ import {Note} from './openxml/word/Note';
|
|||||||
import {parseFootnotes} from './parse/Footnotes';
|
import {parseFootnotes} from './parse/Footnotes';
|
||||||
import {parseEndnotes} from './parse/parseEndnotes';
|
import {parseEndnotes} from './parse/parseEndnotes';
|
||||||
import {renderNotes} from './render/renderNotes';
|
import {renderNotes} from './render/renderNotes';
|
||||||
|
import {Section} from './openxml/word/Section';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染配置
|
* 渲染配置
|
||||||
@ -145,6 +146,11 @@ export interface WordRenderOptions {
|
|||||||
* 自适应宽度,如果设置了 zoom,那么 zoom 优先级更高,这个设置只在 ignoreWidth 为 false 的时候生效
|
* 自适应宽度,如果设置了 zoom,那么 zoom 优先级更高,这个设置只在 ignoreWidth 为 false 的时候生效
|
||||||
*/
|
*/
|
||||||
zoomFitWidth?: boolean;
|
zoomFitWidth?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印可以覆盖其它配置
|
||||||
|
*/
|
||||||
|
printOptions?: WordRenderOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRenderOptions: WordRenderOptions = {
|
const defaultRenderOptions: WordRenderOptions = {
|
||||||
@ -288,6 +294,11 @@ export default class Word {
|
|||||||
*/
|
*/
|
||||||
breakPage = false;
|
breakPage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前渲染的段,因为很多渲染需要,所以为了避免大量传参,这里直接挂在这里
|
||||||
|
*/
|
||||||
|
currentSection: Section;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化一些公共资源,比如
|
* 初始化一些公共资源,比如
|
||||||
*/
|
*/
|
||||||
@ -634,10 +645,21 @@ export default class Word {
|
|||||||
iframe.style.position = 'absolute';
|
iframe.style.position = 'absolute';
|
||||||
iframe.style.top = '-10000px';
|
iframe.style.top = '-10000px';
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
iframe.contentDocument?.write('<div id="print"></div>');
|
iframe.contentDocument?.write(
|
||||||
|
'<style>html, body {margin:0; padding:0}</style><div id="print"></div>'
|
||||||
|
);
|
||||||
await this.render(
|
await this.render(
|
||||||
iframe.contentDocument?.getElementById('print') as HTMLElement,
|
iframe.contentDocument?.getElementById('print') as HTMLElement,
|
||||||
{page: false, pageWrap: false}
|
// 这些配置可以让打印还原度更高
|
||||||
|
{
|
||||||
|
page: true,
|
||||||
|
pageWrap: false,
|
||||||
|
pageShadow: false,
|
||||||
|
pageMarginBottom: 0,
|
||||||
|
pageWrapPadding: undefined,
|
||||||
|
zoom: 1,
|
||||||
|
...this.renderOptions.printOptions
|
||||||
|
}
|
||||||
);
|
);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
iframe.focus();
|
iframe.focus();
|
||||||
@ -682,7 +704,7 @@ export default class Word {
|
|||||||
root.classList.add(this.getClassPrefix());
|
root.classList.add(this.getClassPrefix());
|
||||||
if (renderOptions.page && renderOptions.pageWrap) {
|
if (renderOptions.page && renderOptions.pageWrap) {
|
||||||
root.classList.add(this.wrapClassName);
|
root.classList.add(this.wrapClassName);
|
||||||
root.style.padding = `${renderOptions.pageWrapPadding || 0}px`;
|
root.style.padding = `${renderOptions.pageWrapPadding || 0}pt`;
|
||||||
root.style.background = renderOptions.pageWrapBackground || '#ECECEC';
|
root.style.background = renderOptions.pageWrapBackground || '#ECECEC';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +61,25 @@ export class WDocument {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
console.log('unknown background', background);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const child of background.children) {
|
||||||
|
const name = child.tagName;
|
||||||
|
switch (name) {
|
||||||
|
case 'v:background':
|
||||||
|
// vml 的背景色,不支持
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('unknown background', background);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.documentBackground = documentBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
|
@ -23,9 +23,13 @@ export function shapeToSVG(
|
|||||||
): SVGElement {
|
): SVGElement {
|
||||||
const svg = createSVGElement('svg');
|
const svg = createSVGElement('svg');
|
||||||
// 边框有时候会超过
|
// 边框有时候会超过
|
||||||
svg.setAttribute('style', 'overflow: visible');
|
// z-index 是因为后面可能会有文字,避免遮挡
|
||||||
svg.setAttribute('width', width.toString() + 'px');
|
svg.setAttribute(
|
||||||
svg.setAttribute('height', height.toString() + 'px');
|
'style',
|
||||||
|
'overflow: visible; position: absolute; z-index: -1'
|
||||||
|
);
|
||||||
|
svg.setAttribute('width', width.toString() + 'pt');
|
||||||
|
svg.setAttribute('height', height.toString() + 'pt');
|
||||||
|
|
||||||
// 变量值
|
// 变量值
|
||||||
const vars: Var = presetVal(width, height);
|
const vars: Var = presetVal(width, height);
|
||||||
|
@ -24,7 +24,9 @@ function parseBodyPr(element: Element, style: CSSStyle) {
|
|||||||
const value = attribute.value;
|
const value = attribute.value;
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'numCol':
|
case 'numCol':
|
||||||
style['column-count'] = value;
|
if (value !== '1') {
|
||||||
|
style['column-count'] = value;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'vert':
|
case 'vert':
|
||||||
|
@ -74,7 +74,7 @@ export function parseShdColor(word: Word, element: Element) {
|
|||||||
const val = getVal(element) as ST_Shd;
|
const val = getVal(element) as ST_Shd;
|
||||||
|
|
||||||
if (color === 'auto') {
|
if (color === 'auto') {
|
||||||
color = '000000';
|
color = 'FFFFFF';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color.length === 6) {
|
if (color.length === 6) {
|
||||||
@ -139,6 +139,10 @@ export function parseShdColor(word: Word, element: Element) {
|
|||||||
* 用 alpha 来模拟 ptc 功能
|
* 用 alpha 来模拟 ptc 功能
|
||||||
*/
|
*/
|
||||||
function colorPercent(color: string, percent: number): string {
|
function colorPercent(color: string, percent: number): string {
|
||||||
|
// 白色取 alpha 没什么意义,转成黑色
|
||||||
|
if (color === 'FFFFFF') {
|
||||||
|
color = '000000';
|
||||||
|
}
|
||||||
const r = parseInt(color.substring(0, 2), 16);
|
const r = parseInt(color.substring(0, 2), 16);
|
||||||
const g = parseInt(color.substring(2, 4), 16);
|
const g = parseInt(color.substring(2, 4), 16);
|
||||||
const b = parseInt(color.substring(4, 6), 16);
|
const b = parseInt(color.substring(4, 6), 16);
|
||||||
|
@ -363,6 +363,10 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
|
|||||||
// 目前还不支持 grid
|
// 目前还不支持 grid
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'w:topLinePunct':
|
||||||
|
// 没法支持
|
||||||
|
break;
|
||||||
|
|
||||||
case 'w:wordWrap':
|
case 'w:wordWrap':
|
||||||
// 不太确定这里是用 word-break 还是 overflow-wrap
|
// 不太确定这里是用 word-break 还是 overflow-wrap
|
||||||
if (getValBoolean(child)) {
|
if (getValBoolean(child)) {
|
||||||
@ -425,26 +429,26 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
|
|||||||
|
|
||||||
case 'w:outline':
|
case 'w:outline':
|
||||||
style['text-shadow'] =
|
style['text-shadow'] =
|
||||||
'-1px -1px 0 #AAA, 1px -1px 0 #AAA, -1px 1px 0 #AAA, 1px 1px 0 #AAA';
|
'-1pt -1pt 0 #AAA, 1pt -1pt 0 #AAA, -1pt 1pt 0 #AAA, 1pt 1pt 0 #AAA';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'w:shadown':
|
case 'w:shadown':
|
||||||
case 'w:imprint':
|
case 'w:imprint':
|
||||||
if (getValBoolean(child, true)) {
|
if (getValBoolean(child, true)) {
|
||||||
style['text-shadow'] = '1px 1px 2px rgba(0, 0, 0, 0.6)';
|
style['text-shadow'] = '1pt 1pt 2pt rgba(0, 0, 0, 0.6)';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'w14:shadow':
|
case 'w14:shadow':
|
||||||
const blurRad =
|
const blurRad =
|
||||||
parseSize(child, 'w14:blurRad', LengthUsage.Emu) || '2px';
|
parseSize(child, 'w14:blurRad', LengthUsage.Emu) || '2pt';
|
||||||
// 其它结果算出来不像就先忽略了
|
// 其它结果算出来不像就先忽略了
|
||||||
let color = 'rgba(0, 0, 0, 0.6)';
|
let color = 'rgba(0, 0, 0, 0.6)';
|
||||||
const childColor = parseChildColor(word, child);
|
const childColor = parseChildColor(word, child);
|
||||||
if (childColor) {
|
if (childColor) {
|
||||||
color = childColor;
|
color = childColor;
|
||||||
}
|
}
|
||||||
style['text-shadow'] = `1px 1px ${blurRad} ${color}`;
|
style['text-shadow'] = `1pt 1pt ${blurRad} ${color}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -2,18 +2,16 @@
|
|||||||
* 单位相关的解析,参考了 docxjs 里的实现
|
* 单位相关的解析,参考了 docxjs 里的实现
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type LengthType = 'px' | 'pt' | '%' | '';
|
export type LengthType = 'pt' | '%' | '';
|
||||||
|
|
||||||
export type LengthUsageType = {mul: number; unit: LengthType};
|
export type LengthUsageType = {mul: number; unit: LengthType};
|
||||||
|
|
||||||
const ptToPx = 1.3333;
|
|
||||||
|
|
||||||
export const LengthUsage: Record<string, LengthUsageType> = {
|
export const LengthUsage: Record<string, LengthUsageType> = {
|
||||||
Dxa: {mul: ptToPx * 0.05, unit: 'px'}, //twips
|
Dxa: {mul: 0.05, unit: 'pt'}, //twips
|
||||||
Emu: {mul: (ptToPx * 1) / 12700, unit: 'px'},
|
Emu: {mul: 1 / 12700, unit: 'pt'},
|
||||||
FontSize: {mul: ptToPx * 0.5, unit: 'px'},
|
FontSize: {mul: 0.5, unit: 'pt'},
|
||||||
Border: {mul: ptToPx * 0.125, unit: 'px'},
|
Border: {mul: 0.125, unit: 'pt'},
|
||||||
Point: {mul: ptToPx * 1, unit: 'px'},
|
Point: {mul: 1, unit: 'pt'},
|
||||||
Percent: {mul: 0.02, unit: '%'},
|
Percent: {mul: 0.02, unit: '%'},
|
||||||
LineHeight: {mul: 1 / 240, unit: ''},
|
LineHeight: {mul: 1 / 240, unit: ''},
|
||||||
VmlEmu: {mul: 1 / 12700, unit: ''}
|
VmlEmu: {mul: 1 / 12700, unit: ''}
|
||||||
|
@ -45,11 +45,13 @@ function appendToSection(
|
|||||||
section: Section,
|
section: Section,
|
||||||
child: HTMLElement
|
child: HTMLElement
|
||||||
) {
|
) {
|
||||||
|
// 如果是第一个节点,即便超长也得写入,不然就会出现一个空 section
|
||||||
|
const isFirst = sectionEl.children.length === 0;
|
||||||
// 首先尝试写入
|
// 首先尝试写入
|
||||||
appendChild(sectionEl, child);
|
appendChild(sectionEl, child);
|
||||||
|
|
||||||
// 如果超出了就新建一个 section
|
// 如果超出了就新建一个 section
|
||||||
if (createNewSection(word, sectionEnd, child)) {
|
if (!isFirst && createNewSection(word, sectionEnd, child)) {
|
||||||
const newChild = child.cloneNode(true) as HTMLElement;
|
const newChild = child.cloneNode(true) as HTMLElement;
|
||||||
removeChild(sectionEl, child);
|
removeChild(sectionEl, child);
|
||||||
let newSectionEl = renderSection(word, section, renderOptions);
|
let newSectionEl = renderSection(word, section, renderOptions);
|
||||||
@ -75,11 +77,11 @@ function getSectionEnd(section: Section, sectionEl: HTMLElement): SectionEnd {
|
|||||||
const pageMargin = section.properties.pageMargin;
|
const pageMargin = section.properties.pageMargin;
|
||||||
let bottom = sectionBound.top + sectionBound.height;
|
let bottom = sectionBound.top + sectionBound.height;
|
||||||
if (pageMargin?.bottom) {
|
if (pageMargin?.bottom) {
|
||||||
bottom = bottom - parseInt(pageMargin.bottom.replace('px', ''), 10);
|
bottom = bottom - parseInt(pageMargin.bottom.replace('pt', ''), 10);
|
||||||
}
|
}
|
||||||
let right = sectionBound.left + sectionBound.width;
|
let right = sectionBound.left + sectionBound.width;
|
||||||
if (pageMargin?.right) {
|
if (pageMargin?.right) {
|
||||||
right = right - parseInt(pageMargin.right.replace('px', ''), 10);
|
right = right - parseInt(pageMargin.right.replace('pt', ''), 10);
|
||||||
}
|
}
|
||||||
return {bottom, right};
|
return {bottom, right};
|
||||||
}
|
}
|
||||||
@ -97,15 +99,15 @@ function getTransform(
|
|||||||
if (renderOptions.zoomFitWidth && !renderOptions.ignoreWidth) {
|
if (renderOptions.zoomFitWidth && !renderOptions.ignoreWidth) {
|
||||||
const pageWidth = pageSize?.width;
|
const pageWidth = pageSize?.width;
|
||||||
if (rootWidth && pageWidth) {
|
if (rootWidth && pageWidth) {
|
||||||
let pageWidthNum = parseInt(pageWidth.replace('px', ''), 10);
|
let pageWidthNum = parseInt(pageWidth.replace('pt', ''), 10);
|
||||||
|
|
||||||
if (props.pageMargin) {
|
if (props.pageMargin) {
|
||||||
const pageMargin = props.pageMargin;
|
const pageMargin = props.pageMargin;
|
||||||
pageWidthNum += pageMargin.left
|
pageWidthNum += pageMargin.left
|
||||||
? parseInt(pageMargin.left.replace('px', ''), 10)
|
? parseInt(pageMargin.left.replace('pt', ''), 10)
|
||||||
: 0;
|
: 0;
|
||||||
pageWidthNum += pageMargin.right
|
pageWidthNum += pageMargin.right
|
||||||
? parseInt(pageMargin.right.replace('px', ''), 10)
|
? parseInt(pageMargin.right.replace('pt', ''), 10)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
const zoomWidth = rootWidth / pageWidthNum;
|
const zoomWidth = rootWidth / pageWidthNum;
|
||||||
@ -196,6 +198,7 @@ export default function renderBody(
|
|||||||
let isLastSection = false;
|
let isLastSection = false;
|
||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
zooms.push(getTransform(rootWidth, section, renderOptions));
|
zooms.push(getTransform(rootWidth, section, renderOptions));
|
||||||
|
word.currentSection = section;
|
||||||
let sectionEl = renderSection(word, section, renderOptions);
|
let sectionEl = renderSection(word, section, renderOptions);
|
||||||
appendChild(bodyEl, sectionEl);
|
appendChild(bodyEl, sectionEl);
|
||||||
|
|
||||||
@ -207,7 +210,6 @@ export default function renderBody(
|
|||||||
renderSectionInPage(
|
renderSectionInPage(
|
||||||
word,
|
word,
|
||||||
bodyEl,
|
bodyEl,
|
||||||
|
|
||||||
renderOptions,
|
renderOptions,
|
||||||
sectionEl,
|
sectionEl,
|
||||||
section,
|
section,
|
||||||
|
@ -8,6 +8,8 @@ import renderTable from './renderTable';
|
|||||||
import {Table} from '../openxml/word/Table';
|
import {Table} from '../openxml/word/Table';
|
||||||
import {renderGeom} from './renderGeom';
|
import {renderGeom} from './renderGeom';
|
||||||
import {renderCustGeom} from './renderCustGeom';
|
import {renderCustGeom} from './renderCustGeom';
|
||||||
|
import {CSSStyle} from '../openxml/Style';
|
||||||
|
import parseSides from '../util/parseSides';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染图片
|
* 渲染图片
|
||||||
@ -44,12 +46,52 @@ function renderPic(pic: Pic, word: Word, drawing: Drawing) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修复绝对定位的位置,主要是加上 padding,因为 html 中的定位是不考虑 padding 的
|
||||||
|
* @param word
|
||||||
|
* @param style
|
||||||
|
*/
|
||||||
|
function fixAbsolutePosition(word: Word, style: CSSStyle) {
|
||||||
|
let paddingLeft = 0;
|
||||||
|
let paddingTop = 0;
|
||||||
|
const customPadding = word.renderOptions.padding;
|
||||||
|
if (customPadding) {
|
||||||
|
const {left, top} = parseSides(customPadding);
|
||||||
|
paddingLeft = parseInt(left || '0');
|
||||||
|
paddingTop = parseInt(top || '0');
|
||||||
|
} else {
|
||||||
|
const currentSection = word.currentSection;
|
||||||
|
if (currentSection) {
|
||||||
|
const pageMargin = currentSection.properties.pageMargin;
|
||||||
|
if (pageMargin) {
|
||||||
|
paddingLeft = parseInt(pageMargin.left || '0');
|
||||||
|
paddingTop = parseInt(pageMargin.top || '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const leftStyle = style.left;
|
||||||
|
if (leftStyle) {
|
||||||
|
style.left = `${
|
||||||
|
parseInt(String(leftStyle).replace('pt', ''), 10) + paddingLeft
|
||||||
|
}pt`;
|
||||||
|
}
|
||||||
|
const topStyle = style.top;
|
||||||
|
if (topStyle) {
|
||||||
|
style.top = `${
|
||||||
|
parseInt(String(topStyle).replace('pt', ''), 10) + paddingTop
|
||||||
|
}pt`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染图片,目前只支持 picture
|
* 渲染图片,目前只支持 picture
|
||||||
* http://officeopenxml.com/drwOverview.php
|
* http://officeopenxml.com/drwOverview.php
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function renderDrawing(word: Word, drawing: Drawing): HTMLElement {
|
export function renderDrawing(
|
||||||
|
word: Word,
|
||||||
|
drawing: Drawing
|
||||||
|
): HTMLElement | null {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
|
||||||
if (drawing.position === 'inline') {
|
if (drawing.position === 'inline') {
|
||||||
@ -61,11 +103,16 @@ export function renderDrawing(word: Word, drawing: Drawing): HTMLElement {
|
|||||||
appendChild(container, renderPic(drawing.pic, word, drawing));
|
appendChild(container, renderPic(drawing.pic, word, drawing));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!drawing.relativeFromParagraph) {
|
||||||
|
fixAbsolutePosition(word, drawing.containerStyle || {});
|
||||||
|
}
|
||||||
|
|
||||||
applyStyle(container, drawing.containerStyle);
|
applyStyle(container, drawing.containerStyle);
|
||||||
|
|
||||||
if (drawing.wps) {
|
if (drawing.wps) {
|
||||||
const wps = drawing.wps;
|
const wps = drawing.wps;
|
||||||
const spPr = wps.spPr;
|
const spPr = wps.spPr;
|
||||||
|
|
||||||
applyStyle(container, wps.style);
|
applyStyle(container, wps.style);
|
||||||
|
|
||||||
if (spPr?.xfrm) {
|
if (spPr?.xfrm) {
|
||||||
@ -75,16 +122,16 @@ export function renderDrawing(word: Word, drawing: Drawing): HTMLElement {
|
|||||||
container.style.height = ext.cy;
|
container.style.height = ext.cy;
|
||||||
|
|
||||||
if (spPr.geom) {
|
if (spPr.geom) {
|
||||||
const width = parseFloat(ext.cx.replace('px', ''));
|
const width = parseFloat(ext.cx.replace('pt', ''));
|
||||||
const height = parseFloat(ext.cy.replace('px', ''));
|
const height = parseFloat(ext.cy.replace('pt', ''));
|
||||||
appendChild(
|
appendChild(
|
||||||
container,
|
container,
|
||||||
renderGeom(spPr.geom, spPr, width, height, wps.wpsStyle)
|
renderGeom(spPr.geom, spPr, width, height, wps.wpsStyle)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (spPr.custGeom) {
|
if (spPr.custGeom) {
|
||||||
const width = parseFloat(ext.cx.replace('px', ''));
|
const width = parseFloat(ext.cx.replace('pt', ''));
|
||||||
const height = parseFloat(ext.cy.replace('px', ''));
|
const height = parseFloat(ext.cy.replace('pt', ''));
|
||||||
appendChild(
|
appendChild(
|
||||||
container,
|
container,
|
||||||
renderCustGeom(spPr.custGeom, spPr, width, height, wps.wpsStyle)
|
renderCustGeom(spPr.custGeom, spPr, width, height, wps.wpsStyle)
|
||||||
@ -106,5 +153,10 @@ export function renderDrawing(word: Word, drawing: Drawing): HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没内容就不渲染了,避免高度导致撑开父节点
|
||||||
|
if (container.children.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
@ -37,15 +37,34 @@ function renderNote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 过滤掉 0 和 -1 后是否还有其他的值,没有的话就不需要渲染
|
||||||
|
*/
|
||||||
|
function hasNote(notes: Record<string, Note>) {
|
||||||
|
if (!notes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const id in notes) {
|
||||||
|
if (id !== '0' && id !== '-1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderNotes(word: Word) {
|
export function renderNotes(word: Word) {
|
||||||
const noteRoot = createElement('div');
|
const noteRoot = createElement('div');
|
||||||
|
|
||||||
for (const fId in word.footNotes || {}) {
|
if (hasNote(word.footNotes)) {
|
||||||
renderNote(word, noteRoot, 'footnote', fId, word.footNotes[fId]);
|
for (const fId in word.footNotes) {
|
||||||
|
renderNote(word, noteRoot, 'footnote', fId, word.footNotes[fId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const fId in word.endNotes || {}) {
|
if (hasNote(word.endNotes)) {
|
||||||
renderNote(word, noteRoot, 'endnote', fId, word.endNotes[fId]);
|
for (const fId in word.endNotes || {}) {
|
||||||
|
renderNote(word, noteRoot, 'endnote', fId, word.endNotes[fId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noteRoot.children.length) {
|
if (noteRoot.children.length) {
|
||||||
|
@ -37,12 +37,6 @@ export default function renderParagraph(
|
|||||||
appendChild(p, renderNumbering(p, word, properties.numPr));
|
appendChild(p, renderNumbering(p, word, properties.numPr));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties.tabs) {
|
|
||||||
for (const tab of properties.tabs) {
|
|
||||||
appendChild(p, renderTab(word, tab));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inFldChar = false;
|
let inFldChar = false;
|
||||||
|
|
||||||
for (const child of paragraph.children) {
|
for (const child of paragraph.children) {
|
||||||
|
@ -16,13 +16,12 @@ export function renderSection(
|
|||||||
sectionEl.style.position = 'relative';
|
sectionEl.style.position = 'relative';
|
||||||
|
|
||||||
if (renderOptions.page) {
|
if (renderOptions.page) {
|
||||||
sectionEl.style.overflow = 'hidden';
|
|
||||||
if (renderOptions.pageMarginBottom) {
|
if (renderOptions.pageMarginBottom) {
|
||||||
sectionEl.style.marginBottom = renderOptions.pageMarginBottom + 'px';
|
sectionEl.style.marginBottom = renderOptions.pageMarginBottom + 'pt';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderOptions.pageShadow) {
|
if (renderOptions.pageShadow) {
|
||||||
sectionEl.style.boxShadow = '0 0 8px rgba(0, 0, 0, 0.5)';
|
sectionEl.style.boxShadow = '0 0 8pt rgba(0, 0, 0, 0.5)';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderOptions.pageBackground) {
|
if (renderOptions.pageBackground) {
|
||||||
|
@ -2,6 +2,6 @@ import {createElement} from '../util/dom';
|
|||||||
|
|
||||||
export function renderSeparator() {
|
export function renderSeparator() {
|
||||||
const sep = createElement('hr');
|
const sep = createElement('hr');
|
||||||
sep.style.borderTop = '1px solid #bbb';
|
sep.style.borderTop = '1pt solid #bbb';
|
||||||
return sep;
|
return sep;
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,14 @@ import Word from '../Word';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染 tab
|
* 渲染 tab
|
||||||
|
* 不支持 tabs 里的自定义宽度,因为要算渲染后的宽度,比较麻烦
|
||||||
* http://officeopenxml.com/WPtab.php
|
* http://officeopenxml.com/WPtab.php
|
||||||
*/
|
*/
|
||||||
export function renderTab(word: Word, tab: Tab) {
|
export function renderTab(word: Word, tab: Tab) {
|
||||||
const tabElement = createElement('span');
|
const tabElement = createElement('span');
|
||||||
tabElement.style.display = 'inline-block';
|
tabElement.innerHTML = ' ';
|
||||||
tabElement.style.width = tab.pos;
|
|
||||||
tabElement.innerHTML = ' ';
|
|
||||||
if (tab.leader === 'dot') {
|
if (tab.leader === 'dot') {
|
||||||
tabElement.style.borderBottom = '1px dotted';
|
tabElement.style.borderBottom = '1pt dotted';
|
||||||
}
|
}
|
||||||
return tabElement;
|
return tabElement;
|
||||||
}
|
}
|
||||||
|
43
packages/ooxml-viewer/src/util/parseSides.ts
Normal file
43
packages/ooxml-viewer/src/util/parseSides.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 来自 https://github.com/jednano/parse-css-sides/blob/master/src/index.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ISides {
|
||||||
|
top: string;
|
||||||
|
right: string;
|
||||||
|
bottom: string;
|
||||||
|
left: string;
|
||||||
|
important?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function parseSides(value: string): ISides {
|
||||||
|
const sides = value.split(/\s+/);
|
||||||
|
const pos = sides.lastIndexOf('!important');
|
||||||
|
const important = pos !== -1;
|
||||||
|
if (important) {
|
||||||
|
sides.splice(pos, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberOfSides = sides.length;
|
||||||
|
if (numberOfSides < 1 || numberOfSides > 4) {
|
||||||
|
throw new Error(`Cannot parse ${numberOfSides} sides`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [first, ...rest] = sides;
|
||||||
|
return createSides(first, ...rest);
|
||||||
|
|
||||||
|
function createSides(
|
||||||
|
top: string,
|
||||||
|
right: string = top,
|
||||||
|
bottom: string = top,
|
||||||
|
left: string = right
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
...(important ? {important} : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,13 @@
|
|||||||
"amis-editor": ["./packages/amis-editor/src/index.tsx"]
|
"amis-editor": ["./packages/amis-editor/src/index.tsx"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"watchOptions": {
|
||||||
|
"watchFile": "useFsEvents",
|
||||||
|
"watchDirectory": "useFsEvents",
|
||||||
|
"fallbackPolling": "dynamicPriority",
|
||||||
|
"synchronousWatchDirectory": true,
|
||||||
|
"excludeDirectories": ["**/node_modules"]
|
||||||
|
},
|
||||||
"types": ["typePatches"],
|
"types": ["typePatches"],
|
||||||
"references": [],
|
"references": [],
|
||||||
"include": [
|
"include": [
|
||||||
|
Loading…
Reference in New Issue
Block a user