useAIPPT.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import { ref } from 'vue'
  2. import { nanoid } from 'nanoid'
  3. import type { ImageClipDataRange, PPTElement, PPTImageElement, PPTShapeElement, PPTTextElement, Slide, TextType } from '@/types/slides'
  4. import type { AIPPTSlide } from '@/types/AIPPT'
  5. import { useSlidesStore } from '@/store'
  6. import useAddSlidesOrElements from './useAddSlidesOrElements'
  7. import useSlideHandler from './useSlideHandler'
  8. interface ImgPoolItem {
  9. id: string
  10. src: string
  11. width: number
  12. height: number
  13. }
  14. export default () => {
  15. const slidesStore = useSlidesStore()
  16. const { addSlidesFromData } = useAddSlidesOrElements()
  17. const { isEmptySlide } = useSlideHandler()
  18. const imgPool = ref<ImgPoolItem[]>([])
  19. const transitionIndex = ref(0)
  20. const transitionTemplate = ref<Slide | null>(null)
  21. const checkTextType = (el: PPTElement, type: TextType) => {
  22. return (el.type === 'text' && el.textType === type) || (el.type === 'shape' && el.text && el.text.type === type)
  23. }
  24. const getUseableTemplates = (templates: Slide[], n: number, type: TextType) => {
  25. if (n === 1) {
  26. const list = templates.filter(slide => {
  27. const items = slide.elements.filter(el => checkTextType(el, type))
  28. const titles = slide.elements.filter(el => checkTextType(el, 'title'))
  29. const texts = slide.elements.filter(el => checkTextType(el, 'content'))
  30. return !items.length && titles.length === 1 && texts.length === 1
  31. })
  32. if (list.length) return list
  33. }
  34. let target: Slide | null = null
  35. const list = templates.filter(slide => {
  36. const len = slide.elements.filter(el => checkTextType(el, type)).length
  37. return len >= n
  38. })
  39. if (list.length === 0) {
  40. const sorted = templates.sort((a, b) => {
  41. const aLen = a.elements.filter(el => checkTextType(el, type)).length
  42. const bLen = b.elements.filter(el => checkTextType(el, type)).length
  43. return aLen - bLen
  44. })
  45. target = sorted[sorted.length - 1]
  46. }
  47. else {
  48. target = list.reduce((closest, current) => {
  49. const currentLen = current.elements.filter(el => checkTextType(el, type)).length
  50. const closestLen = closest.elements.filter(el => checkTextType(el, type)).length
  51. return (currentLen - n) <= (closestLen - n) ? current : closest
  52. })
  53. }
  54. return templates.filter(slide => {
  55. const len = slide.elements.filter(el => checkTextType(el, type)).length
  56. const targetLen = target!.elements.filter(el => checkTextType(el, type)).length
  57. return len === targetLen
  58. })
  59. }
  60. const getAdaptedFontsize = ({
  61. text,
  62. fontSize,
  63. fontFamily,
  64. width,
  65. maxLine,
  66. }: {
  67. text: string
  68. fontSize: number
  69. fontFamily: string
  70. width: number
  71. maxLine: number
  72. }) => {
  73. const canvas = document.createElement('canvas')
  74. const context = canvas.getContext('2d')!
  75. let newFontSize = fontSize
  76. const minFontSize = 10
  77. while (newFontSize >= minFontSize) {
  78. context.font = `${newFontSize}px ${fontFamily}`
  79. const textWidth = context.measureText(text).width
  80. const line = Math.ceil(textWidth / width)
  81. if (line <= maxLine) return newFontSize
  82. const step = newFontSize <= 22 ? 1 : 2
  83. newFontSize = newFontSize - step
  84. }
  85. return minFontSize
  86. }
  87. const getFontInfo = (htmlString: string) => {
  88. const fontSizeRegex = /font-size:\s*(\d+(?:\.\d+)?)\s*px/i
  89. const fontFamilyRegex = /font-family:\s*['"]?([^'";]+)['"]?\s*(?=;|>|$)/i
  90. const defaultInfo = {
  91. fontSize: 16,
  92. fontFamily: 'Microsoft Yahei',
  93. }
  94. const fontSizeMatch = htmlString.match(fontSizeRegex)
  95. const fontFamilyMatch = htmlString.match(fontFamilyRegex)
  96. return {
  97. fontSize: fontSizeMatch ? (+fontSizeMatch[1].trim()) : defaultInfo.fontSize,
  98. fontFamily: fontFamilyMatch ? fontFamilyMatch[1].trim() : defaultInfo.fontFamily,
  99. }
  100. }
  101. const getNewTextElement = ({
  102. el,
  103. text,
  104. maxLine,
  105. longestText,
  106. digitPadding,
  107. }: {
  108. el: PPTTextElement | PPTShapeElement
  109. text: string
  110. maxLine: number
  111. longestText?: string
  112. digitPadding?: boolean
  113. }): PPTTextElement | PPTShapeElement => {
  114. const padding = 10
  115. const width = el.width - padding * 2 - 2
  116. let content = el.type === 'text' ? el.content : el.text!.content
  117. const fontInfo = getFontInfo(content)
  118. const size = getAdaptedFontsize({
  119. text: longestText || text,
  120. fontSize: fontInfo.fontSize,
  121. fontFamily: fontInfo.fontFamily,
  122. width,
  123. maxLine,
  124. })
  125. const parser = new DOMParser()
  126. const doc = parser.parseFromString(content, 'text/html')
  127. const treeWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
  128. const firstTextNode = treeWalker.nextNode()
  129. if (firstTextNode) {
  130. if (digitPadding && firstTextNode.textContent && firstTextNode.textContent.length === 2 && text.length === 1) {
  131. firstTextNode.textContent = '0' + text
  132. }
  133. else firstTextNode.textContent = text
  134. }
  135. if (doc.body.innerHTML.indexOf('font-size') === -1) {
  136. const p = doc.querySelector('p')
  137. if (p) p.style.fontSize = '16px'
  138. }
  139. content = doc.body.innerHTML.replace(/font-size:(.+?)px/g, `font-size: ${size}px`)
  140. return el.type === 'text' ? { ...el, content, lineHeight: size < 15 ? 1.2 : el.lineHeight } : { ...el, text: { ...el.text!, content } }
  141. }
  142. const getUseableImage = (el: PPTImageElement): ImgPoolItem | null => {
  143. let img: ImgPoolItem | null = null
  144. let imgs = []
  145. if (el.width === el.height) imgs = imgPool.value.filter(img => img.width === img.height)
  146. else if (el.width > el.height) imgs = imgPool.value.filter(img => img.width > img.height)
  147. else imgs = imgPool.value.filter(img => img.width <= img.height)
  148. if (!imgs.length) imgs = imgPool.value
  149. img = imgs[Math.floor(Math.random() * imgs.length)]
  150. imgPool.value = imgPool.value.filter(item => item.id !== img!.id)
  151. return img
  152. }
  153. const getNewImgElement = (el: PPTImageElement): PPTImageElement => {
  154. const img = getUseableImage(el)
  155. if (!img) return el
  156. let scale = 1
  157. let w = el.width
  158. let h = el.height
  159. let range: ImageClipDataRange = [[0, 0], [0, 0]]
  160. const radio = el.width / el.height
  161. if (img.width / img.height >= radio) {
  162. scale = img.height / el.height
  163. w = img.width / scale
  164. const diff = (w - el.width) / 2 / w * 100
  165. range = [[diff, 0], [100 - diff, 100]]
  166. }
  167. else {
  168. scale = img.width / el.width
  169. h = img.height / scale
  170. const diff = (h - el.height) / 2 / h * 100
  171. range = [[0, diff], [100, 100 - diff]]
  172. }
  173. const clipShape = (el.clip && el.clip.shape) ? el.clip.shape : 'rect'
  174. const clip = { range, shape: clipShape }
  175. const src = img.src
  176. return { ...el, src, clip }
  177. }
  178. const getMdContent = (content: string) => {
  179. const regex = /```markdown([^```]*)```/
  180. const match = content.match(regex)
  181. if (match) return match[1].trim()
  182. return content.replace('```markdown', '').replace('```', '')
  183. }
  184. const getJSONContent = (content: string) => {
  185. const regex = /```json([^```]*)```/
  186. const match = content.match(regex)
  187. if (match) return match[1].trim()
  188. return content.replace('```json', '').replace('```', '')
  189. }
  190. const presetImgPool = (imgs: ImgPoolItem[]) => {
  191. imgPool.value = imgs
  192. }
  193. const AIPPT = (templateSlides: Slide[], _AISlides: AIPPTSlide[], imgs?: ImgPoolItem[]) => {
  194. slidesStore.updateSlideIndex(slidesStore.slides.length - 1)
  195. if (imgs) imgPool.value = imgs
  196. const AISlides: AIPPTSlide[] = []
  197. for (const template of _AISlides) {
  198. if (template.type === 'content') {
  199. const items = template.data.items
  200. if (items.length === 5 || items.length === 6) {
  201. const items1 = items.slice(0, 3)
  202. const items2 = items.slice(3)
  203. AISlides.push({ ...template, data: { ...template.data, items: items1 } })
  204. AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 3 })
  205. }
  206. else if (items.length === 7 || items.length === 8) {
  207. const items1 = items.slice(0, 4)
  208. const items2 = items.slice(4)
  209. AISlides.push({ ...template, data: { ...template.data, items: items1 } })
  210. AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 4 })
  211. }
  212. else if (items.length === 9 || items.length === 10) {
  213. const items1 = items.slice(0, 3)
  214. const items2 = items.slice(3, 6)
  215. const items3 = items.slice(6)
  216. AISlides.push({ ...template, data: { ...template.data, items: items1 } })
  217. AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 3 })
  218. AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 6 })
  219. }
  220. else if (items.length > 10) {
  221. const items1 = items.slice(0, 4)
  222. const items2 = items.slice(4, 8)
  223. const items3 = items.slice(8)
  224. AISlides.push({ ...template, data: { ...template.data, items: items1 } })
  225. AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 4 })
  226. AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 8 })
  227. }
  228. else {
  229. AISlides.push(template)
  230. }
  231. }
  232. else if (template.type === 'contents') {
  233. const items = template.data.items
  234. if (items.length === 11) {
  235. const items1 = items.slice(0, 6)
  236. const items2 = items.slice(6)
  237. AISlides.push({ ...template, data: { ...template.data, items: items1 } })
  238. AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 6 })
  239. }
  240. else if (items.length > 11) {
  241. const items1 = items.slice(0, 10)
  242. const items2 = items.slice(10)
  243. AISlides.push({ ...template, data: { ...template.data, items: items1 } })
  244. AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 10 })
  245. }
  246. else {
  247. AISlides.push(template)
  248. }
  249. }
  250. else AISlides.push(template)
  251. }
  252. const coverTemplates = templateSlides.filter(slide => slide.type === 'cover')
  253. const contentsTemplates = templateSlides.filter(slide => slide.type === 'contents')
  254. const transitionTemplates = templateSlides.filter(slide => slide.type === 'transition')
  255. const contentTemplates = templateSlides.filter(slide => slide.type === 'content')
  256. const endTemplates = templateSlides.filter(slide => slide.type === 'end')
  257. if (!transitionTemplate.value) {
  258. const _transitionTemplate = transitionTemplates[Math.floor(Math.random() * transitionTemplates.length)]
  259. transitionTemplate.value = _transitionTemplate
  260. }
  261. const slides = []
  262. for (const item of AISlides) {
  263. if (item.type === 'cover') {
  264. const coverTemplate = coverTemplates[Math.floor(Math.random() * coverTemplates.length)]
  265. const elements = coverTemplate.elements.map(el => {
  266. if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
  267. if (el.type !== 'text' && el.type !== 'shape') return el
  268. if (checkTextType(el, 'title') && item.data.title) {
  269. return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
  270. }
  271. if (checkTextType(el, 'content') && item.data.text) {
  272. return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
  273. }
  274. return el
  275. })
  276. slides.push({
  277. ...coverTemplate,
  278. id: nanoid(10),
  279. elements,
  280. })
  281. }
  282. else if (item.type === 'contents') {
  283. const _contentsTemplates = getUseableTemplates(contentsTemplates, item.data.items.length, 'item')
  284. const contentsTemplate = _contentsTemplates[Math.floor(Math.random() * _contentsTemplates.length)]
  285. const sortedNumberItems = contentsTemplate.elements.filter(el => checkTextType(el, 'itemNumber'))
  286. const sortedNumberItemIds = sortedNumberItems.sort((a, b) => {
  287. if (sortedNumberItems.length > 6) {
  288. let aContent = ''
  289. let bContent = ''
  290. if (a.type === 'text') aContent = a.content
  291. if (a.type === 'shape') aContent = a.text!.content
  292. if (b.type === 'text') bContent = b.content
  293. if (b.type === 'shape') bContent = b.text!.content
  294. if (aContent && bContent) {
  295. const aIndex = parseInt(aContent)
  296. const bIndex = parseInt(bContent)
  297. return aIndex - bIndex
  298. }
  299. }
  300. const aIndex = a.left + a.top * 2
  301. const bIndex = b.left + b.top * 2
  302. return aIndex - bIndex
  303. }).map(el => el.id)
  304. const sortedItems = contentsTemplate.elements.filter(el => checkTextType(el, 'item'))
  305. const sortedItemIds = sortedItems.sort((a, b) => {
  306. if (sortedItems.length > 6) {
  307. const aItemNumber = sortedNumberItems.find(item => item.groupId === a.groupId)
  308. const bItemNumber = sortedNumberItems.find(item => item.groupId === b.groupId)
  309. if (aItemNumber && bItemNumber) {
  310. let aContent = ''
  311. let bContent = ''
  312. if (aItemNumber.type === 'text') aContent = aItemNumber.content
  313. if (aItemNumber.type === 'shape') aContent = aItemNumber.text!.content
  314. if (bItemNumber.type === 'text') bContent = bItemNumber.content
  315. if (bItemNumber.type === 'shape') bContent = bItemNumber.text!.content
  316. if (aContent && bContent) {
  317. const aIndex = parseInt(aContent)
  318. const bIndex = parseInt(bContent)
  319. return aIndex - bIndex
  320. }
  321. }
  322. }
  323. const aIndex = a.left + a.top * 2
  324. const bIndex = b.left + b.top * 2
  325. return aIndex - bIndex
  326. }).map(el => el.id)
  327. const longestText = item.data.items.reduce((longest, current) => current.length > longest.length ? current : longest, '')
  328. const unusedElIds: string[] = []
  329. const unusedGroupIds: string[] = []
  330. const elements = contentsTemplate.elements.map(el => {
  331. if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
  332. if (el.type !== 'text' && el.type !== 'shape') return el
  333. if (checkTextType(el, 'item')) {
  334. const index = sortedItemIds.findIndex(id => id === el.id)
  335. const itemTitle = item.data.items[index]
  336. if (itemTitle) return getNewTextElement({ el, text: itemTitle, maxLine: 1, longestText })
  337. unusedElIds.push(el.id)
  338. if (el.groupId) unusedGroupIds.push(el.groupId)
  339. }
  340. if (checkTextType(el, 'itemNumber')) {
  341. const index = sortedNumberItemIds.findIndex(id => id === el.id)
  342. const offset = item.offset || 0
  343. return getNewTextElement({ el, text: index + offset + 1 + '', maxLine: 1, digitPadding: true })
  344. }
  345. return el
  346. }).filter(el => !unusedElIds.includes(el.id) && !(el.groupId && unusedGroupIds.includes(el.groupId)))
  347. slides.push({
  348. ...contentsTemplate,
  349. id: nanoid(10),
  350. elements,
  351. })
  352. }
  353. else if (item.type === 'transition') {
  354. transitionIndex.value = transitionIndex.value + 1
  355. const elements = transitionTemplate.value.elements.map(el => {
  356. if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
  357. if (el.type !== 'text' && el.type !== 'shape') return el
  358. if (checkTextType(el, 'title') && item.data.title) {
  359. return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
  360. }
  361. if (checkTextType(el, 'content') && item.data.text) {
  362. return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
  363. }
  364. if (checkTextType(el, 'partNumber')) {
  365. return getNewTextElement({ el, text: transitionIndex.value + '', maxLine: 1, digitPadding: true })
  366. }
  367. return el
  368. })
  369. slides.push({
  370. ...transitionTemplate.value,
  371. id: nanoid(10),
  372. elements,
  373. })
  374. }
  375. else if (item.type === 'content') {
  376. const _contentTemplates = getUseableTemplates(contentTemplates, item.data.items.length, 'item')
  377. const contentTemplate = _contentTemplates[Math.floor(Math.random() * _contentTemplates.length)]
  378. const sortedTitleItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'itemTitle')).sort((a, b) => {
  379. const aIndex = a.left + a.top * 2
  380. const bIndex = b.left + b.top * 2
  381. return aIndex - bIndex
  382. }).map(el => el.id)
  383. const sortedTextItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'item')).sort((a, b) => {
  384. const aIndex = a.left + a.top * 2
  385. const bIndex = b.left + b.top * 2
  386. return aIndex - bIndex
  387. }).map(el => el.id)
  388. const sortedNumberItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'itemNumber')).sort((a, b) => {
  389. const aIndex = a.left + a.top * 2
  390. const bIndex = b.left + b.top * 2
  391. return aIndex - bIndex
  392. }).map(el => el.id)
  393. const itemTitles = []
  394. const itemTexts = []
  395. for (const _item of item.data.items) {
  396. if (_item.title) itemTitles.push(_item.title)
  397. if (_item.text) itemTexts.push(_item.text)
  398. }
  399. const longestTitle = itemTitles.reduce((longest, current) => current.length > longest.length ? current : longest, '')
  400. const longestText = itemTexts.reduce((longest, current) => current.length > longest.length ? current : longest, '')
  401. const elements = contentTemplate.elements.map(el => {
  402. if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
  403. if (el.type !== 'text' && el.type !== 'shape') return el
  404. if (item.data.items.length === 1) {
  405. const contentItem = item.data.items[0]
  406. if (checkTextType(el, 'content') && contentItem.text) {
  407. return getNewTextElement({ el, text: contentItem.text, maxLine: 6 })
  408. }
  409. }
  410. else {
  411. if (checkTextType(el, 'itemTitle')) {
  412. const index = sortedTitleItemIds.findIndex(id => id === el.id)
  413. const contentItem = item.data.items[index]
  414. if (contentItem && contentItem.title) {
  415. return getNewTextElement({ el, text: contentItem.title, longestText: longestTitle, maxLine: 1 })
  416. }
  417. }
  418. if (checkTextType(el, 'item')) {
  419. const index = sortedTextItemIds.findIndex(id => id === el.id)
  420. const contentItem = item.data.items[index]
  421. if (contentItem && contentItem.text) {
  422. return getNewTextElement({ el, text: contentItem.text, longestText, maxLine: 4 })
  423. }
  424. }
  425. if (checkTextType(el, 'itemNumber')) {
  426. const index = sortedNumberItemIds.findIndex(id => id === el.id)
  427. const offset = item.offset || 0
  428. return getNewTextElement({ el, text: index + offset + 1 + '', maxLine: 1, digitPadding: true })
  429. }
  430. }
  431. if (checkTextType(el, 'title') && item.data.title) {
  432. return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
  433. }
  434. return el
  435. })
  436. slides.push({
  437. ...contentTemplate,
  438. id: nanoid(10),
  439. elements,
  440. })
  441. }
  442. else if (item.type === 'end') {
  443. const endTemplate = endTemplates[Math.floor(Math.random() * endTemplates.length)]
  444. const elements = endTemplate.elements.map(el => {
  445. if (el.type === 'image' && el.imageType && imgPool.value.length) return getNewImgElement(el)
  446. return el
  447. })
  448. slides.push({
  449. ...endTemplate,
  450. id: nanoid(10),
  451. elements,
  452. })
  453. }
  454. }
  455. if (isEmptySlide.value) slidesStore.setSlides(slides)
  456. else addSlidesFromData(slides)
  457. }
  458. return {
  459. presetImgPool,
  460. AIPPT,
  461. getMdContent,
  462. getJSONContent,
  463. }
  464. }