pptxtojson 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptxtojson",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "A javascript tool for parsing .pptx file",
5
5
  "type": "module",
6
6
  "main": "./dist/index.umd.js",
package/src/fill.js CHANGED
@@ -702,25 +702,47 @@ export async function getSlideBackgroundFill(warpObj) {
702
702
  }
703
703
  }
704
704
 
705
- export async function getShapeFill(node, warpObj, source, groupHierarchy = []) {
706
- const fillType = getFillType(getTextByPathList(node, ['p:spPr']))
705
+ function getShapeFillCandidates(node, source, slideLayoutSpNode, slideMasterSpNode) {
706
+ const candidates = [{ node, source }]
707
+
708
+ if (slideLayoutSpNode) {
709
+ candidates.push({
710
+ node: slideLayoutSpNode,
711
+ source: 'slideLayoutBg',
712
+ })
713
+ }
714
+ if (slideMasterSpNode) {
715
+ candidates.push({
716
+ node: slideMasterSpNode,
717
+ source: 'slideMasterBg',
718
+ })
719
+ }
720
+
721
+ return candidates
722
+ }
723
+
724
+ async function resolveShapeFillFromNode(node, warpObj, source, groupHierarchy) {
725
+ if (!node) return { state: 'missing' }
726
+
727
+ const spPr = getTextByPathList(node, ['p:spPr'])
728
+ const fillType = spPr ? getFillType(spPr) : ''
707
729
  let type = 'color'
708
730
  let fillValue = ''
709
731
  if (fillType === 'NO_FILL') {
710
- return null
732
+ return { state: 'none' }
711
733
  }
712
734
  else if (fillType === 'SOLID_FILL') {
713
- const shpFill = node['p:spPr']['a:solidFill']
735
+ const shpFill = spPr['a:solidFill']
714
736
  fillValue = getSolidFill(shpFill, undefined, undefined, warpObj)
715
737
  type = 'color'
716
738
  }
717
739
  else if (fillType === 'GRADIENT_FILL') {
718
- const shpFill = node['p:spPr']['a:gradFill']
740
+ const shpFill = spPr['a:gradFill']
719
741
  fillValue = getGradientFill(shpFill, warpObj)
720
742
  type = 'gradient'
721
743
  }
722
744
  else if (fillType === 'PIC_FILL') {
723
- const shpFill = node['p:spPr']['a:blipFill']
745
+ const shpFill = spPr['a:blipFill']
724
746
  const picFill = await getPicFill(source, shpFill, warpObj)
725
747
  const opacity = getPicFillOpacity(shpFill)
726
748
  fillValue = {
@@ -732,12 +754,13 @@ export async function getShapeFill(node, warpObj, source, groupHierarchy = []) {
732
754
  type = 'image'
733
755
  }
734
756
  else if (fillType === 'PATTERN_FILL') {
735
- const shpFill = node['p:spPr']['a:pattFill']
757
+ const shpFill = spPr['a:pattFill']
736
758
  fillValue = getPatternFill({ 'a:pattFill': shpFill }, warpObj)
737
759
  type = 'pattern'
738
760
  }
739
761
  else if (fillType === 'GROUP_FILL') {
740
- return findFillInGroupHierarchy(groupHierarchy, warpObj, source)
762
+ const groupFill = await findFillInGroupHierarchy(groupHierarchy, warpObj, source)
763
+ return groupFill ? { state: 'found', fill: groupFill } : { state: 'none' }
741
764
  }
742
765
  if (!fillValue) {
743
766
  const clrName = getTextByPathList(node, ['p:style', 'a:fillRef'])
@@ -745,15 +768,36 @@ export async function getShapeFill(node, warpObj, source, groupHierarchy = []) {
745
768
  type = 'color'
746
769
  }
747
770
  if (!fillValue) {
748
- return null
771
+ return { state: 'missing' }
749
772
  }
750
773
 
751
774
  return {
752
- type,
753
- value: fillValue,
775
+ state: 'found',
776
+ fill: {
777
+ type,
778
+ value: fillValue,
779
+ }
754
780
  }
755
781
  }
756
782
 
783
+ export async function getShapeFill(node, warpObj, source, options = {}) {
784
+ const {
785
+ groupHierarchy = [],
786
+ slideLayoutSpNode,
787
+ slideMasterSpNode,
788
+ } = options
789
+
790
+ const candidates = getShapeFillCandidates(node, source, slideLayoutSpNode, slideMasterSpNode)
791
+ for (const candidate of candidates) {
792
+ const result = await resolveShapeFillFromNode(candidate.node, warpObj, candidate.source, groupHierarchy)
793
+
794
+ if (result.state === 'none') return null
795
+ if (result.state === 'found') return result.fill
796
+ }
797
+
798
+ return null
799
+ }
800
+
757
801
  async function findFillInGroupHierarchy(groupHierarchy, warpObj, source) {
758
802
  for (const groupNode of groupHierarchy) {
759
803
  if (!groupNode || !groupNode['p:grpSpPr']) continue
@@ -892,4 +936,4 @@ export function getSolidFill(solidFill, clrMap, phClr, warpObj) {
892
936
  if (color && color.indexOf('#') === -1) color = '#' + color
893
937
 
894
938
  return color
895
- }
939
+ }
package/src/pptxtojson.js CHANGED
@@ -7,7 +7,7 @@ import { getVerticalAlign, getTextAutoFit } from './paragraph'
7
7
  import { getTextInsets } from './textInsets'
8
8
  import { getPosition, getSize } from './position'
9
9
  import { genTextBody, getTextNodeValue } from './text'
10
- import { getCustomShapePath, identifyShape } from './shape'
10
+ import { getCustomShapePath, identifyShape, isStrokeOnlyCustomGeometry } from './shape'
11
11
  import { extractFileExtension, getTextByPathList, angleToDegrees, isVideoLink, escapeHtml, hasValidText, numberToFixed } from './utils'
12
12
  import { getShadow } from './shadow'
13
13
  import { getTableBorders, getTableCellParams, getTableRowParams } from './table'
@@ -267,7 +267,9 @@ async function processSingleSlide(zip, sldFileName, themeContent, defaultTextSty
267
267
  }
268
268
  }
269
269
 
270
+ let currentThemeContent = themeContent
270
271
  if (themeFilename) {
272
+ currentThemeContent = currentThemeContent || await readXmlFileCached(zip, themeFilename, xmlCache)
271
273
  const themeName = themeFilename.split('/').pop()
272
274
  const themeResFileName = themeFilename.replace(themeName, '_rels/' + themeName) + '.rels'
273
275
  const themeResContent = await readXmlFile(zip, themeResFileName)
@@ -305,7 +307,7 @@ async function processSingleSlide(zip, sldFileName, themeContent, defaultTextSty
305
307
  slideMasterTextStyles,
306
308
  layoutResObj,
307
309
  masterResObj,
308
- themeContent,
310
+ themeContent: currentThemeContent,
309
311
  themeResObj,
310
312
  diagramFileCache: {},
311
313
  defaultTextStyle,
@@ -618,8 +620,8 @@ async function processGroupSpNode(node, warpObj, source, parentGroupHierarchy =
618
620
  if (rotate) rotate = angleToDegrees(rotate)
619
621
 
620
622
  // 计算缩放因子
621
- const ws = cx / chcx
622
- const hs = cy / chcy
623
+ const ws = chcx === 0 ? 0 : cx / chcx
624
+ const hs = chcy === 0 ? 0 : cy / chcy
623
625
 
624
626
  // 构建当前组合层级(将当前组合添加到父级层级中)
625
627
  const currentGroupHierarchy = [...parentGroupHierarchy, node]
@@ -638,30 +640,44 @@ async function processGroupSpNode(node, warpObj, source, parentGroupHierarchy =
638
640
  }
639
641
  }
640
642
 
643
+ const transformGroupedElement = (element, offsetX = 0, offsetY = 0) => {
644
+ const elementRotate = element.rotate || 0
645
+ const normalizedRotate = ((elementRotate % 360) + 360) % 360
646
+ const isUniformScale = Math.abs(ws - hs) < 0.000001
647
+ const shouldSwapDimensions = normalizedRotate === 90 || normalizedRotate === 270
648
+ const centerX = element.left + element.width / 2
649
+ const centerY = element.top + element.height / 2
650
+ const nextCenterX = (centerX - offsetX) * ws
651
+ const nextCenterY = (centerY - offsetY) * hs
652
+ const widthScale = shouldSwapDimensions && !isUniformScale ? hs : ws
653
+ const heightScale = shouldSwapDimensions && !isUniformScale ? ws : hs
654
+ const width = element.width * widthScale
655
+ const height = element.height * heightScale
656
+
657
+ const transformed = {
658
+ ...element,
659
+ left: numberToFixed(nextCenterX - width / 2),
660
+ top: numberToFixed(nextCenterY - height / 2),
661
+ width: numberToFixed(width),
662
+ height: numberToFixed(height),
663
+ }
664
+ return transformed
665
+ }
666
+
641
667
  const processedElements = elements.map(element => ({
642
- ...element,
643
- left: numberToFixed((element.left - chx) * ws),
644
- top: numberToFixed((element.top - chy) * hs),
645
- width: numberToFixed(element.width * ws),
646
- height: numberToFixed(element.height * hs),
668
+ ...transformGroupedElement(element, chx, chy),
647
669
  ...(element.type === 'group' && element.elements ? {
648
- elements: processNestedGroupElements(element.elements, ws, hs)
670
+ elements: processNestedGroupElements(element.elements)
649
671
  } : {})
650
672
  }))
651
673
 
652
- function processNestedGroupElements(elements, ws, hs, depth = 0) {
674
+ function processNestedGroupElements(elements, depth = 0) {
653
675
  if (depth > 10) return elements
654
676
 
655
677
  return elements.map(element => {
656
- const processed = {
657
- ...element,
658
- left: numberToFixed(element.left * ws),
659
- top: numberToFixed(element.top * hs),
660
- width: numberToFixed(element.width * ws),
661
- height: numberToFixed(element.height * hs),
662
- }
678
+ const processed = transformGroupedElement(element)
663
679
  if (element.type === 'group' && element.elements) {
664
- processed.elements = processNestedGroupElements(element.elements, ws, hs, depth + 1)
680
+ processed.elements = processNestedGroupElements(element.elements, depth + 1)
665
681
  }
666
682
  return processed
667
683
  })
@@ -761,6 +777,7 @@ async function genShape(node, slideLayoutSpNode, slideMasterSpNode, name, type,
761
777
 
762
778
  const { top, left } = getPosition(slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode)
763
779
  const { width, height } = getSize(slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode)
780
+ const pathViewBox = { x: 0, y: 0, width, height }
764
781
 
765
782
  const isFlipV = getTextByPathList(slideXfrmNode, ['attrs', 'flipV']) === '1'
766
783
  const isFlipH = getTextByPathList(slideXfrmNode, ['attrs', 'flipH']) === '1'
@@ -779,7 +796,11 @@ async function genShape(node, slideLayoutSpNode, slideMasterSpNode, name, type,
779
796
  if (node['p:txBody']) content = genTextBody(node['p:txBody'], node, slideLayoutSpNode, slideMasterSpNode, type, warpObj)
780
797
 
781
798
  const { borderColor, borderWidth, borderType, strokeDasharray } = getBorder(node, type, warpObj)
782
- const fill = await getShapeFill(node, warpObj, source, groupHierarchy)
799
+ const fill = await getShapeFill(node, warpObj, source, {
800
+ groupHierarchy,
801
+ slideLayoutSpNode,
802
+ slideMasterSpNode,
803
+ })
783
804
 
784
805
  let shadow
785
806
  const outerShdwNode = getTextByPathList(node, ['p:spPr', 'a:effectLst', 'a:outerShdw'])
@@ -823,36 +844,48 @@ async function genShape(node, slideLayoutSpNode, slideMasterSpNode, name, type,
823
844
  const d = getCustomShapePath(custShapType, w, h)
824
845
  if (!isHasValidText) data.content = ''
825
846
 
826
- return {
847
+ const customShapeData = {
827
848
  ...data,
828
849
  type: 'shape',
829
850
  shapType: 'custom',
830
851
  path: d,
852
+ pathViewBox: { x: 0, y: 0, width: w, height: h },
831
853
  }
854
+ if (isStrokeOnlyCustomGeometry(custShapType)) customShapeData.strokeOnly = true
855
+
856
+ return customShapeData
832
857
  }
833
858
 
834
859
  let shapePath = ''
835
860
  if (shapType) shapePath = getShapePath(shapType, width, height, node)
861
+ const STROKE_ONLY_PRESET_SHAPE_TYPES = ['arc', 'leftBrace', 'rightBrace', 'bracePair', 'leftBracket', 'rightBracket', 'bracketPair']
862
+ const isStrokeOnlyPresetShape = STROKE_ONLY_PRESET_SHAPE_TYPES.includes(shapType)
836
863
 
837
864
  if (shapType && (type === 'obj' || !type || shapType !== 'rect')) {
838
865
  if (!isHasValidText) data.content = ''
839
- return {
866
+ const shapeData = {
840
867
  ...data,
841
868
  type: 'shape',
842
869
  shapType,
843
870
  path: shapePath,
871
+ pathViewBox,
844
872
  keypoints,
845
873
  }
874
+ if (isStrokeOnlyPresetShape) shapeData.strokeOnly = true
875
+ return shapeData
846
876
  }
847
877
  if (shapType && !isHasValidText && (fill || borderWidth)) {
848
- return {
878
+ const shapeData = {
849
879
  ...data,
850
880
  type: 'shape',
851
881
  content: '',
852
882
  shapType,
853
883
  path: shapePath,
884
+ pathViewBox,
854
885
  keypoints,
855
886
  }
887
+ if (isStrokeOnlyPresetShape) shapeData.strokeOnly = true
888
+ return shapeData
856
889
  }
857
890
  return {
858
891
  ...data,
package/src/shape.js CHANGED
@@ -223,6 +223,15 @@ export function getCustomShapePath(custShapType, w, h) {
223
223
  return d
224
224
  }
225
225
 
226
+ export function isStrokeOnlyCustomGeometry(custShapType) {
227
+ const pathNodes = getTextByPathList(custShapType, ['a:pathLst', 'a:path'])
228
+ if (!pathNodes) return false
229
+
230
+ const pathNodeList = Array.isArray(pathNodes) ? pathNodes : [pathNodes]
231
+
232
+ return pathNodeList.length === 1 && getTextByPathList(pathNodeList[0], ['attrs', 'fill']) === 'none'
233
+ }
234
+
226
235
  export function identifyShape(shapeData) {
227
236
  const pathLst = shapeData['a:pathLst']
228
237
  if (!pathLst || !pathLst['a:path']) return 'custom'