<button class="btn btn-sm btn-dark text-danger" (click)="remove(i); $event.stopPropagation()" title="Delete page" i18n-title>
<i-bs name="trash"></i-bs>
</button>
- <button class="btn btn-sm btn-dark" (click)="toggleSplit(i); $event.stopPropagation()" title="Split document here" i18n-title>
+ <button class="btn btn-sm btn-dark" (click)="toggleSplit(i); $event.stopPropagation()" title="Add / remove document split here" i18n-title>
<i-bs name="scissors"></i-bs>
</button>
</div>
</div>
<div class="border-end border-bottom bg-light py-1 px-2 document-check z-10">
<div class="form-check">
- <input type="checkbox" class="form-check-input" id="page{{i}}" [checked]="p.selected" (click)="toggleSelection(i); $event.stopPropagation()">
+ <input type="checkbox" class="form-check-input" id="page{{i}}" [checked]="p.selected" (click)="toggleSelection(i); $event.stopPropagation()">
<label class="form-check-label" for="page{{i}}"></label>
</div>
</div>
}
</div>
</div>
-<div class="modal-footer">
- <div class="form-check form-switch me-auto">
- <input class="form-check-input" type="checkbox" id="deleteSwitch" [(ngModel)]="deleteOriginal">
- <label class="form-check-label" for="deleteSwitch" i18n>Delete original after edit</label>
+<div class="modal-footer flex-column">
+ <div class="d-flex w-100 justify-content-between align-items-center">
+ <div class="btn-group" role="group">
+ <input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="EditMode.Create" id="editModeCreate" name="editmode">
+ <label for="editModeCreate" class="btn btn-outline-primary btn-sm">
+ <i-bs name="plus"></i-bs>
+ <span class="form-check-label ms-1" i18n>Create new document(s)</span>
+ </label>
+ <input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="EditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
+ <label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
+ <i-bs name="pencil"></i-bs>
+ <span class="form-check-label ms-2" i18n>Update existing document</span>
+ </label>
+ </div>
+ @if (editMode === EditMode.Create) {
+ <div class="form-check ms-3">
+ <input class="form-check-input" type="checkbox" id="copyMeta" [(ngModel)]="includeMetadata">
+ <label class="form-check-label" for="copyMeta" i18n>Copy metadata</label>
+ </div>
+ <div class="form-check ms-3">
+ <input class="form-check-input" type="checkbox" id="deleteOriginal" [(ngModel)]="deleteOriginal">
+ <label class="form-check-label" for="deleteOriginal" i18n>Delete original</label>
+ </div>
+ }
+ <button type="button" class="btn ms-auto me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
+ <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
</div>
- <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
- <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
</div>
loaded?: boolean
}
+enum EditMode {
+ Update = 'update',
+ Create = 'create',
+}
+
@Component({
selector: 'pngx-pdf-editor',
templateUrl: './pdf-editor.component.html',
],
})
export class PDFEditorComponent extends ConfirmDialogComponent {
+ public EditMode = EditMode
+
private documentService = inject(DocumentService)
- activeModal = inject(NgbActiveModal)
+ activeModal: NgbActiveModal = inject(NgbActiveModal)
documentID: number
pages: PageOperation[] = []
totalPages = 0
- deleteOriginal = false
+ editMode: EditMode = EditMode.Create
+ deleteOriginal: boolean = false
+ updateDocument: boolean = false
+ includeMetadata: boolean = true
get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID)
toggleSplit(i: number) {
this.pages[i].splitAfter = !this.pages[i].splitAfter
+ if (this.pages[i].splitAfter) {
+ // force create mode
+ this.editMode = EditMode.Create
+ }
}
selectAll() {
return this.pages.some((p) => p.selected)
}
+ hasSplit(): boolean {
+ return this.pages.some((p) => p.splitAfter)
+ }
+
drop(event: CdkDragDrop<PageOperation[]>) {
moveItemInArray(this.pages, event.previousIndex, event.currentIndex)
}
method: 'edit_pdf',
parameters: {
operations: [{ page: 1, rotate: 0, doc: 0 }],
- delete_original: false,
+ update_document: false,
+ include_metadata: true,
},
})
req.flush(true)
this.documentsService
.bulkEdit([this.document.id], 'edit_pdf', {
operations: modal.componentInstance.getOperations(),
- delete_original: modal.componentInstance.deleteOriginal,
+ update_document: modal.componentInstance.updateDocument,
+ include_metadata: modal.componentInstance.includeMetadata,
})
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
operations: list[dict],
*,
delete_original: bool = False,
+ update_document: bool = False,
+ include_metadata: bool = True,
user: User | None = None,
) -> Literal["OK"]:
"""
if op.get("rotate"):
dst.pages[-1].rotate(op["rotate"], relative=True)
+ if update_document:
+ if len(pdf_docs) != 1:
+ logger.error(
+ "Update requested but multiple output documents specified",
+ )
+ return "ERROR"
+ pdf = pdf_docs[0]
+ pdf.remove_unreferenced_resources()
+ pdf.save(doc.source_path)
+ doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
+ doc.page_count = len(pdf.pages)
+ doc.save()
+ update_document_content_maybe_archive_file.delay(document_id=doc.id)
+ else:
consume_tasks = []
- overrides: DocumentMetadataOverrides = (
+ overrides = (
DocumentMetadataOverrides().from_document(doc)
+ if include_metadata
+ else DocumentMetadataOverrides()
)
if user is not None:
overrides.owner_id = user.id
except Exception as e:
logger.exception(f"Error editing document {doc.id}: {e}")
+ return "ERROR"
return "OK"
raise serializers.ValidationError("rotate must be an integer")
if "doc" in op and not isinstance(op["doc"], int):
raise serializers.ValidationError("doc must be an integer")
- if "delete_original" in parameters:
- if not isinstance(parameters["delete_original"], bool):
- raise serializers.ValidationError("delete_original must be a boolean")
+ if "update_document" in parameters:
+ if not isinstance(parameters["update_document"], bool):
+ raise serializers.ValidationError("update_document must be a boolean")
else:
- parameters["delete_original"] = False
+ parameters["update_document"] = False
+ if "include_metadata" in parameters:
+ if not isinstance(parameters["include_metadata"], bool):
+ raise serializers.ValidationError("include_metadata must be a boolean")
+ else:
+ parameters["include_metadata"] = True
+
+ if parameters["update_document"]:
+ max_idx = max(op.get("doc", 0) for op in parameters["operations"])
+ if max_idx > 0:
+ raise serializers.ValidationError(
+ "update_document only allowed with a single output document",
+ )
def validate(self, attrs):
method = attrs["method"]
method in [bulk_edit.merge, bulk_edit.split]
and parameters["delete_originals"]
)
- or (method == bulk_edit.edit_pdf and parameters["delete_original"])
+ or (method == bulk_edit.edit_pdf and parameters["update_document"])
):
has_perms = user_is_owner_of_all_documents
# check global add permissions for methods that create documents
if (
has_perms
- and method in [bulk_edit.split, bulk_edit.merge, bulk_edit.edit_pdf]
- and not user.has_perm(
- "documents.add_document",
+ and (
+ method in [bulk_edit.split, bulk_edit.merge]
+ or (
+ method == bulk_edit.edit_pdf
+ and not parameters["update_document"]
+ )
)
+ and not user.has_perm("documents.add_document")
):
has_perms = False
method in [bulk_edit.merge, bulk_edit.split]
and parameters["delete_originals"]
)
- or (method == bulk_edit.edit_pdf and parameters["delete_original"])
)
and not user.has_perm("documents.delete_document")
):