ソースを参照

add: 提取页面

wzl 1 年間 前
コミット
ab0de023c8

+ 44 - 20
packages/core/src/index.js

@@ -2749,21 +2749,7 @@ class ComPDFKitViewer {
     }
   }
 
-  async copyDocument() {
-    const doc = await this.messageHandler.sendWithPromise('copyDocument', {
-      doc: this.doc,
-      oldPassword: this.#oldPwd
-    })
-
-    const pages = await this.updatePages(doc)
-
-    this.docEditorCopy = {
-      doc,
-      pages
-    }
-  }
-
-  async updatePages(doc) {
+  async getPages(doc) {
     const pagesCount = await this.messageHandler.sendWithPromise("GetPageCount", { doc })
 
     const pages = []
@@ -2785,8 +2771,16 @@ class ComPDFKitViewer {
   // 页面编辑 - 获取每个页面图片
   // 获取范围内的页面图片 index起始索引 num页面数量
   async getDocEditorPages(index = 0, num = null) {
-    if (!this.docEditorCopy) await this.copyDocument()
-    const { doc, pages } = this.docEditorCopy
+    if (!this.docEditorCopy) {
+      const doc = await this.messageHandler.sendWithPromise('CopyDocument', {
+        doc: this.doc,
+        oldPassword: this.#oldPwd
+      })
+      const pages = await this.getPages(doc)
+  
+      this.docEditorCopy = { doc, pages }
+    }
+    const pages = this.docEditorCopy.pages
     const radio = window.devicePixelRatio || 1
 
     for (let i = 0; i < (num ?? pages.length); i++) {
@@ -2870,14 +2864,14 @@ class ComPDFKitViewer {
     if (op.type === 'insert') {
       if (op.file) {
         const res = await this.messageHandler.sendWithPromise('ImportPagesAtIndex', {
-          doc: this.docEditorCopy.doc,
+          doc,
           file: op.file,
           range: op.range,
           index: op.index
         })
         if(!res) console.warn('insert pdf', res)
         
-        this.docEditorCopy.pages = await this.updatePages(doc)
+        this.docEditorCopy.pages = await this.getPages(doc)
         return this.docEditorCopy.pages.length
       } else {
         const res = await this.messageHandler.sendWithPromise('InsertPage', {
@@ -2915,7 +2909,37 @@ class ComPDFKitViewer {
         if(!res) console.warn('copy', res)
       })
     }
-    this.docEditorCopy.pages = await this.updatePages(doc)
+    if (op.type === 'extract') {
+      const blobData = await this.messageHandler.sendWithPromise('ExtractPage', {
+        doc,
+        range: op.range,
+        separateFile: op.separateFile
+      })
+      if(!blobData) console.warn('extract', 0)
+
+      if (op.separateFile) {
+        const zip = new JSZip()
+
+        let files = []
+        for (let i = 0; i < blobData.length; i++) {
+          const file = blobData[i]
+          files.push({ blobData: file.blobData, filename: file.name + '.pdf' })
+        }
+        
+        files.forEach((file) => {
+          zip.file(file.filename, file.blobData, { binary: true })
+        })
+        
+        zip.generateAsync({ type: 'blob' }).then((content) => {
+          saveAs(content, 'extracted documents.zip')
+        })
+      } else {
+        const filename = this._docName
+        saveAs(blobData, filename)
+      }
+      return
+    }
+    this.docEditorCopy.pages = await this.getPages(doc)
   }
 }
 

+ 38 - 1
packages/core/src/worker/compdfkit_worker.js

@@ -1043,7 +1043,7 @@ class CPDFWorker {
       fileReader.readAsArrayBuffer(data.fontFile)
     })
 
-    messageHandler.on('copyDocument', async (data) => {
+    messageHandler.on('CopyDocument', async (data) => {
       const { doc, oldPassword } = data
       const newDoc = await copyDocument(doc, oldPassword)
       return newDoc
@@ -1144,6 +1144,43 @@ class CPDFWorker {
     messageHandler.on('ClearDocument', (doc) => {
       Module._ClearDocument(doc)
     })
+
+    messageHandler.on('ExtractPage', async (data) => {
+      const { doc, range, separateFile } = data
+
+      if (separateFile) {
+        let blobDataList = []
+
+        for (let i = 0; i < range.length; i++) {
+          const pageNum = range[i] + 1 + ''
+          
+          const page = stringToNewUTF8(pageNum)
+          const newDoc = Module._InitDocument()
+          Module._CreateDocument(newDoc)
+          Module._ImportPagesAtIndex(
+            newDoc,
+            doc,
+            page,
+            0
+          )
+          const blobData = saveDocument(newDoc, 2)
+          blobDataList.push({ name: pageNum, blobData })
+        }
+        return blobDataList
+
+      } else {
+        const exportRange = stringToNewUTF8(range)
+        const newDoc = Module._InitDocument()
+        Module._CreateDocument(newDoc)
+        Module._ImportPagesAtIndex(
+          newDoc,
+          doc,
+          exportRange,
+          0
+        )
+        return saveDocument(newDoc, 2)
+      }
+    })
   }
 }
 

+ 256 - 0
packages/webview/src/components/Dialogs/ExtractPageSettingDialog.vue

@@ -0,0 +1,256 @@
+<template>
+  <div class="extract-page-setting-popup" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName" :close="false">
+      <template #header>
+        <p>Extract</p>
+      </template>
+
+      <p class="title">{{ $t('documentEditor.dialog.pageRange') }}</p>
+
+      <div class="container">
+        <div class="select-content">
+          <div @click="pageRange = 'all'" class="option"><RadioBtnSel v-if="pageRange === 'all'" /><RadioBtnDis v-else />{{ $t('documentEditor.dialog.allPages') }}</div>
+          <div @click="pageRange = 'odd'" class="option"><RadioBtnSel v-if="pageRange === 'odd'" /><RadioBtnDis v-else />{{ $t('documentEditor.dialog.oddPage') }}</div>
+          <div @click="pageRange = 'even'" class="option"><RadioBtnSel v-if="pageRange === 'even'" /><RadioBtnDis v-else />{{ $t('documentEditor.dialog.evenPage') }}</div>
+          <div @click="pageRange = 'custom'" class="option custom-page">
+            <RadioBtnSel v-if="pageRange === 'custom'" /><RadioBtnDis v-else />{{ $t('documentEditor.dialog.page') }}
+            <div class="addition">
+              <input type="text" v-model="customRange" :placeholder="$t('documentEditor.dialog.pageTip')" @blur="validateCustomRange">
+              <span>/ {{ totalPages }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="check-content">
+        <div class="check-box" @click="separateFile = !separateFile">
+          <div class="check" :class="{'active': separateFile}"><Checkbox v-show="separateFile" /></div>
+          <span>{{ $t('documentEditor.eachPage') }}</span>
+        </div>
+        <div class="check-box" @click="deleteAfter = !deleteAfter">
+          <div class="check" :class="{'active': deleteAfter}"><Checkbox v-show="deleteAfter" /></div>
+          <span>{{ $t('documentEditor.deleteAfter') }}</span>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="rect-button white" @click="closeDialog">{{ $t('cancel') }}</div>
+        <div class="rect-button blue" :class="{ 'disabled': pageRange === 'pdf' && (!inputFile || (pageRange === 'custom' && !validCustomRange)) }" @click="confirm">{{ $t('documentEditor.extract') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+
+const props = defineProps([ 'selectedPageList', 'totalPages' ])
+const emits = defineEmits(['extractPage'])
+
+const useViewer = useViewerStore()
+
+const dialogName = 'extractPageSettingDialog'
+const show = computed(() => useViewer.isElementOpen(dialogName))
+
+const pageRange = ref('custom')
+const customRange = ref('')
+const validCustomRange = ref('')
+const separateFile = ref(false)
+const deleteAfter = ref(false)
+let initialRange = ''
+
+watch(() => show.value, (newVal, oldVal) => {
+  if (newVal) {
+    const selectedPageStr = props.selectedPageList.map(function(num) {
+      return num + 1
+    }).join(',')
+
+    initialRange = formatText(selectedPageStr)
+    customRange.value = validCustomRange.value = initialRange
+  }
+})
+
+// 关闭弹窗
+const closeDialog = () => {
+  useViewer.closeElement(dialogName)
+
+  pageRange.value = 'custom'
+}
+
+// 确认
+const confirm = async () => {
+  const data = {
+    range: pageRange.value === 'custom' ? customRange.value : pageRange.value,
+    separateFile: separateFile.value,
+    deleteAfter: deleteAfter.value
+  }
+  emits('extractPage', data)
+  closeDialog()
+}
+
+// 输入自定义页面范围校验 格式化
+const validateCustomRange = () => {
+  validCustomRange.value = formatText(customRange.value)
+  customRange.value = validCustomRange.value
+}
+const formatText = (inputText) => {
+  const max = props.totalPages
+  const text = inputText.replace(/\s/g, '')
+  const matches = text.match(/\d+-\d+|\d+|\d+/g)
+
+  if (matches) {
+    const sortedNums = matches
+      .flatMap(match => {
+        if (match.includes('-')) {
+          const [start, end] = match.split('-')
+          const smallest = Math.min(start, end)
+          const largest = Math.min(Math.max(start, end), max)
+          return Array.from({ length: largest - smallest + 1 }, (_, i) =>
+            String(Number(smallest) + i)
+          )
+        } else {
+          return Math.min(match, max)
+        }
+      })
+      .sort((a, b) => a - b)
+
+    const formattedText = []
+
+    let start = sortedNums[0]
+    let prev = sortedNums[0]
+
+    for (let i = 1; i < sortedNums.length; i++) {
+      const current = sortedNums[i]
+
+      const prevNum = Number(prev)
+      const currentNum = Number(current)
+
+      if (currentNum - prevNum > 1) {
+        formattedText.push(formatRange(start, prev))
+        start = current
+      }
+      prev = current
+    }
+    formattedText.push(formatRange(start, prev))
+    return formattedText.join(', ')
+  }
+  return initialRange
+}
+const formatRange = (start, end) => {
+  return (start === end) ? start.toString() : `${start}-${end}`
+}
+</script>
+
+<style lang="scss">
+.extract-page-setting-popup {
+  .dialog-container {
+    width: 460px;
+    box-shadow: 0px 4px 32px 0px rgba(129, 149, 200, 0.32);
+
+    main {
+      margin-top: 16px;
+    }
+  }
+
+  .title {
+    font-size: 14px;
+    font-weight: 700;
+    line-height: 16px;
+  }
+
+  .container {
+    margin-top: 8px;
+    margin-bottom: 16px;
+    padding: 8px 16px 16px;
+    border-radius: 4px;
+    background: #F2F3F5;
+
+    .select-content .option:not(:first-child) {
+      margin-top: 16px;
+    }
+    
+    .addition {
+      position: relative;
+      display: flex;
+      align-items: center;
+      
+      span {
+        text-wrap: nowrap;
+        font-size: 14px;
+        line-height: 16px;
+      }
+
+      input {
+        padding: 0 20px 0 8px;
+        width: 100%;
+        height: 24px;
+        background: var(--c-right-side-content-fillbox-bg);
+        border: 1px solid var(--c-right-side-content-fillbox-border);
+        border-radius: 1px;
+        font-size: 14px;
+        line-height: 16px;
+      }
+
+      input[type="text"]:focus {
+        border-color: #0078D7;
+      }
+    }
+
+    .option.custom-page {
+      padding: 0;
+
+      .addition {
+        flex: 1;
+        margin-left: 20px;
+
+        span {
+          margin-left: 8px;
+          color: #999;
+        }
+      }
+    }
+  }
+
+  .check-content {
+    padding: 0 16px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .check-box {
+      padding: 5px 0;
+      display: inline-flex;
+      align-items: center;
+      cursor: pointer;
+
+      & + .check-box {
+        margin-top: 8px;
+      }
+
+      .check {
+        margin-right: 4px;
+        width: 14px;
+        height: 14px;
+        background: var(--c-right-side-content-fillbox-bg);
+        border: 1px solid var(--c-right-side-content-fillbox-border);
+        border-radius: 1px;
+        overflow: hidden;
+
+        &.active {
+          border: none;
+        }
+
+        svg {
+          vertical-align: top;
+        }
+      }
+
+      span {
+        font-size: 14px;
+        line-height: 16px;
+      }
+    }
+  }
+}
+</style>

+ 6 - 7
packages/webview/src/components/Dialogs/InsertPageSettingDialog.vue

@@ -87,14 +87,11 @@
 <script setup>
 import { ref, computed, watch, h } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
-import { useDocumentStore } from '@/stores/modules/document'
-import core from '@/core'
 
 const props = defineProps([ 'selectedPageIndex', 'totalPages' ])
 const emits = defineEmits(['insertPage'])
 
 const useViewer = useViewerStore()
-const useDocument = useDocumentStore()
 
 const dialogName = 'insertPageSettingDialog'
 const show = computed(() => useViewer.isElementOpen(dialogName))
@@ -110,8 +107,10 @@ const place = ref('first')
 const targetIndex = ref(1)
 const targetPlace = ref(0)
 
-watch(() => props.selectedPageIndex, (newVal, oldVal) => {
-  targetIndex.value = newVal > 0 ? newVal : 1
+watch(() => show.value, (newVal, oldVal) => {
+  if (newVal) {
+    targetIndex.value = props.selectedPageIndex > 0 ? props.selectedPageIndex : 1
+  }
 })
 
 // 关闭弹窗
@@ -200,7 +199,7 @@ const validateCustomRange = () => {
   validCustomRange.value = formatText(customRange.value)
   customRange.value = validCustomRange.value
 }
-function formatText(inputText) {
+const formatText = (inputText) => {
   const text = inputText.replace(/\s/g, '')
   const matches = text.match(/\d+-\d+|\d+|\d+/g)
 
@@ -242,7 +241,7 @@ function formatText(inputText) {
   }
   return '1'
 }
-function formatRange(start, end) {
+const formatRange = (start, end) => {
   return (start === end) ? start.toString() : `${start}-${end}`
 }
 </script>

+ 58 - 2
packages/webview/src/components/DocumentEditorContainer/DocumentEditorContainer.vue

@@ -22,7 +22,7 @@
         <CopyPage />
         <span>{{ $t('documentEditor.copy') }}</span>
       </Button>
-      <Button class="with-text" :class="{ disabled: !selectedPageList.length }" @click="">
+      <Button class="with-text" :class="{ disabled: !selectedPageList.length }" @click="openExtractPageDialog">
         <ExtractPage />
         <span>{{ $t('documentEditor.extract') }}</span>
       </Button>
@@ -58,6 +58,7 @@
   </div>
   <InsertPageSettingDialog @insertPage="handleInsertPage" :selectedPageIndex="selectedPageList.length === 1 ? selectedPageList[0] + 1 : -1" :totalPages="pageList.length" />
   <DeletePageDialog @deletePage="handleDeletePage" />
+  <ExtractPageSettingDialog @extractPage="handleExtractPage" :totalPages="pageList.length" :selectedPageList="selectedPageList" />
 </template>
 
 <script setup>
@@ -65,6 +66,7 @@ import { computed, ref, watch, onMounted, reactive, onUnmounted } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
 import { useDocumentStore } from '@/stores/modules/document'
 import core from '@/core'
+import { saveAs } from 'file-saver'
 
 const useViewer = useViewerStore()
 const useDocument = useDocumentStore()
@@ -184,7 +186,6 @@ const handleDeletePage = () => {
 
 // 旋转页面
 const rotatePage = (rotation) => {
-  selectedPageList.sort((a, b) => b - a)
   selectedPageList.forEach(index => {
     const page = pageList[index]
     page.rotation += rotation
@@ -240,6 +241,61 @@ const replacePage = () => {
     handleDeletePage()
   })
 }
+
+// 打开提取页面弹窗
+const openExtractPageDialog = () => {
+  useViewer.openElement('extractPageSettingDialog')
+}
+
+// 提取页面
+const handleExtractPage = async (data) => {
+  let exportRange = data.range.replace(/\s/g, '')
+  const pagesCount = pageList.length
+  if (data.range === 'all') {
+    exportRange = '1-' + pagesCount
+    // exportRange = ''
+  } else if (data.range === 'odd') {
+    exportRange = Array.from({ length: pagesCount }, (_, i) => i + 1).filter(num => num % 2 !== 0).join(",")
+  } else if (data.range === 'even') {
+    exportRange = Array.from({ length: pagesCount }, (_, i) => i + 1).filter(num => num % 2 === 0).join(",")
+  }
+
+  let parts = exportRange.split(',')
+  let indexArray = []
+
+  parts.forEach(part => {
+    if (part.includes('-')) {
+      let [start, end] = part.split('-').map(num => parseInt(num))
+      for (let i = start; i <= end; i++) {
+        indexArray.push(i - 1)
+      }
+    } else {
+      indexArray.push(parseInt(part) - 1)
+    }
+  })
+
+  const operation = {
+    type: 'extract',
+    range: data.separateFile ? indexArray : exportRange,
+    separateFile: data.separateFile
+  }
+  await core.saveDocumentEdit(operation)
+  useDocument.setDocEditorOperationList(operation)
+
+  if (data.deleteAfter) {
+    indexArray.reverse().forEach(index => {
+      pageList.splice(index, 1)
+    })
+
+    const operation = {
+      type: 'delete',
+      selectedIndexArray: indexArray
+    }
+    core.saveDocumentEdit(operation)
+    useDocument.setDocEditorOperationList(operation)
+    selectedPageList.length = 0
+  }
+}
 </script>
 
 <style lang="scss">

+ 1 - 1
packages/webview/src/components/DocumentEditorHeader/DocumentEditorHeader.vue

@@ -14,7 +14,7 @@ import { computed, watch } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
 import { useDocumentStore } from '@/stores/modules/document'
 import core from '@/core'
-import { saveAs } from 'file-saver';
+import { saveAs } from 'file-saver'
 
 const useViewer = useViewerStore()
 const useDocument = useDocumentStore()

+ 4 - 2
packages/webview/src/stores/modules/viewer.js

@@ -36,7 +36,8 @@ export const useViewerStore = defineStore({
       editTextPanel: false,
       preventDialog: false,
       insertPageSettingDialog: false,
-      deletePageDialog: false
+      deletePageDialog: false,
+      extractPageSettingDialog: false
     },
     activeElementsTab: {
       leftPanelTab: 'THUMBS',
@@ -358,7 +359,8 @@ export const useViewerStore = defineStore({
         printSettingDialog: false,
         editTextPanel: false,
         insertPageSettingDialog: false,
-        deletePageDialog: false
+        deletePageDialog: false,
+        extractPageSettingDialog: false
       },
       this.activeElementsTab = {
         leftPanelTab: 'THUMBS',