diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 77d912e4..ae923e59 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -104,7 +104,7 @@ jobs:
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
- sudo apt-get install libvips
+ sudo apt-get install libvips liblept5
- name: Run Brakeman
run: bundle exec brakeman -q --exit-on-warn
@@ -162,7 +162,7 @@ jobs:
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
- sudo apt-get install -y libvips
+ sudo apt-get install -y libvips liblept5
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
rm -f pdfium-linux.tgz
diff --git a/Dockerfile b/Dockerfile
index a4896f63..e3346423 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,16 +2,20 @@ FROM ruby:4.0.5-alpine AS download
WORKDIR /fonts
-RUN apk --no-cache add wget && \
+RUN apk --no-cache add wget unzip && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
- wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-musl-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
+ wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260613/pdfium-musl-$(uname -m).zip" && \
+ case "$(uname -m)" in \
+ x86_64) echo "2c953ff72ee2dda07e7fc577e25841cc3d6464468a7c5adfaea574efcbc3b90b pdfium-linux.zip" ;; \
+ aarch64) echo "23bbe287d2753fdb05741c7660647eb0ef0d2e4da2ce0722bfa9d9d455bd64e2 pdfium-linux.zip" ;; \
+ esac | sha256sum -c - && \
mkdir -p /pdfium-linux && \
- tar -xzf pdfium-linux.tgz -C /pdfium-linux
+ unzip -q pdfium-linux.zip -d /pdfium-linux
FROM ruby:4.0.5-alpine AS webpack
@@ -48,7 +52,7 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app
-RUN apk add --no-cache libpq vips redis onnxruntime && \
+RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -82,7 +86,7 @@ COPY --chown=docuseal:docuseal .version ./public/version
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
-COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
+COPY --from=download /pdfium-linux/licenses/ /usr/lib/libpdfium-licenses/
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
diff --git a/app/controllers/template_documents_crop_controller.rb b/app/controllers/template_documents_crop_controller.rb
new file mode 100644
index 00000000..c09b084c
--- /dev/null
+++ b/app/controllers/template_documents_crop_controller.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class TemplateDocumentsCropController < ApplicationController
+ load_and_authorize_resource :template
+ before_action :load_attachment
+
+ rescue_from Leptonica::LeptonicaError do
+ render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
+ end
+
+ def index
+ render json: { corners: Leptonica.detect_document_corners(@attachment.download) }
+ end
+
+ def create
+ authorize!(:update, @template)
+
+ document = Templates::CreateDocumentCrop.call(@template, @attachment, crop_params)
+
+ render json: {
+ document: document.as_json(
+ methods: %i[metadata signed_key],
+ include: {
+ preview_images: { methods: %i[url metadata filename] }
+ }
+ )
+ }
+ end
+
+ private
+
+ def load_attachment
+ @attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
+ end
+
+ def crop_params
+ params.permit(:scan, :rotate, :flip_h, :flip_v, corners: [%i[x y]])
+ end
+end
diff --git a/app/controllers/template_documents_modify_controller.rb b/app/controllers/template_documents_modify_controller.rb
new file mode 100644
index 00000000..30db1fd9
--- /dev/null
+++ b/app/controllers/template_documents_modify_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class TemplateDocumentsModifyController < ApplicationController
+ load_and_authorize_resource :template
+
+ def create
+ authorize!(:update, @template)
+
+ documents_layout =
+ params.require(:documents).map do |item|
+ item.permit(:attachment_uuid,
+ pages: [:attachment_uuid, :page, :rotate,
+ { redact: [%i[x y w h]], replaced_page: %i[attachment_uuid page] }]).to_h
+ end
+
+ Templates::ModifyDocuments.call(@template, documents_layout)
+
+ render json: {
+ schema: @template.schema,
+ fields: @template.fields,
+ submitters: @template.submitters,
+ documents: @template.schema_documents.reload.preload(:blob, preview_images_attachments: :blob).as_json(
+ methods: %i[metadata signed_key],
+ include: {
+ preview_images: { methods: %i[url metadata filename] }
+ }
+ )
+ }
+ rescue Templates::ModifyDocuments::InvalidLayout
+ render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
+ end
+end
diff --git a/app/controllers/template_documents_page_objects_controller.rb b/app/controllers/template_documents_page_objects_controller.rb
new file mode 100644
index 00000000..de9aa0b3
--- /dev/null
+++ b/app/controllers/template_documents_page_objects_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class TemplateDocumentsPageObjectsController < ApplicationController
+ load_and_authorize_resource :template
+
+ def index
+ attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
+
+ render json: Templates::ModifyDocuments.page_objects(attachment, params[:page].to_i)
+ end
+end
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue
index 8bb92422..098fcfb5 100644
--- a/app/javascript/template_builder/builder.vue
+++ b/app/javascript/template_builder/builder.vue
@@ -320,6 +320,7 @@
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@reorder="reorderFields"
+ @edit="editModalDocumentUuid = item.attachment_uuid"
@down="moveDocument(item, 1)"
@change="save"
/>
@@ -685,6 +686,17 @@
@close="isRevisionsModalOpen = false"
@apply="onRevisionApply"
/>
+
@@ -704,6 +716,7 @@ import DocumentControls from './controls'
import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
import RevisionsModal from './revisions_modal'
+import DocumentsEditorModal from './documents_editor_modal'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory, IconX } from '@tabler/icons-vue'
import { v4 } from 'uuid'
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
@@ -747,7 +760,8 @@ export default {
IconHistory,
IconDeviceFloppy,
IconX,
- RevisionsModal
+ RevisionsModal,
+ DocumentsEditorModal
},
provide () {
return {
@@ -1103,6 +1117,7 @@ export default {
isDragFile: false,
isMathLoaded: false,
isRevisionsModalOpen: false,
+ editModalDocumentUuid: null,
revisions: [],
beforeRevisionSnapshot: null,
zoomLevel: 1
@@ -1678,7 +1693,7 @@ export default {
ref.x = e.clientX - ref.offsetX
ref.y = e.clientY - ref.offsetY
- } else if (e.dataTransfer?.types?.includes('Files')) {
+ } else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid) {
this.isDragFile = true
}
},
@@ -2121,6 +2136,10 @@ export default {
}
},
onKeyDown (event) {
+ if (this.editModalDocumentUuid) {
+ return
+ }
+
if (event.key === 'Tab' && document.activeElement === document.body) {
event.stopImmediatePropagation()
event.preventDefault()
@@ -3139,6 +3158,22 @@ export default {
onDocumentsReplaceAndTemplateClone (template) {
window.Turbo.visit(`/templates/${template.id}/edit`)
},
+ onDocumentsModified (data) {
+ this.template.schema = data.schema
+ this.template.fields = data.fields
+ this.template.submitters = data.submitters
+ this.template.documents = data.documents
+
+ this.selectedAreasRef.value = []
+
+ if (!this.template.submitters.find((s) => s.uuid === this.selectedSubmitter?.uuid)) {
+ this.selectedSubmitter = this.template.submitters[0]
+ }
+
+ this.editModalDocumentUuid = null
+
+ this.save()
+ },
moveDocument (item, direction) {
const currentIndex = this.template.schema.indexOf(item)
diff --git a/app/javascript/template_builder/documents_editor_crop.vue b/app/javascript/template_builder/documents_editor_crop.vue
new file mode 100644
index 00000000..2916ef91
--- /dev/null
+++ b/app/javascript/template_builder/documents_editor_crop.vue
@@ -0,0 +1,328 @@
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/template_builder/documents_editor_modal.vue b/app/javascript/template_builder/documents_editor_modal.vue
new file mode 100644
index 00000000..1d25c8c3
--- /dev/null
+++ b/app/javascript/template_builder/documents_editor_modal.vue
@@ -0,0 +1,1166 @@
+
+
+
+
+
+
+ {{ t('edit_documents') }}
+
+
×
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+ {{ t('upload_to_document').replace('{document}', doc.name) }}
+
+
+
+
+
+
+ {{ t('add_a_new_document') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/template_builder/documents_editor_page.vue b/app/javascript/template_builder/documents_editor_page.vue
new file mode 100644
index 00000000..1bdc93e8
--- /dev/null
+++ b/app/javascript/template_builder/documents_editor_page.vue
@@ -0,0 +1,224 @@
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('page') }} {{ pageNumber }}
+
+
+
+
+
diff --git a/app/javascript/template_builder/documents_editor_redact.vue b/app/javascript/template_builder/documents_editor_redact.vue
new file mode 100644
index 00000000..7e3a792c
--- /dev/null
+++ b/app/javascript/template_builder/documents_editor_redact.vue
@@ -0,0 +1,413 @@
+
+
+
+
+
![]()
+
+
+
+
+ {{ t('there_is_no_text_to_redact_on_this_page') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js
index aa3c69a7..8f5c02b6 100644
--- a/app/javascript/template_builder/i18n.js
+++ b/app/javascript/template_builder/i18n.js
@@ -96,6 +96,23 @@ const en = {
add_pdf_documents_or_images: 'Add PDF documents or images',
add_documents_or_images: 'Add documents or images',
add_a_new_document: 'Add a new document',
+ edit_documents: 'Edit documents',
+ move_forward: 'Move forward',
+ move_backward: 'Move backward',
+ remove_page: 'Remove page',
+ merge_with_previous: 'Merge with previous',
+ merge_with_next: 'Merge with next',
+ move_up: 'Move up',
+ move_down: 'Move down',
+ rotate: 'Rotate',
+ redact: 'Redact',
+ crop: 'Crop',
+ crop_and_scan: 'Crop & Scan',
+ flip_horizontal: 'Flip horizontal',
+ flip_vertical: 'Flip vertical',
+ there_is_no_text_to_redact_on_this_page: 'This page contains only images. Redact tool can be used only with text pages',
+ reset: 'Reset',
+ upload_to_document: 'Upload to "{document}"',
replace_existing_document: 'Replace existing document',
clone_and_replace_documents: 'Clone and replace documents',
required: 'Required',
@@ -319,6 +336,23 @@ const es = {
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
add_documents_or_images: 'Agregar documentos o imágenes',
add_a_new_document: 'Agregar un nuevo documento',
+ edit_documents: 'Editar documentos',
+ move_forward: 'Mover adelante',
+ move_backward: 'Mover atrás',
+ remove_page: 'Eliminar página',
+ merge_with_previous: 'Combinar con el anterior',
+ merge_with_next: 'Combinar con el siguiente',
+ move_up: 'Mover arriba',
+ move_down: 'Mover abajo',
+ rotate: 'Rotar',
+ redact: 'Censurar',
+ crop: 'Recortar',
+ crop_and_scan: 'Recortar y escanear',
+ flip_horizontal: 'Voltear horizontal',
+ flip_vertical: 'Voltear vertical',
+ there_is_no_text_to_redact_on_this_page: 'Esta página contiene solo imágenes. La herramienta de censura solo puede usarse con páginas de texto',
+ reset: 'Restablecer',
+ upload_to_document: 'Subir a "{document}"',
replace_existing_document: 'Reemplazar documento existente',
clone_and_replace_documents: 'Clonar y reemplazar documentos',
required: 'Requerido',
@@ -548,6 +582,23 @@ const it = {
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
add_documents_or_images: 'Aggiungi documenti o immagini',
add_a_new_document: 'Aggiungi un nuovo documento',
+ edit_documents: 'Modifica documenti',
+ move_forward: 'Sposta avanti',
+ move_backward: 'Sposta indietro',
+ remove_page: 'Rimuovi pagina',
+ merge_with_previous: 'Unisci al precedente',
+ merge_with_next: 'Unisci al successivo',
+ move_up: 'Sposta su',
+ move_down: 'Sposta giù',
+ rotate: 'Ruota',
+ redact: 'Oscura',
+ crop: 'Ritaglia',
+ crop_and_scan: 'Ritaglia e scansiona',
+ flip_horizontal: 'Rifletti orizzontale',
+ flip_vertical: 'Rifletti verticale',
+ there_is_no_text_to_redact_on_this_page: 'Questa pagina contiene solo immagini. Lo strumento di oscuramento può essere usato solo con pagine di testo',
+ reset: 'Reimposta',
+ upload_to_document: 'Carica in "{document}"',
replace_existing_document: 'Sostituisci documento esistente',
clone_and_replace_documents: 'Clona e sostituisci documenti',
required: 'Obbligatorio',
@@ -771,6 +822,23 @@ const pt = {
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
add_documents_or_images: 'Adicionar documentos ou imagens',
add_a_new_document: 'Adicionar um novo documento',
+ edit_documents: 'Editar documentos',
+ move_forward: 'Mover para frente',
+ move_backward: 'Mover para trás',
+ remove_page: 'Remover página',
+ merge_with_previous: 'Mesclar com o anterior',
+ merge_with_next: 'Mesclar com o próximo',
+ move_up: 'Mover para cima',
+ move_down: 'Mover para baixo',
+ rotate: 'Girar',
+ redact: 'Censurar',
+ crop: 'Cortar',
+ crop_and_scan: 'Cortar e digitalizar',
+ flip_horizontal: 'Inverter horizontal',
+ flip_vertical: 'Inverter vertical',
+ there_is_no_text_to_redact_on_this_page: 'Esta página contém apenas imagens. A ferramenta de censura só pode ser usada com páginas de texto',
+ reset: 'Redefinir',
+ upload_to_document: 'Enviar para "{document}"',
replace_existing_document: 'Substituir documento existente',
clone_and_replace_documents: 'Clonar e substituir documentos',
required: 'Obrigatório',
@@ -1000,6 +1068,23 @@ const fr = {
add_pdf_documents_or_images: 'Ajouter des documents PDF ou des images',
add_documents_or_images: 'Ajouter des documents ou des images',
add_a_new_document: 'Ajouter un nouveau document',
+ edit_documents: 'Modifier les documents',
+ move_forward: 'Déplacer en avant',
+ move_backward: 'Déplacer en arrière',
+ remove_page: 'Supprimer la page',
+ merge_with_previous: 'Fusionner avec le précédent',
+ merge_with_next: 'Fusionner avec le suivant',
+ move_up: 'Déplacer vers le haut',
+ move_down: 'Déplacer vers le bas',
+ rotate: 'Pivoter',
+ redact: 'Caviarder',
+ crop: 'Rogner',
+ crop_and_scan: 'Rogner et numériser',
+ flip_horizontal: 'Miroir horizontal',
+ flip_vertical: 'Miroir vertical',
+ there_is_no_text_to_redact_on_this_page: 'Cette page ne contient que des images. L\'outil de caviardage ne peut être utilisé qu\'avec des pages de texte',
+ reset: 'Réinitialiser',
+ upload_to_document: 'Téléverser dans "{document}"',
replace_existing_document: 'Remplacer le document existant',
clone_and_replace_documents: 'Cloner et remplacer des documents',
required: 'Obligatoire',
@@ -1226,6 +1311,23 @@ const de = {
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
add_a_new_document: 'Neues Dokument hinzufügen',
+ edit_documents: 'Dokumente bearbeiten',
+ move_forward: 'Nach vorne',
+ move_backward: 'Nach hinten',
+ remove_page: 'Seite entfernen',
+ merge_with_previous: 'Mit vorherigem zusammenführen',
+ merge_with_next: 'Mit nächstem zusammenführen',
+ move_up: 'Nach oben verschieben',
+ move_down: 'Nach unten verschieben',
+ rotate: 'Drehen',
+ redact: 'Schwärzen',
+ crop: 'Zuschneiden',
+ crop_and_scan: 'Zuschneiden & Scan',
+ flip_horizontal: 'Horizontal spiegeln',
+ flip_vertical: 'Vertikal spiegeln',
+ there_is_no_text_to_redact_on_this_page: 'Diese Seite enthält nur Bilder. Das Schwärzungswerkzeug kann nur mit Textseiten verwendet werden',
+ reset: 'Zurücksetzen',
+ upload_to_document: 'In "{document}" hochladen',
replace_existing_document: 'Vorhandenes Dokument ersetzen',
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
required: 'Erforderlich',
@@ -1452,6 +1554,23 @@ const nl = {
add_pdf_documents_or_images: 'PDF-documenten of afbeeldingen toevoegen',
add_documents_or_images: 'Documenten of afbeeldingen toevoegen',
add_a_new_document: 'Nieuw document toevoegen',
+ edit_documents: 'Documenten bewerken',
+ move_forward: 'Naar voren',
+ move_backward: 'Naar achteren',
+ remove_page: 'Pagina verwijderen',
+ merge_with_previous: 'Samenvoegen met vorige',
+ merge_with_next: 'Samenvoegen met volgende',
+ move_up: 'Omhoog verplaatsen',
+ move_down: 'Omlaag verplaatsen',
+ rotate: 'Draaien',
+ redact: 'Redigeren',
+ crop: 'Bijsnijden',
+ crop_and_scan: 'Bijsnijden en scannen',
+ flip_horizontal: 'Horizontaal spiegelen',
+ flip_vertical: 'Verticaal spiegelen',
+ there_is_no_text_to_redact_on_this_page: 'Deze pagina bevat alleen afbeeldingen. De redactietool kan alleen worden gebruikt met tekstpagina\'s',
+ reset: 'Opnieuw instellen',
+ upload_to_document: 'Uploaden naar "{document}"',
replace_existing_document: 'Bestaand document vervangen',
clone_and_replace_documents: 'Documenten klonen en vervangen',
required: 'Vereist',
diff --git a/app/javascript/template_builder/preview.vue b/app/javascript/template_builder/preview.vue
index efd21777..58b49c9d 100644
--- a/app/javascript/template_builder/preview.vue
+++ b/app/javascript/template_builder/preview.vue
@@ -72,6 +72,15 @@
style="min-width: 170px"
@click="closeDropdown"
>
+
+
+